Active Record 关联
关联的类型
Rails 有六种关联:
belongs_to
has_one
has_many
has_many :through
has_one :through
has_and_belongs_to_many
belongs_to
声明一对一的关系,当前模型的实例属于另一个模型的实例,例如订单属于顾客可定义为:
class Order < ActiveRecord::Base
belongs_to :customer
end
一个订单只会属于一个顾客,因此belongs_to
声明中只能用单数形式。
使用了belongs_to
声明的 orders 数据表中会出现一个:customer_id
字段。
has_one
声明一对一的关系,通常与belongs_to
搭配使用。
class Order < ActiveRecord::Base
belongs_to :customer
end
class Customer < ActiveRecord::Base
has_one :order
end
一个顾客只拥有一个订单,customers 数据表中不会多出额外的字段。
一对一关系的两个模型在逻辑上存在着主从关系:"主"拥有(has_one
)"从","从"属于(belongs_to
)"主"。
has_many
声明一对多的关系,通常与belongs_to
搭配使用。
表示当前模型的实例拥有零个或者多个其他模型的实例。
class Order < ActiveRecord::Base
belongs_to :customer
end
class Customer < ActiveRecord::Base
has_many :orders
end
与has_one
不同,使用has_many
声明时要使用复数形式。
has_many :through
借助中间模型,建立两个模型之间多对多的关联。
例如
class Physician < ActiveRecord::Base
has_many :appointments
has_many :patients, through: :appointments
end
class Appointment < ActiveRecord::Base
belongs_to :physician
belongs_to :patient
end
class Patient < ActiveRecord::Base
has_many :appointments
has_many :physicians, through: :appointments
end
又例如文档-章节-段落之间的关联:
class Document < ActiveRecord::Base
has_many :sections
has_many :paragraphs, through: :sections
end
class Section < ActiveRecord::Base
belongs_to :document
has_many :paragraphs
end
class Paragraph < ActiveRecord::Base
belongs_to :section
end
通过document.paragraphs
可直接访问文档的所有段落。
has_and_belongs_to_many
直接建立两个模型之间的多对多关系,例如装配体-零件:
class Assembly < ActiveRecord::Base
has_and_belongs_to_many :parts
end
class Part < ActiveRecord::Base
has_and_belongs_to_many :assemblies
end
使用 belongs_to 还是 has_one
"主"拥有(has_one
)"从","从"属于(belongs_to
)"主"。
外键会放在使用belongs_to
声明的模型中。
使用 has_many :through 还是 has_and_belongs_to_many
不管哪种方式,数据库都会建立第三个中间数据表,只是has_many :through
为这个中间数据表建立了模型,而has_and_belongs_to_many
不会。
使用哪一种方式取决于这个中间数据表是否有必要作为独立实体存在,使用has_and_belongs_to_many
关联时需要在数据库中手动建立中间数据表。
多态关联
当前模型同时属于其它多个模型,可以为每个关联的模型声明一个belongs_to
,也可以使用多态关联。
class Picture < ActiveRecord::Base
belongs_to :imageable, ploymorphic: true
end
class Employee < ActiveRecord::Base
has_many :pictures, as: :imageable
end
class Product < ActiveRecord::Base
has_many :pictures, as: :imageable
end
自连接
模型和自己建立关联。例如在雇员信息表中,每个雇员可能属于上级,也可能拥有下级:
class Employee < ActiveRecord::Base
has_many :subordinates,
class_name: "Employee",
foreign_key: "manager_id"
belongs_to :manager,
class_name: "Employee"
end
如此,employee.subordinates
和emloyee.manager
可用。
小技巧和注意事项
缓存控制
通过关联取回的数据都会使用缓存,例如
customer.orders # retrieve orders from the databasecustomer.orders # retrieve orders from the cachedcustomer.orders.size # use the cached
调用关联方法时传入true
可重载缓存:
customer.orders.empty? # from the cachecustomer.orders(true).empty? # from the database
避免命名冲突
关联的名字要避开ActiveRecord::Base
自动添加的实例方法,例如attributes
, connection
等。
更新模式
模式即数据表,Rails中建立的关联并不会改变数据库的结构,因此与关联相关的数据表结构需要我们手动修改。不同关联对应的修改可能不同,例如外键关联需要在声明belongs_to
的模型对应的数据表中添加外键字段;声明has_and_belongs_to_many
的两个模型需要建立额外的中间数据表(连接数据表)。
has_and_belongs_to_many关联下,最好通过:join_table
选项指定连接数据表的名称。
控制关联的作用域
发生关联的模型必须位于同一个作用域(module)内,例如
module MyApplication module Business class Supplier < ActiveRecord::Base has_one :account end class Account < ActiveRecord::Base belongs_to :supplier end endend
Supplier模型和Account模型同属于一个作用域(MyApplication::Business
)内。
关联处于不同作用域的模型需要显式指定模型所属的作用域,例如
module MyApplication module Business class Supplier < ActiveRecord::Base has_one :account, class_name: "MyApplication::Billing::Account" end end module Billing class Account < ActiveRecord::Base belongs_to :supplier, class_name: "MyApplication::Business::Supplier" end endend
使用:class_name
选项指定关联的模型的完整名称。
双向关联
对某个数据对象的直接引用和作为关联对象的间接引用,逻辑上是一个东西,但是在内存中是两块区域,修改一个并不会立即同步到另一个,例如在“一个customer拥有多个orders,一个order只属于一个customer”的关联中
customer = Customer.firstorder = customer.orders.first
从数据库取回数据时,customer.name
与order.customer.name
是一致的。如果此时修改customer.name
或order.customer.name
其中一个的值,它们就不相等了。这是因为它们两个虽然逻辑上表示一个东西,但是在内存中却属于两个区域。
使用:inverse_of
同步关联两端的操作。
class Customer < ActiveRecord::Base has_many :orders, inverse_of: :customerendclass Order < ActiveRecord::Base belongs_to :customer, inverse_of: :ordersend
:inverse_of
的限制:
- 不能和
:through
一起使用:不能跨模型同步,这是个很复杂的事情 - 不能和
:polymorphic
一起使用:多态关联涉及的模型是复杂的、不确定的 - 不能和
:as
选项一起使用 - 在 belongs_to 关联中,会忽略 has_many 关联的 inverse_of 选项:没看懂介个
关联详解
belongs_to 关联详解
belongs_to 关联声明了当前模型与关联的模型是一对一的关系,即当前模型中包含外键。
belongs_to 关联添加的方法
class Order < ActiveRecord::Base belongs_to :customerend
order
会自动获得五个相关的方法:
order.customer(force_reload = false)
返回关联对象或者nil
order.customer=(another_customer)
order.build_customer(attributes = {})
构建关联对象,不存入数据库order.create_customer(attributes = {})
创建关联对象,进行验证,存入数据库order.create_customer!(attributes = {})
验证失败时抛出异常
belongs_to 方法的选项
:auto_save
保存父对象时自动保存所有子对象。
:class_name
无法从关联的名字获取关联模型时,使用:class_name
选项指定关联模型的位置。例如
- 关联模型与当前模型属于不同的作用域;
- 关联模型的类名与关联的名字不一致;
- ……
:counter_cache
缓存关联对象拥有当前模型实例的数目,在顾客-订单模型中
class Customer < ActiveRecord::Base has_many :ordersendclass Order < ActiveRecord::Base belongs_to :customer, counter_cache: trueend
然后就可以提升访问customer.orders.size
的效率。
但是,模型的定义不会影响模式的表达,因此我们还需要在 customers 数据表中添加 orders_count 字段。该名称为默认名称,如果使用其他字段名,只需要将该字段名显式指定给:counter_cache
选项即可,例如
class Order < ActiveRecord::Base belongs_to :customer, counter_cache: :count_of_ordersend
:dependent
当 belongs_to 和 has_one 搭配使用时,表示两个模型是一对一关系。设置 dependent 选项可以在删除一个对象时,也删除关联对象。注意不要在 belongs_to - has_many 关联中使用这个选项。
:foreign_key
外键字段名,默认是关联名加_id
后缀。
class Order < ActiveRecord::Base belongs_to :customer, class_name: "Patron", foreign_key: "patron_id"end
模型是模型,模式是模式。外键字段需要在迁移中手动添加。
:inverse_of
双向关联。
class Customer < ActiveRecord::Base has_many _orders, inverse_of: trueendclass Order < ActiveRecord::Base belongs_to :customer, inverse_of: trueend
:polymorphic
:touch
意为“摸一下”。当保存或者删除当前对象时,更新关联对象的updated_at
字段。
class Order < ActiveRecord::Base belongs_to :customer, touch: trueend
或者显示指定更新哪个时间戳字段:
class Order < ActiveRecord::Base belongs_to :customer, touch: orders_updated_atend
:validate
保存对象时,是否自动验证关联对象,默认为false
。
belongs_to 的作用域
略。
检查关联对象是否存在
检查订单的关联顾客是否存在:
order.customer.nil?
has_one 关联详解
has_one 关联添加的方法
class Supplier < ActiveRecord::Base has_one :accountend
实例 supplier 获得的方法有:
supplier.account(force_reload = false) -> Account | nil
supplier.account=another_account
supplier.build_account(attributes = {}) -> Account
supplier.create_account(attributes = {}) -> Account
supplier.create_account!(attributes = {}) -> Account
has_one 方法的选项
很多与 belongs_to 的选项相似。
:as
多态关联中使用。
:autosave
:class_name
:dependent
在供应商-账户模型中,设置供应商 has_one 的dependent
选项,以表明在处理供应商对象时如何处理账户对象:
:destroy
销毁该供应商拥有的账户对象和数据库记录:delete
直接删除供应商拥有的账户的数据库记录,因此不会触发关联对象的回调:nullify
将账户对应的数据库记录的外键改为 NULL,不会触发回调:restrict_with_exception
存在关联账户时抛出异常:restrict_with_error
存在关联账户时发生错误
如果关联模型外键有 NOT NULL 约束,则:nillify
失败,关联销毁失败。
:foreign_key
:inverse_of
:primary_key
:source
:source_type
:through
:validate
has_one 的作用域
检查关联的对象是否存在
supplier.account.nil?
什么时候保存对象
将账户对象赋值给供应商对象的关联时,会自动保存供应商对象,因为账户对象需要供应商作为外键,而稳定的外键需要将供应商保存到数据库才能生成。如果替换供应商的关联账户,所有与替换相关的对象都会保存,因为外键需要更新。
如果供应商对象没有被保存,与之关联的账户对象也不会被保存。
has_many 关联详解
当前对象拥有多个关联对象,外键保存在关联对象中。
has_many 关联添加的方法
class Customer < ActiveRecord::Base has_many :ordersend
customer 对象自动获得了 16 个相关的方法:
.orders(force_reload = false) -> Order[]
.orders << order1, ...
将添加的 orders 的外键设为 customer 的主键.orders.delete(order1, ...)
删除多个 orders,将它们的外键设为 NULL.orders.destroy(order1, ...)
删除多个 orders 对象和数据库记录.orders = another_orders
.order_ids
关联的 orders 的 ID 列表.order_ids = ids
.orders.clear
.orders.empty?
orders.size
orders.find(...)
orders.where(...)
惰性加载orders.exist?(...)
根据条件检查关联对象数组中是否存在符合条件的对象orders.build(attributes = {}, ...)
初始化关联对象,但不会自动保存orders.create(attributes = {}, ...)
初始化关联对象,会自动保存对象orders.create!(attributes = {}, ...)
has_many 方法的选项
:as
多态关联:autosave
:class_name
:dependent
:destroy
:delete_all
从数据库中直接删除所有关联对象的记录,不回调:nullify
设置外键为 NULL,不回调:restrict_with_exception
:restrict_with_error
:foreign_key
:inverse_of
:primary_key
关联对象的外键应该保存当前对象的哪一个字段,默认是id字段:source
搭配 through 使用:source_type
搭配 through 和多态关联使用:through
:validate
has_many 的作用域
高级用法,自定义 has_many 查询关联对象的方式,使用作用域代码块指定选项。
where
关联对象必须满足的条件
class Customer < ActiveRecord::Base # customer.confirmed_orders返回所有经过确认的订单 has_many :confirmed_orders, -> { where confirmed: true }, class_name: "Order"end
extending
group
结合 through 进行分组查询
class Customer < ActiveRecord::Base # customer.line_items返回与customer关联的所有orders关联的商品??? has_many line_items, -> { group 'orders.id' }, through: :ordersend
inlcudes
按需加载间接关联
class Customer < ActiveRecord::Base # customer.orders.line_items可访问了 has_many :orders, -> { inlcudes :line_items }end
limit
限制通过关联获取的对象的数量
class Customer < ActiveRecord::Base has_many :recent_orders, -> { order('order_date desc').limit(100) }, class_name: "Order"
offset
通过关联获取对象时跳过前面一部分
class Customer < ActiveRecord::Base has_many :orders, -> { offset 10 }end
order
关联对象的顺序
class Customer < ActiveRecord::Base has_many :orders, -> { order "date_confirmed DESC" }end
readonly
关联对象只读。
class Customer < ActiveRecord::Base has_many :orders, -> { readonly }end
select
选择关联对象的哪些字段,默认全部,注意选中关联对象的主键和外键。
distinct
搭配 through 使用,确保间接关联的对象没有重复值。
class Person < ActiveRecord::Base has_many :readings has_many :posts, -> { distinct }, through: :readings
什么时候保存对象
……
评论区