ActiveRecord Query Interface - 资料表操作

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

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

    Arel 是 relational algebra” library。但根据 2.0 实作者 tenderlove 的说法,也可以说是一种 SQL compiler。

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

    all 和 none

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

    1. categories_null_object = Category.none

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

    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 可以非常弹性的组合出 SQL 查询,例如:

    1. c9 = Category.where( :name => 'Ruby', :position => 1 )

    其中参数有两种写法,一种是 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

    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 查询会取出资料的所有字段,有时候你可能不需要所有资料,为了效能我们可以只取出其中特定字段:

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

    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则是让可以再增加条件,例如我们想为上面的查询增加条件是找出业绩销售超过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!

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

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

    数字字段可以使用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] ).destroy_all

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

    统计方法

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

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

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

    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?

    虚拟属性(Virtual Attribute)

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

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

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

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