Ruby on Rails 实战圣经

使用 Rails 5.0+ 及 Ruby 2.3+

欢迎留下 E-mail,若有更新消息可以通知您。若您有任何意见、鼓励或勘误,也欢迎来信给我。愿意赞助支持的话,这是我的支付宝微信

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

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

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)

find_by_*

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

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_sqlcount_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运用了数据库的group_by功能,让我们可以将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 )

其中createcreate!就等于new完就savesave!,有无惊叹号的差别在于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

数字字段可以使用incrementdecrement方法,也有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_namelast_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的资料表是eventsEventCategory的资料表是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

更多线上资源

》回到页首