ActiveRecord Query Interface - 资料表操作
A person does not really understand something until after teaching it to a computer. – Donald Knuth
这一章将介绍更多ActiveRecord的CRUD方式。
读取资料
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
拿出数据库中的第一笔和最后一笔资料:
c1 = Category.first
c2 = Category.last
all 和 none
拿出数据库中全部的资料和无。
categories = Category.all
categories_null_object = Category.none
如果资料量较多,请不要在正式上线环境中执行.all 把所有资料拿出来,这样会耗费非常多的内存。请用分页或缩小查询范围。
none
看起来很没有用,主要是为了造出 Null object。
find
已知资料的主键 ID 的值的话,可以使用 find 方法:
c3 = Category.find(1)
c4 = Category.find(2)
find 也可以接受阵列参数,这样就会一次找寻多个并回传阵列:
arr = Category.find([1,2])
# 或是
arr = Category.find(1,2)
findby*
这个动态的方法可以非常简单的直接条件查询字段,例如:
category = Category.find_by_name("Business")
category = Category.find_by_name_and_position("Business", 123)
如果找不到资料的话,会丢 ActiveRecord::RecordNotFound 例外。如果是 find_by_id 就不会丢出例外,而是回传 nil。
reload
这个方法可以将物件从数据库里重新加载一次:
> e = Event.first
> e.name = "test"
> e.reload
pluck
这个方法可以非常快速的捞出指定字段的资料:
Event.pluck(:name)
=> ["foo", "bar"]
Category.pluck(:id, :name)
=> [ [1, "Tech"], [2, "Business"] ]
find_by_sql
如果需要手动撰写 SQL,可以使用find_by_sql
和count_by_sql
,例如:
c8 = Category.find_by_sql("select * from categories")
不过需要用到的机会应该很少。
where 查询条件
where 可以非常弹性的组合出 SQL 查询,例如:
c9 = Category.where( :name => 'Ruby', :position => 1 )
c10 = Category.where( "name = ? or position = ?", 'Ruby', 2 )
其中参数有两种写法,一种是 Hash,另一种是用?
替换组合出SQL
。前者的写法虽然比较简洁,但是就没办法写出 or 的查询。注意到不要使用字串写法,例如
Category.where("name = #{params[:name]}") # 请不要这样写
这是因为字串写法会有SQL injection的安全性问题,所以请改用Hash或?
的形式来带入变量。
where.not
where.not
可以组合出不等于的查询,例如:
Category.where.not( :name => 'Ruby' )
会查询所有name
不是Ruby
的资料,这跟Category.where("name != ?", 'Ruby')
是一样的作用。
limit
limit 可以限制笔数
c = Category.limit(5).all
c.size # 5
order
order 可以设定排序条件
Category.order("position")
Category.order("position DESC")
Category.order("position DESC, name ASC")
如果要消去order条件,可以用reorder
:
Category.order("position").reorder("name") # 改用 name 排序
Category.order("position").reorder(nil) # 取消所有排序
offset
offset 可以设定忽略前几笔不取出,通常用于资料分页:
c = Category.limit(2)
c.first.id # 1
c = Category.limit(2).offset(3)
c.first.id # 4
select
默认的 SQL 查询会取出资料的所有字段,有时候你可能不需要所有资料,为了效能我们可以只取出其中特定字段:
Category.select("id, name")
例如字段中有 Binary 资料时,你不会希望每次都读取出庞大的 Binary 资料佔用内存,而只希望在使用者要下载的时候才读取出来。
readonly
c = Category.readonly.first
如此查询出来的c
就无法修改或删除,不然会丢出ActiveRecord::ReadOnlyRecord
例外。
group 和 having
group
运用了数据库的groupby
功能,让我们可以将_SQL计算后(例如count)的结果依照某一个字段分组后回传,例如说今天我有一批订单,里面有分店的销售金额,我希望能这些金额全部加总起来变成的各分店销售总金额,这时候我就可以这么做:
Order.select("store_name, sum(sales)").group("store")
这样会执行类似这样的SQL:
SELECT store_name, sum(sales) FROM orders GROUP BY store_name
having
则是让group
可以再增加条件,例如我们想为上面的查询增加条件是找出业绩销售超过10000的分店,那么我可以这么下:
Order.select("store_name, sum(sales)").group("store").having("sum(sales) > ?", 10000)
所执行的SQL便会是:
SELECT store_name, sum(sales) FROM orders GROUP BY store_name HAVING sum(sales) > 10000
串接写法
以上的 where, order , limit, offset, joins, select 等等,都可以自由串接起来组合出最终的 SQL 条件:
c12 = Category.where( :name => 'Ruby' ).order("id desc").limit(3)
find_each 批次处理
如果资料量很大,但是又需要全部拿出来处理,可以使用 find_each 批次处理
Category.where("position > 1").find_each do |category|
category.do_some_thing
end
默认会批次捞 1000 笔,如果需要设定可以加上 :batch_size 参数。
新增资料
ActiveRecord提供了四种API,分别是save、save!、create和create!:
> a = Category.new( :name => 'Ruby', :position => 1 )
> a.save
> b = Category.new( :name => 'Perl', :position => 2 )
> b.save!
> Category.create( :name => 'Python', :position => 3 )
> c = Category.create!( :name => 'PHP', :position => 4 )
其中create和create!就等于new
完就save和save!,有无惊叹号的差别在于validate资料验证不正确的动作,无惊叹号版本会回传布林值(true或false),有惊叹号版本则是验证错误会丢出例外。
何时使用惊叹号版本呢?save和create通常用在会处理回传布林值(true/false)的情况下(例如在 controller 里面根据成功失败决定 render 或 redirect),否则在预期应该会储存成功的情况下,请用 save!或create! 来处理,这样一旦碰到储存失败的情形,才好追踪 bug。
透过:validate => false
参数可以略过验证
> c.save( :validate => false )
new_record?
这个方法可以知道物件是否已经存在于数据库:
> c.new_record?
=> false
> c.persisted?
=> true
first_or_initialize 和 first_or_create
这个方法可以很方便的先查询有没有符合条件的资料,没有的话就初始化,例如:
c = Category.where( :name => "Ruby" ).first || Category.new( :name => "Ruby" )
可以改写成
c = Category.where( :name => "Ruby" ).first_or_initialize
如果要直接存进数据库,可以改用first_or_create
:
c = Category.where( :name => "Ruby" ).first_or_create
或是Validate失败丢例外的版本:
c = Category.where( :name => "Ruby" ).first_or_create!
更新资料
更新一个ActiveRecord物件:
c13 = Category.first
c13.update(attributes)
c13.update!(attributes)
c13.update_column(attribute_name, value)
c13.update_columns(attributes)
注意 update_column 会略过 validation 资料验证注意 mass assign 安全性问题,详见安全性一章。
我们也可以用update_all
来一次更新数据库的多笔资料:
> Category.where( :name => "Old Name" ).update_all( :name => "New Name" )
increment 和 decrement
数字字段可以使用increment
和decrement
方法,也有increment!
和decrement!
立即存进数据库的用法。
post = Post.first
post.increment!(:comments_count)
post.decrement!(:comments_count)
有
!
的版本会直接save
另外也有 class 方法可以使用,这样就不需要先捞物件:
Post.increment_count(:comments_count, post_id)
toggle
Boolean字段可以使用toggle
方法,同样也有toggle!
删除资料
一种是先抓到该物件,然后删除:
c12 = Category.first
c12.destroy
另一种是直接对类别呼叫删除,例如:
Category.delete(2) #
Category.delete([2,3,4])
Category.where( ["position > ?", 3] ).delete_all
Category.where( ["position > ?", 3] ).destroy_all
delete 不会有 callback 回呼,destroy 有 callback 回呼。什么是回呼请详见下一章。
统计方法
Category.count
Category.average(:position)
Category.maximum(:position)
Category.minimum(:position)
Category.sum(:position)
其中我们可以利用上述的 where 条件缩小范围,例如:
Category.where( :name => "Ruby").count
Scopes 作用域
Model Scopes是一项非常酷的功能,它可以将常用的查询条件宣告起来,让程式变得干净易读,更厉害的是可以串接使用。例如,我们编辑app/models/event.rb,加上两个Scopes:
class Event < ApplicationRecord
scope :open_public, -> { where( :is_public => true ) }
scope :recent_three_days, -> { where(["created_at > ? ", Time.now - 3.days ]) }
end
> Event.create( :name => "public event", :is_public => true )
> Event.create( :name => "private event", :is_public => false )
> Event.create( :name => "private event", :is_public => true )
> Event.open_public
> Event.open_public.recent_three_days
-> {…}
是Ruby语法,等同于Proc.new{…}
或lambda{…}
,用来建立一个匿名方法物件
串接的顺序没有影响的,都会一并套用。我们也可以串接在has_many关联后:
> user.events.open_public.recent_three_days
接着,我们可以设定一个默认的Scope,通常会拿来设定排序:
class Event < ApplicationRecord
default_scope -> { order('id DESC') }
end
unscoped
方法可以暂时取消默认的default_scope:
Event.unscoped do
Event.all
# SELECT * FROM events
end
最后,Scope也可以接受参数,例如:
class Event < ApplicationRecord
scope :recent, ->(date) { where("created_at > ?", date) }
# 等同于 scope :recent, lambda{ |date| where(["created_at > ? ", date ]) }
# 或 scope :recent, Proc.new{ |t| where(["created_at > ? ", t ]) }
end
Event.recent( Time.now - 7.days )
不过,笔者会推荐上述这种带有参数的Scope,改成如下的类别方法,可以比较明确看清楚参数是什么,特别是你想给默认值的时候:
class Event < ApplicationRecord
def self.recent(t=Time.now)
where(["created_at > ? ", t ])
end
end
Event.recent( Time.now - 7.days )
这样的效果是一样的,也是一样可以和其他Scope做串接。
all
方法可以将Model转成可以串接的形式,方便依照参数组合出不同查询,例如
fruits = Fruit.all
fruits = fruits.where(:colour => 'red') if options[:red_only]
fruits = fruits.limit(10) if limited?
可以呼叫
to_sql
方法观察实际ORM转出来的SQL,例如Event.open_public.recent_three_days.to_sql
虚拟属性(Virtual Attribute)
有时候表单里操作的属性资料,不一定和数据库的字段完全对应。例如资料表分成first_name和last_name两个字段好了,但是表单输入和显示的时候,只需要一个属性叫做full_name,这时候你就可以在model里面定义这样的方法:
def full_name
"#{self.first_name} #{self.last_name}"
end
def full_name=(value)
self.first_name, self.last_name = value.to_s.split(" ", 2)
end
自订资料表名称或主键字段
我们在环境设定与Bundler一章曾提及Rails的命名惯例,资料表的名称默认就是Model类别名称的复数小写,例如Event的资料表是events、EventCategory的资料表是event_categories。不过英文博大精深,Rails转出来的复数不一定是正确的英文单字,这时候你可以修改config/initializers/inflections.rb
进行订正。
如果你的资料表不使用这个命名惯例,例如连接到旧的数据库,或是主键字段不是id
,也可以手动指定:
class Category < ApplicationRecord
self.table_name = "your_table_name"
self.primary_key = "your_primary_key_name"
end