你可能错过的 Rails 技巧 —Posted by bigcircle
记得前段时间 RailsConf2012 之后看过一个不错的pdf,10 things you didn’t know rails could do
说是10个,但是给出了42个实例,这几天抽空又回味了下,料很多,写的很好,顺便总结学习下
Pass 掉第一个 fridayhug,我们是开心拥抱每一天
%w(action_controller/railtie coderay markaby).map &method(:require) run TheSmallestRailsApp ||= Class.new(Rails::Application) { config.secret_token = routes.append { root to: proc { [200, {"Content-Type" => "text/html"}, [Markaby::Builder.new.html { title @title = "The Smallest Rails App" h3 "I am #@title!" p "Here is my source code:" div { CodeRay.scan_file(__FILE__).div(line_numbers: :table) } p { a "Make me smaller", href: "//goo.gl/YdRpy" } }]] } }.to_s initialize! }
2 – 提醒功能 TODO
class UsersController < ApplicationController # TODO: Make it possible to create new users. end class User < ActiveRecord::Base # FIXME: Should token really be accessible? attr_accessible :bil, :email, :name, :token end
执行 rake notes
app/controllers/users_controller.rb: * [ 2] [TODO] Make it possible to create new users. app/models/user.rb: * [ 2] [FIXME] Should token really be accessible? app/views/articles/index.html.erb: * [ 1] [OPTIMIZE] Paginate this listing.
查看单独的 TODO / FIXME / OPTIMIZE
rake notes:todo app/controllers/users_controller.rb: * [ 2] Make it possible to create new users.
可以自定义提醒名称
class Article < ActiveRecord::Base belongs_to :user attr_accessible :body, :subject # JEG2: Add that code from your blog here. end
不过需要敲一长串,可以alias个快捷键
rake notes:custom ANNOTATION=JEG2 app/models/article.rb: * [ 4]Add that code from your blog here.
3 – 沙箱模式执行 rails c
rails c --sandbox
沙箱模式会有回滚事务机制,对数据库的操作在退出之前都会自动回滚到之前未修改的数据
4 – 在 rails c 控制台中使用 rails helper 方法
$ rails c Loading development environment (Rails 3.2.3) >> helper.number_to_currency(100) => "$100.00" >> helper.time_ago_in_words(3.days.ago) => "3 days"
5 – 开发模式用 thin 代替 webrick
group :development do gem 'thin' end rails s thin / thin start
6 – 允许自定义配置
- lib/custom/railtie.rb module Custom class Railtie < Rails::Railtie config.custom = ActiveSupport::OrderedOptions.new end end - config/application.rb require_relative "../lib/custom/railtie" module Blog class Application < Rails::Application # ... config.custom.setting = 42 end end
7 – keep funny
作者给出了个介绍 ruby 以及一些相关 blog的网站 rubydramas,搞笑的是这个网站右上角标明
Powered by PHP
用 isitrails.com 检查了下,果然不是用 rails 做的,这个应该是作者分享 ppt 过程中的一个小插曲吧
8 -理解简写的迁移文件
rails g resources user name:string email:string token:string bio:text
字段会被默认为 string 属性,查看了下 源码,果然有初始化定义
rails g resources user name email token:string{6} bio:text
会生成用样的 migration 文件
class CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.string :name t.string :email t.string :token, :limit => 6 t.text :bio t.timestamps end end end
9 – 给 migration 添加索引
rails g resource user name:index email:uniq token:string{6} bio:text
会生成 name 和 email 的索引,同时限定 email 唯一
class CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.string :name t.string :email t.string :token, :limit => 6 t.text :bio t.timestamps end add_index :users, :name add_index :users, :email, :unique => true end end
10 – 添加关联关系
rails g resource article user:references subject body:text
会自动关联生成对应的 belongs_to 和 外键,并添加索引
class CreateArticles < ActiveRecord::Migration def change create_table :articles do |t| t.references :user t.string :subject t.text :body t.timestamps end add_index :articles, :user_id end end
class Article < ActiveRecord::Base belongs_to :user attr_accessible :body, :subject end
11 – 显示数据迁移记录
rake db:migrate:status
会输出 migration 的状态,这在解决迁移版本冲突的时候很有用
database: db/development.sqlite3 status Migration ID Migration Name --------------------------------------- up 20120414155612 Create users up 20120414160528 Create articles down 20120414161355 Create comments
12 – 导入 csv 文件
csv 文件内容如下
Name,Email James,james@example.com Dana,dana@example.com Summer,summer@example.com
创建 rake 任务导入 users 表
require 'csv' namespace :users do desc "Import users from a CSV file" task :import => :environment do path = ENV.fetch("CSV_FILE") { File.join(File.dirname(__FILE__), *%w[.. .. db data users.csv]) } CSV.foreach(path, headers: true, header_converters: :symbol) do |row| User.create(row.to_hash) end end end
13 – 数据库中储存 csv
class Article < ActiveRecord::Base require 'csv' module CSVSerializer module_function def load(field); field.to_s.parse_csv; end def dump(object); Array(object).to_csv; end end serialize :categories, CSVSerializer attr_accessible :body, :subject, :categories end
serialize 用于在文本字段储存序列化的值,如序列,Hash,Array等,例如
user = User.create(:preferences => { "background" => "black", "display" => large }) User.find(user.id).preferences # => { "background" => "black", "display" => large }
这个例子中将 CSVSerializer to_csv序列化之后储存在 categories 这个文本类型字段中
14 – 用 pluck 查询
$ rails c loading development environment(Rails 3.2.3) >> User.select(:email).map(&:email) User Load(0.1ms) SELECT email FROM "users" => ["james@example.com", "dana@example.com", "summer@example.com"] >> User.pluck(:email) (0.2ms) SELECT email FROM "users" => ["james@example.com", "dana@example.com", "summer@example.com"] >> User.uniq.pluck(:email) (0.2ms) SELECT DISTINCT email FROM "users" => ["james@example.com", "dana@example.com", "summer@example.com"]
pluck 的实现方式其实也是 select 之后再 map
def pluck(column_name) column_name = column_name.to_s klass.connection.select_all(select(column_name).arel).map! do |attributes| klass.type_cast_attribute(attributes.keys.first, klass.initialize_attributes(attributes)) end end
15 – 使用 group count
创建 article 关联 model event
rails g resource event article:belongs_to triggle
创建3条 edit 记录和10条 view 记录。 Event.count 标明有13条记录, group(:triggle).count 表示统计 triggle group 之后的数量,也可以用 count(:group => :trigger)
$ rails c
>> article = Article.last => #<article id:1="">>> {edit:3, view:10}.each do |trigger, count| ?> count.times do ?> Event.new(trigger: trigger).tap{ |e| e.article= article; e.save! } ?> end => {:edit => 3, :view => 10} >> Event.count => 13 >> Event.group(:trigger).count => {"edit" => 3, "view" => 10}</article>
16 – 覆盖关联关系
class Car < ActiveRecord::Base belongs_to :owner belongs_to :previous_owner, class_name: "Owner" def owner=(new_owner) self.previous_owner = owner super end end
car更改 owner 时,如果有了 new_owner,就把原 owner 赋给 previous_owner,注意要加上 super
17 – 构造示例数据
$ rails c Loading development environment (Rails 3.2.3) >> User.find(1) => # >> jeg2 = User.instantiate("id" => 1, "email" => " => # >> jeg2.name = "James Edward Gray II" => "James Edward Gray II" >> jeg2.save! => true >> User.find(1) james@example.com", ...>
伪造一条记录,并不是数据库中的真实数据,也不会把原有数据覆盖
18 – PostgreSQL 中使用无限制的 string
去掉适配器中对 string 长度的限制,这个应该是 PostgreSQL 数据库的特性
module PsqlApp class Application < Rails::Application # Switch to limitless strings initializer "postgresql.no_default_string_limit" do ActiveSupport.on_load(:active_record) do adapter = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter adapter::NATIVE_DATABASE_TYPES[:string].delete(:limit) end end end end
创建 user 表,bio 字符串
rails g resource user bio
$ rails c Loading development environment (Rails 3.2.3) >> very_long_bio = "X" * 10_000; :set => :set >> User.create!(bio: very_long_bio) => # >> User.last.bio.size => 10000
19 – PostgreSQL 中使用全文搜
rails g resource article subject body:text
更改迁移文件,添加索引和 articles_search_update 触发器
class CreateArticles < ActiveRecord::Migration def change create_table :articles do |t| t.string :subject t.text :body t.column :search, "tsvector" t.timestamps end execute <
Model 中添加 search 方法
class Article < ActiveRecord::Base attr_accessible :body, :subject def self.search(query) sql = sanitize_sql_array(["plainto_tsquery('english', ?)", query]) where( "search @@ #{sql}" ).order( "ts_rank_cd(search, #{sql}) DESC" ) end end
PostgreSQL 数据库没用过,这段看例子吧
$ rails c Loading development environment (Rails 3.2.3) >> Article.create!(subject: "Full Text Search") => #<article id:="" 1="">>> Article.create!(body: "A stemmed search.") => #<article id:="" 2="">>> Article.create!(body: "You won't find me!") => #<article id:="" 3="">>> Article.search("search").map { |a| a.subject || a.body } => ["Full Text Search", "A stemmed search."] >> Article.search("stemming").map { |a| a.subject || a.body } => ["A stemmed search."]</article></article></article>
21 – 每个用户使用不同的数据库
- user_database.rb def connect_to_user_database(name) config = ActiveRecord::Base.configurations["development"].merge("database" => "db/#{name}.sqlite3") ActiveRecord::Base.establish_connection(config) end
创建 rake 任务:新增用户数据库和创建
require "user_database" namespace :db do desc "Add a new user database" task :add => %w[environment load_config] do name = ENV.fetch("DB_NAME") { fail "DB_NAME is required" } connect_to_user_database(name) ActiveRecord::Base.connection end namespace :migrate do desc "Migrate all user databases" task :all => %w[environment load_config] do ActiveRecord::Migration.verbose = ENV.fetch("VERBOSE", "true") == "true" Dir.glob("db/*.sqlite3") do |file| next if file == "db/test.sqlite3" connect_to_user_database(File.basename(file, ".sqlite3")) ActiveRecord::Migrator.migrate( ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] && ENV["VERSION"].to_i ) do |migration| ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope) end end end end end
执行几个rake 任务创建不同的数据库
$ rails g resource user name $ rake db:add DB_NAME=ruby_rogues $ rake db:add DB_NAME=grays $ rake db:migrate:all == CreateUsers: migrating ================================== -- create_table(:users) -> 0.0008s == CreateUsers: migrated (0.0008s) ========================= == CreateUsers: migrating ================================== -- create_table(:users) -> 0.0007s == CreateUsers: migrated (0.0008s) =========================
创建几条记录,为不同的数据库创建不同的数据
$ rails c >> connect_to_user_database("ruby_rogues") => # >> User.create!(name: "Chuck") => # >> User.create!(name: "Josh") => # >> User.create!(name: "Avdi") => # ... >> connect_to_user_database("grays") => # >> User.create!(name: "James") => # >> User.create!(name: "Dana") => # >> User.create!(name: "Summer") => #
ApplicationController 里面添加 before_filter 根据登陆的二级域名判断连接哪个数据库
class ApplicationController < ActionController::Base protect_from_forgery before_filter :connect_to_database private def connect_to_database connect_to_user_database(request.subdomains.first) end end
中场休息时,去找了下 RailsConf 2012 的视频看了看,刚好看到关于 这篇 的介绍,片子还挺长,41分钟,演讲者长相和声音都很不符合大家对 Rails 的认知,大家有兴趣的可以去听听
21 – 自动写文件
class Comment < ActiveRecord::Base # ... Q_DIR = (Rails.root + "comment_queue").tap(&:mkpath) after_save :queue_comment def queue_comment File.atomic_write(Q_DIR + "#{id}.txt") do |f| f.puts "Article: #{article.subject}" f.puts "User: #{user.name}" f.puts body end end end
找了下 Api 是 Rails 对 Ruby 基础类的扩展
22 – 合并嵌套 Hash
$ rails c Loading development environment (Rails 3.2.3) >> {nested: {one: 1}}.merge(nested: {two: 2}) => {:nested=>{:two=>2}} >> {nested: {one: 1}}.deep_merge(nested: {two: 2}) => {:nested=>{:one=>1, :two=>2}}
主要是用到了 deep_merge 合并相同的 key
23 – Hash except
$ rails c Loading development environment (Rails 3.2.3) >> params = {controller: "home", action: "index", from: "Google"} => {:controller=>"home", :action=>"index", :from=>"Google"} >> params.except(:controller, :action) => {:from=>"Google"}
这个方法经常会用到,可能用的人也很多
24 – add defaults to Hash
$ rails c Loading development environment (Rails 3.2.3) >> {required: true}.merge(optional: true) => {:required=>true, :optional=>true} >> {required: true}.reverse_merge(optional: true) => {:optional=>true, :required=>true} >> {required: true, optional: false}.merge(optional: true) => {:required=>true, :optional=>true} >> {required: true, optional: false}.reverse_merge(optional: true) => {:optional=>false, :required=>true}
这几个都是对 Hash 类的增强,merge 会替换原有相同 key 的值,但 reverse_merge 不会
从源码就可以看出,会事先 copy 一份 default hash
def reverse_merge(other_hash) super self.class.new_from_hash_copying_default(other_hash) end
25 – String.value? 方法
看下面的几个例子
$ rails c Loading development environment (Rails 3.2.3) >> env = Rails.env => “development” >> env.development? => true >> env.test? => false >> “magic”.inquiry.magic? => true >> article = Article.first => #
env, “magic” 可以直接使用 value? 的方法,这个扩展是 String#inquiry 方法
def inquiry ActiveSupport::StringInquirer.new(self) end # 用method_missing 实现 def method_missing(method_name, *arguments) if method_name.to_s[-1,1] == "?" self == method_name.to_s[0..-2] else super end end
类型的一个例子,同样用到了 inquiry 方法
class Article < ActiveRecord::Base # ... STATUSES = %w[Draft Published] validates_inclusion_of :status, in: STATUSES def method_missing(method, *args, &block) if method =~ /\A#{STATUSES.map(&:downcase).join("|")}\?\z/ status.downcase.inquiry.send(method) else super end end end
26 - 让你成为杂志的封面 (暖场之用)
搞笑哥拿出了 DHH 当选 Linux journal 杂志封面的图片,会场也是哄堂大笑 ^.^
27 – 隐藏注释
<h1>Home Page</h1> # 生成的 html<!-- HTML comments stay in the rendered content --> <h1>Home Page</h1>
这个一下没看懂。。试了下项目里面的代码,原来是隐藏的意思。。 28 – 理解更短的 erb 语法
# ... module Blog class Application < Rails::Application # Broken: config.action_view.erb_trim_mode = '%' ActionView::Template::Handlers::ERB.erb_implementation = Class.new(ActionView::Template::Handlers::Erubis) do include ::Erubis::PercentLineEnhancer end end end end
接着在 view 页面替换用 % 表示原来,有点像 slim
% if current_user.try(:admin?) % end
29 – 用 block 避免视图层赋值
<table> <% @cart.products.each do |product| %> <tr> <td><%= product.name %></td> <td><%= number_to_currency product.price %></td> </tr> <% end %> <tr> <td>Subtotal</td> <td><%= number_to_currency @cart.total %></td> </tr> <tr> <td>Tax</td> <td><%= number_to_currency(tax = calculate_tax(@cart.total)) %></td> </tr> <tr> <td>Total</td> <td><%= number_to_currency(@cart.total + tax) %></td> </tr> </table>
tax = calculate_tax(@cart.total)
会先被赋值再被下面引用 用 block 重构下,把逻辑代码放到 helper 里面
module CartHelper def calculate_tax(total, user = current_user) tax = TaxTable.for(user).calculate(total) if block_given? yield tax else tax end end end
<table> <% @cart.products.each do |product| %> <tr> <td><%= product.name %></td> <td><%= number_to_currency product.price %></td> </tr> <% end %> <tr> <td>Subtotal</td> <td><%= number_to_currency @cart.total %></td> </tr> <% calculate_tax @cart.total do |tax| %> <tr> <td>Tax</td> <td><%= number_to_currency tax %></td> </tr> <tr> <td>Total</td> <td><%= number_to_currency(@cart.total + tax) %></td> </tr> <% end %> </table>
30 – 同时生成多个标签
<h1>Articles</h1> <% @articles.each do |article| %> <%= content_tag_for(:div, article) do %> <h2><%= article.subject %></h2> <% end %> <% end %><%= content_tag_for(:div, @articles) do |article| %> <h2><%= article.subject %></h2> <% end %>
content_tag_for 具体用法可以参考 Api,意思比较明白 to_partial_path 是 ActiveModel 內建的实例方法,返回一个和可识别关联对象路径的字符串,原文是这么说的,目前还没看明白这么用的目的在哪
这篇 blog 介绍了4个最喜欢的 Rails3.2 隐藏特性,
这4条都在这个系列中,作者可能也是从这学来的吧
31 – Render Any Object
class Event < ActiveRecord::Base # ... def to_partial_path "events/#{trigger}" # events/edit or events/view end end
to_partial_path 是 ActiveModel 內建的实例方法,返回一个和可识别关联对象路径的字符串,原文是这么说的,目前还没看明白这么用的目的在哪
Returns a string identifying the path associated with the object. ActionPack uses this to find a suitable partial to represent the object.
32 – 生成 group option
%w[One Two Three], "Group B" => %w[One Two Three] ) ) %>
这个其实就是用到了 grouped_options_for_select ,我在前面的 博文 提到过这几个 select 的用法
33 -定制你自己喜欢的 form 表单
class LabeledFieldsWithErrors < ActionView::Helpers::FormBuilder def errors_for(attribute) if (errors = object.errors[attribute]).any? @template.content_tag(:span, errors.to_sentence, class: "error") end end def method_missing(method, *args, &block) if %r{ \A (?labeled_)? (?\w+?) (?_with_errors)? \z }x =~ method and respond_to?(wrapped) and [labeled, with_errors].any?(&:present?) attribute, tags = args.first, [ ] tags << label(attribute) if labeled.present? tags << send(wrapped, *args, &block) tags << errors_for(attribute) if with_errors.present? tags.join(" ").html_safe else super end end end
定义了几个不想去看懂的 method_missing 方法。。 修改 application.rb,添加配置
class Application < Rails::Application # ... require "labeled_fields_with_errors" config.action_view.default_form_builder = LabeledFieldsWithErrors config.action_view.field_error_proc = ->(field, _) { field } end
创建 form 表单可以这样书写
<%= form_for @article do |f| %> <p><%= f.text_field <p><%= f.labeled_text_field <p><%= f.text_field_with_errors <p><%= f.labeled_text_field_with_errors :subject %></p> <%= f.submit %> <% end %>
生成如下的 html 页面
<p><input id="article_subject" name="article[subject]" size="30" type="text" value="" /></p> <p><label for="article_subject">Subject</label> <input id="article_subject" name="article[subject]" size="30" type="text" value="" /></p> <p><input id="article_subject" name="article[subject]" size="30" type="text" value="" /> <span class="error">can't be blank</span></p> <p><label for="article_subject">Subject</label> <input id="article_subject" name="article[subject]" size="30" type="text" value="" /> <span class="error">can't be blank</span></p> <!-- ... -->
不是很喜欢这种方式,反而把简单的html搞复杂了,让后来维护的人增加额外的学习成本 不是很喜欢这种方式,反而把简单的html搞复杂了,让后来维护的人增加额外的学习成本
34 - Inspire theme songs about your work (再次暖场时刻)
2011年 Farmhouse Conf 上主持人 Ron Evans 专门用口琴演奏了为大神 Tenderlove 写的歌 - Ruby Hero Tenderlove! ,听了半天不知道唱的啥。。 想找下有没有美女 Rubist, 看了下貌似没有,都是大妈,这位 Meghann Millard 尚可远观,大姐装束妖娆,手握纸条,蚊蝇环绕,不时微笑,长的真有点像 gossip girl 里面的 Jenny Humphrey
35 - 灵活的异常操作
修改 application.rb 定义
class Application < Rails::Application # ... config.exceptions_app = routes end
每次有异常时路由都会被调用,你可以用下面的方法简单 render 404 页面
match "/404", :to => "errors#not_found"
这个例子也在开头提到的那篇博文里面,感兴趣可以去自己研究下
36 – 给 Sinatra 添加路由
- Gemfile source 'https://rubygems.org' # ... gem "resque", require: "resque/server" module AdminValidator def matches?(request) if (id = request.env["rack.session"]["user_id"]) current_user = User.find_by_id(id) current_user.try(:admin?) else false end end end
挂载 Resque::Server 至 /admin/resqu
Blog::Application.routes.draw do # ... require "admin_validator" constraints AdminValidator do mount Resque::Server, at: "/admin/resque" end end
这个也没有试验,不清楚具体用法,sinatra 平时也基本不用
37 – 导出CSV流
class ArticlesController < ApplicationController def index respond_to do |format| format.html do @articles = Article.all end format.csv do headers["Content-Disposition"] = %Q{attachment; filename="articles.csv"} self.response_body = Enumerator.new do |response| csv = CSV.new(response, row_sep: "\n") csv << %w[Subject Created Status] Article.find_each do |article| csv << [ article.subject, article.created_at.to_s(:long), article.status ]  end end end end end # ... end 导出 csv 是很常用的功能,很多时候报表都需要,这个还是比较实用的 38 - do some work in the background 给 articles 添加文本类型 stats 字段
rails g migration add_stats_to_articles stats:text
添加一个计算 stats 方法 和 一个 after_create 方法,在创建一条记录后,会把 calculate_stats 添加到 Queue 队列,当队列中有任务时,后台创建一个线程执行该 job
class Article < ActiveRecord::Base # ... serialize :stats def calculate_stats words = Hash.new(0) body.to_s.scan(/\S+/) { |word| words[word] += 1 } sleep 10 # simulate a lot of work self.stats = {words: words} end require "thread" def self.queue; @queue ||= Queue.new end def self.thread @thread ||= Thread.new do while job = queue.pop job.call end end end thread # start the Thread after_create :add_stats def add_stats self.class.queue << -> { calculate_stats; save } end end
添加一条记录,10秒后会自动给该记录 stats 字段添加 words Hash
$ rails c Loading development environment (Rails 3.2.3) >> Article.create!(subject: "Stats", body: "Lorem ipsum..."); Time.now.strftime("%H:%M:%S") => "15:24:10" >> [Article.last.stats, Time.now.strftime("%H:%M:%S")] => [nil, "15:24:13"] >> [Article.last.stats, Time.now.strftime("%H:%M:%S")] =>[{:words=>{"Lorem"=>1, "ipsum"=>1, ...}, "15:24:26"]
39 – 用 Rails 生成静态站点
修改 config/environment/development.rb
Static::Application.configure do # ... # Show full error reports and disable caching config.consider_all_requests_local = true config.action_controller.perform_caching = !!ENV["GENERATING_SITE"] # ... # Don't fallback to assets pipeline if a precompiled asset is missed config.assets.compile = !ENV["GENERATING_SITE"] # Generate digests for assets URLs config.assets.digest = !!ENV["GENERATING_SITE"] # ... end class ApplicationController < ActionController::Base protect_from_forgery if ENV["GENERATING_SITE"] after_filter do |c| c.cache_page(nil, nil, Zlib::BEST_COMPRESSION) end end end
修改 rake static:generate 任务
require "open-uri" namespace :static do desc "Generate a static copy of the site" task :generate => %w[environment assets:precompile] do site = ENV.fetch("RSYNC_SITE_TO") { fail "Must set RSYNC_SITE_TO" } server = spawn( {"GENERATING_SITE" => "true"}, "bundle exec rails s thin -p 3001" ) sleep 10 # FIXME: start when the server is up # FIXME: improve the following super crude spider paths = %w[/] files = [ ] while path = paths.shift files << File.join("public", path.sub(%r{/\z}, "/index") + ".html") File.unlink(files.last) if File.exist? files.last files << files.last + ".gz" File.unlink(files.last) if File.exist? files.last page = open("http://localhost:3001#{path}") { |url| url.read } page.scan(/]+href="([^"]+)"/) do |link| paths << link.first end end system("rsync -a public #{site}") Process.kill("INT", server) Process.wait(server) system("bundle exec rake assets:clean") files.each do |file| File.unlink(file) end end end
生成到某个地方,去查看吧
rake static:generate RSYNC_SITE_TO=/Users/james/Desktop
后面几个都不感兴趣,没有测试,说好的42个,瞎扯了3个pass掉了,实在是吐血了
Over.