ActiveRecord Query Interface - 资料表操作

A person does not really understand something until after teaching it to a computer. – Donald Knuth

这一章将介绍更多ActiveRecordCRUD方式。

读取资料

ActiveRecord 使用了 Arel 技术来实作查询功能,你可以自由组合 where、limit、select、order 等条件。

Arel 是 relational algebra” library。但根据 2.0 实作者 tenderlove 的说法,也可以说是一种 SQL compiler。 http://engineering.attinteractive.com/2010/12/architecture-of-arel-2-0/

first 和 last

拿出数据库中的第一笔和最后一笔资料:

  1. c1 = Category.first
  2. c2 = Category.last

all 和 none

拿出数据库中全部的资料和无。

  1. categories = Category.all
  2. categories_null_object = Category.none

如果资料量较多,请不要在正式上线环境中执行.all 把所有资料拿出来,这样会耗费非常多的内存。请用分页或缩小查询范围。

none 看起来很没有用,主要是为了造出 Null object

find

已知资料的主键 ID 的值的话,可以使用 find 方法:

  1. c3 = Category.find(1)
  2. c4 = Category.find(2)

find 也可以接受阵列参数,这样就会一次找寻多个并回传阵列:

  1. arr = Category.find([1,2])
  2. # 或是
  3. arr = Category.find(1,2)

findby*

这个动态的方法可以非常简单的直接条件查询字段,例如:

  1. category = Category.find_by_name("Business")
  2. category = Category.find_by_name_and_position("Business", 123)

如果找不到资料的话,会丢 ActiveRecord::RecordNotFound 例外。如果是 find_by_id 就不会丢出例外,而是回传 nil。

reload

这个方法可以将物件从数据库里重新加载一次:

  1. > e = Event.first
  2. > e.name = "test"
  3. > e.reload

pluck

这个方法可以非常快速的捞出指定字段的资料:

  1. Event.pluck(:name)
  2. => ["foo", "bar"]
  3. Category.pluck(:id, :name)
  4. => [ [1, "Tech"], [2, "Business"] ]

find_by_sql

如果需要手动撰写 SQL,可以使用find_by_sqlcount_by_sql,例如:

  1. c8 = Category.find_by_sql("select * from categories")

不过需要用到的机会应该很少。

where 查询条件

where 可以非常弹性的组合出 SQL 查询,例如:

  1. c9 = Category.where( :name => 'Ruby', :position => 1 )
  2. c10 = Category.where( "name = ? or position = ?", 'Ruby', 2 )

其中参数有两种写法,一种是 Hash,另一种是用?替换组合出SQL。前者的写法虽然比较简洁,但是就没办法写出 or 的查询。注意到不要使用字串写法,例如

  1. Category.where("name = #{params[:name]}") # 请不要这样写

这是因为字串写法会有SQL injection的安全性问题,所以请改用Hash?的形式来带入变量。

where.not

where.not可以组合出不等于的查询,例如:

  1. Category.where.not( :name => 'Ruby' )

会查询所有name不是Ruby的资料,这跟Category.where("name != ?", 'Ruby')是一样的作用。

limit

limit 可以限制笔数

  1. c = Category.limit(5).all
  2. c.size # 5

order

order 可以设定排序条件

  1. Category.order("position")
  2. Category.order("position DESC")
  3. Category.order("position DESC, name ASC")

如果要消去order条件,可以用reorder

  1. Category.order("position").reorder("name") # 改用 name 排序
  2. Category.order("position").reorder(nil) # 取消所有排序

offset

offset 可以设定忽略前几笔不取出,通常用于资料分页:

  1. c = Category.limit(2)
  2. c.first.id # 1
  3. c = Category.limit(2).offset(3)
  4. c.first.id # 4

select

默认的 SQL 查询会取出资料的所有字段,有时候你可能不需要所有资料,为了效能我们可以只取出其中特定字段:

  1. Category.select("id, name")

例如字段中有 Binary 资料时,你不会希望每次都读取出庞大的 Binary 资料佔用内存,而只希望在使用者要下载的时候才读取出来。

readonly

  1. c = Category.readonly.first

如此查询出来的c就无法修改或删除,不然会丢出ActiveRecord::ReadOnlyRecord例外。

group 和 having

group运用了数据库的groupby功能,让我们可以将_SQL计算后(例如count)的结果依照某一个字段分组后回传,例如说今天我有一批订单,里面有分店的销售金额,我希望能这些金额全部加总起来变成的各分店销售总金额,这时候我就可以这么做:

  1. Order.select("store_name, sum(sales)").group("store")

这样会执行类似这样的SQL:

  1. SELECT store_name, sum(sales) FROM orders GROUP BY store_name

having则是让group可以再增加条件,例如我们想为上面的查询增加条件是找出业绩销售超过10000的分店,那么我可以这么下:

  1. Order.select("store_name, sum(sales)").group("store").having("sum(sales) > ?", 10000)

所执行的SQL便会是:

  1. SELECT store_name, sum(sales) FROM orders GROUP BY store_name HAVING sum(sales) > 10000

串接写法

以上的 where, order , limit, offset, joins, select 等等,都可以自由串接起来组合出最终的 SQL 条件:

  1. c12 = Category.where( :name => 'Ruby' ).order("id desc").limit(3)

find_each 批次处理

如果资料量很大,但是又需要全部拿出来处理,可以使用 find_each 批次处理

  1. Category.where("position > 1").find_each do |category|
  2. category.do_some_thing
  3. end

默认会批次捞 1000 笔,如果需要设定可以加上 :batch_size 参数。

新增资料

ActiveRecord提供了四种API,分别是save、save!、create和create!:

  1. > a = Category.new( :name => 'Ruby', :position => 1 )
  2. > a.save
  3. > b = Category.new( :name => 'Perl', :position => 2 )
  4. > b.save!
  5. > Category.create( :name => 'Python', :position => 3 )
  6. > c = Category.create!( :name => 'PHP', :position => 4 )

其中createcreate!就等于new完就savesave!,有无惊叹号的差别在于validate资料验证不正确的动作,无惊叹号版本会回传布林值(true或false),有惊叹号版本则是验证错误会丢出例外。

何时使用惊叹号版本呢?save和create通常用在会处理回传布林值(true/false)的情况下(例如在 controller 里面根据成功失败决定 render 或 redirect),否则在预期应该会储存成功的情况下,请用 save!或create! 来处理,这样一旦碰到储存失败的情形,才好追踪 bug。

透过:validate => false参数可以略过验证

  1. > c.save( :validate => false )

new_record?

这个方法可以知道物件是否已经存在于数据库:

  1. > c.new_record?
  2. => false
  3. > c.persisted?
  4. => true

first_or_initialize 和 first_or_create

这个方法可以很方便的先查询有没有符合条件的资料,没有的话就初始化,例如:

  1. c = Category.where( :name => "Ruby" ).first || Category.new( :name => "Ruby" )

可以改写成

  1. c = Category.where( :name => "Ruby" ).first_or_initialize

如果要直接存进数据库,可以改用first_or_create

  1. c = Category.where( :name => "Ruby" ).first_or_create

或是Validate失败丢例外的版本:

  1. c = Category.where( :name => "Ruby" ).first_or_create!

更新资料

更新一个ActiveRecord物件:

  1. c13 = Category.first
  2. c13.update(attributes)
  3. c13.update!(attributes)
  4. c13.update_column(attribute_name, value)
  5. c13.update_columns(attributes)

注意 update_column 会略过 validation 资料验证注意 mass assign 安全性问题,详见安全性一章。

我们也可以用update_all来一次更新数据库的多笔资料:

  1. > Category.where( :name => "Old Name" ).update_all( :name => "New Name" )

increment 和 decrement

数字字段可以使用incrementdecrement方法,也有increment!decrement!立即存进数据库的用法。

  1. post = Post.first
  2. post.increment!(:comments_count)
  3. post.decrement!(:comments_count)

! 的版本会直接 save

另外也有 class 方法可以使用,这样就不需要先捞物件:

  1. Post.increment_count(:comments_count, post_id)

toggle

Boolean字段可以使用toggle方法,同样也有toggle!

删除资料

一种是先抓到该物件,然后删除:

  1. c12 = Category.first
  2. c12.destroy

另一种是直接对类别呼叫删除,例如:

  1. Category.delete(2) #
  2. Category.delete([2,3,4])
  3. Category.where( ["position > ?", 3] ).delete_all
  4. Category.where( ["position > ?", 3] ).destroy_all

delete 不会有 callback 回呼,destroy 有 callback 回呼。什么是回呼请详见下一章。

统计方法

  1. Category.count
  2. Category.average(:position)
  3. Category.maximum(:position)
  4. Category.minimum(:position)
  5. Category.sum(:position)

其中我们可以利用上述的 where 条件缩小范围,例如:

  1. Category.where( :name => "Ruby").count

Scopes 作用域

Model Scopes是一项非常酷的功能,它可以将常用的查询条件宣告起来,让程式变得干净易读,更厉害的是可以串接使用。例如,我们编辑app/models/event.rb,加上两个Scopes

  1. class Event < ApplicationRecord
  2. scope :open_public, -> { where( :is_public => true ) }
  3. scope :recent_three_days, -> { where(["created_at > ? ", Time.now - 3.days ]) }
  4. end
  5. > Event.create( :name => "public event", :is_public => true )
  6. > Event.create( :name => "private event", :is_public => false )
  7. > Event.create( :name => "private event", :is_public => true )
  8. > Event.open_public
  9. > Event.open_public.recent_three_days

-> {…}是Ruby语法,等同于Proc.new{…}lambda{…},用来建立一个匿名方法物件

串接的顺序没有影响的,都会一并套用。我们也可以串接在has_many关联后:

  1. > user.events.open_public.recent_three_days

接着,我们可以设定一个默认的Scope,通常会拿来设定排序:

  1. class Event < ApplicationRecord
  2. default_scope -> { order('id DESC') }
  3. end

unscoped方法可以暂时取消默认的default_scope

  1. Event.unscoped do
  2. Event.all
  3. # SELECT * FROM events
  4. end

最后,Scope也可以接受参数,例如:

  1. class Event < ApplicationRecord
  2. scope :recent, ->(date) { where("created_at > ?", date) }
  3. # 等同于 scope :recent, lambda{ |date| where(["created_at > ? ", date ]) }
  4. # 或 scope :recent, Proc.new{ |t| where(["created_at > ? ", t ]) }
  5. end
  6. Event.recent( Time.now - 7.days )

不过,笔者会推荐上述这种带有参数的Scope,改成如下的类别方法,可以比较明确看清楚参数是什么,特别是你想给默认值的时候:

  1. class Event < ApplicationRecord
  2. def self.recent(t=Time.now)
  3. where(["created_at > ? ", t ])
  4. end
  5. end
  6. Event.recent( Time.now - 7.days )

这样的效果是一样的,也是一样可以和其他Scope做串接。

all方法可以将Model转成可以串接的形式,方便依照参数组合出不同查询,例如

  1. fruits = Fruit.all
  2. fruits = fruits.where(:colour => 'red') if options[:red_only]
  3. fruits = fruits.limit(10) if limited?

可以呼叫to_sql方法观察实际ORM转出来的SQL,例如Event.open_public.recent_three_days.to_sql

虚拟属性(Virtual Attribute)

有时候表单里操作的属性资料,不一定和数据库的字段完全对应。例如资料表分成first_namelast_name两个字段好了,但是表单输入和显示的时候,只需要一个属性叫做full_name,这时候你就可以在model里面定义这样的方法:

  1. def full_name
  2. "#{self.first_name} #{self.last_name}"
  3. end
  4. def full_name=(value)
  5. self.first_name, self.last_name = value.to_s.split(" ", 2)
  6. end

自订资料表名称或主键字段

我们在环境设定与Bundler一章曾提及Rails的命名惯例,资料表的名称默认就是Model类别名称的复数小写,例如Event的资料表是eventsEventCategory的资料表是event_categories。不过英文博大精深,Rails转出来的复数不一定是正确的英文单字,这时候你可以修改config/initializers/inflections.rb进行订正。

如果你的资料表不使用这个命名惯例,例如连接到旧的数据库,或是主键字段不是id,也可以手动指定:

  1. class Category < ApplicationRecord
  2. self.table_name = "your_table_name"
  3. self.primary_key = "your_primary_key_name"
  4. end

更多线上资源