这一章讲的是model间的关系,这个和数据库表间关系有点相似,但是更加的像一个真实世界的关系,比如谁has_many谁,谁belong谁,这样的话,我们进行一些操作时是很简单的。就像下面的一样
class Customer < ActiveRecord::Base
has_many :orders, dependent: :destroy
end
class Order < ActiveRecord::Base
belongs_to :customer
end
Customer有很多order,每个order仅能属于一个Customer,当一个Customer 进行destroy时,附属的Order也将被删除。
对于上面的例子,我们新建order时,要用它属于的Customer创建。
@order = @customer.orders.create(order_date: Time.now)
下面我们将简单说下rails支持的association有哪些
belongs_to
has_one
has_many
has_many :through
has_one :through
has_and_belongs_to_many
下面我们一个个的说说什么意思
belongs_to
belongs_to建立了一个对另一个model的one-to-one连接,简单来说就是 Abelongs_to B 那么A就属于B,一个A只能对应一个B,而一个B可以拥有一个或多个A。看下面的例子
class Order < ActiveRecord::Base
belongs_to :customer
end
注意,belongs_to后面跟的并不是:customers 而是:customer,也就是会所belongs_to跟着单数形式。其实很好理解,每个order(一个model就是一条记录)只能属于一个customer的,所以是单数啦。
相应的migration应该是这样子的
class CreateOrders <ActiveRecord::Migration
defchange
create_table :customers do |t|
t.string :name
t.timestamps
end
create_table :orders do |t|
t.belongs_to :customer
t.datetime :order_date
t.timestamps
end
end
end
has_one
has_one 同样是建立了一个one-to-one的联系,但有一点不同的是,这里不说的那么专业,简单而言 A has_one B 那么 A 将有一个 B。看下面的一个例子
class Supplier < ActiveRecord::Base
has_one :account
end
它和belongs_to最大的区别是那个外键的位置,可以仔细比较下两张图,即可发现。
它的migration是类似于这样的
class CreateSuppliers < ActiveRecord::Migration
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.belongs_to :supplier
t.string :account_number
t.timestamps
end
end
end
has_many
has_many建立了一个one-to-many的连接,一般我们在 A中写了 Abelongs_to B,在B中我们就写 B has_many A(或者B has_one A),表明了B 有零个或者多个 A,例子
class Customer < ActiveRecord::Base
has_many :orders
end
注意,has_many是跟着复数形式的
它的migration类似于这样(对比这belongs_to看)
class CreateCustomers < ActiveRecord::Migration
def change
create_table :customers do |t|
t.string :name
t.timestamps
end
create_table :orders do |t|
t.belongs_to :customer
t.datetime :order_date
t.timestamps
end
end
end
has_many :through
这个联系是指 A 通过 C 与B 建立了many-to-many的联系。比如病人挂号看病,每个病人可以挂一个号,每个号可以看一个医生,但是病人通过挂号就可以看很多个医生,同时医生也可以挂号为多个病人服务,这里 A 就是病人(Patient)B就是预约号(Appointment)C就是医生(Physician)上述用代码实现的话就像下面
注意,其实through是一个参数,其实调用的还是has_many这个函数。
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
这个migration类似这样
class CreateAppointments < ActiveRecord::Migration
def change
create_table :physicians do |t|
t.string :name
t.timestamps
end
create_table :patients do |t|
t.string :name
t.timestamps
end
create_table :appointments do |t|
t.belongs_to :physician
t.belongs_to :patient
t.datetime :appointment_date
t.timestamps
end
end
end
这样 physician.patients= patients rails就可以识别了。
其实,这个函数更加强大,你只要记住一点,A has_many B , through: C , 那么A 就有很多C,同时有很多B,A中不必写has_manyC ,C也不必写belongs_to A。类似的,我们可以把A认为一篇文章,B为段落,C为句子。假如我们希望 @document.paragraphs 那么下面的写法就比较好,简单来说就是A有很多B, B有很多C。C仅属于一个B,B仅属于一个A.
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
has_one :through
和has_many :through类似,只不过是建立的one-to-one关系。直接说例子,比如说每个供应商有一个账号,每个账号有一个账号历史,那么就应该类似的实现
class Supplier < ActiveRecord::Base
has_one :account
has_one :account_history, through: :account
end
class Account < ActiveRecord::Base
belongs_to :supplier
has_one :account_history
end
class AccountHistory < ActiveRecord::Base
belongs_to :account
end
migration如下
create_table :accounts do |t|
t.belongs_to :supplier
t.string :account_number
t.timestamps
end
create_table :account_histories do |t|
t.belongs_to :account
t.integer :credit_rating
t.timestamps
end
end
end
has_and_belongs_to_many
这个是建立直接的many-to-many联系,假如A has_and_blongs_to_many B,那么他们将生成一个新表叫As_Bs,直接看例子,组件和零件的关系,每个零件属于多个组件,一个组件包含多个零件。所以
class Assembly < ActiveRecord::Base
has_and_belongs_to_many :parts
end
class Part < ActiveRecord::Base
has_and_belongs_to_many :assemblies
end
migration如下
class CreateAssembliesAndParts < ActiveRecord::Migration
def change
create_table :assemblies do |t|
t.string :name
t.timestamps
end
create_table :parts do |t|
t.string :part_number
t.timestamps
end
create_table :assemblies_parts do |t|
t.belongs_to :assembly
t.belongs_to :part
end
end
end
多态连接关系(polymorphic association)
假如我们的model A 可能与B或者C有关系,那么我们就要采用多态连接了。主要想法是开启多态,然后把连接指向一个B和C公用的name上,然后B和C再重命名为该name。
就比如下面的例子,employee 和 product 都有image。实现如下
class Picture < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
end
class Employee < ActiveRecord::Base
has_many :pictures, as: :imageable
end
class Product < ActiveRecord::Base
has_many :pictures, as: :imageable
end
对于Picture 它belongs_to一个叫imageable的model,而这个model是多态的。对于Employee和Product他们重命名为imageable然后has_many Picture,所以,@employee.pictures.或者@product.pictures.可以使用。假如我们想通过picture访问它的拥有者,可以通过 @picture.imageable访问,这个需要求改migration来使他工作,其实就是加一个外键,加上id和type(id就是id,type是说是employee还是product),修改后的migration如下
class CreatePictures <ActiveRecord::Migration
defchange
create_table :pictures do |t|
t.string :name
t.references :imageable, polymorphic: true #声明了外键,并且说明是多态的
t.timestamps
end
end
end
主要用法就如上面所说的了,下面我们讲一些使用的小技巧
我们所执行的操作都是基于缓存的,假如我们需要重新载入缓存该怎么办呢?这个是有可能用到的,因为假如我们的数据库数据更改了但是缓存中数据并没有更改,那么基于该缓存的一切操作将不再正确了。其实重载也很简单,只要传递一个true参数就行了
假如原来的
customer.orders # retrieves orders from the database
customer.orders.size # uses the cached copy of orders
customer.orders.empty? # uses the cached copy of orders
假如传递了truecustomer.orders # retrieves orders from the database
customer.orders.size # uses the cached copy of orders
customer.orders(true).empty? # discards the cached copy of orders and goes back to the database
最后,我们仔细说说每个部分association将产生的函数和可接受参数
一、belongs_to
看例子
classOrder < ActiveRecord::Base
belongs_to:customer
end
那么,他将产生以下函数(customer是根据model名字不同变化的,其余部分不变)
customer
customer=
build_customer
create_customer
为了更加通用,我们将这样讲,A belongs_to : association,一般形式是这样的,然后依托上面的例子将
所以它产生了下面四个通用函数。
association(force_reload = false)
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
1. association(force_reload = false)
访问A belongs_to 的association,其中的参数决定是否重新从数据库中加载缓存。例如下面的
@customer = @order.customer
2. association=(associate)
利用已经存在的association 修改A belongs_to的association,例如
@order.customer = @customer
3. build_association(attributes = {})
新建一个A belongs_to 的association,但是相当于执行了new,但是没有save,参数就是new association需要的参数。例如
@customer = @order.build_customer(customer_number: 123, customer_name: "John Doe")
4. create_association(attributes = {})
与build_association类似,不过他相当于执行了create,也就是save了。@customer = @order.create_customer(customer_number: 123, customer_name: "John Doe")
belongs_to的参数
belongs_to 的参数有下面的几个
1. :autosave #设置为true,那么rails将自动保存加载的model,并且自动消除
2. :class_name #用于指定model,假如两个不属于同一个model,那么就在class_name以字符串形式写出
3. :counter_cache #避免每次查询还要遍历数据库计数。你可以设置为true
class Order < ActiveRecord::Base
belongs_to :customer, counter_cache: true
end
class Customer < ActiveRecord::Base
has_many :orders
end
你还可以将这个信息存到数据库中,但是这个会给你增加一列,他后面跟着列的名字,如下
class Order < ActiveRecord::Base
belongs_to :customer, counter_cache: :count_of_orders
end
class Customer < ActiveRecord::Base
has_many :orders
end
4. :dependent
dependent说的是当A has_many B时,假如我们删除掉A,与A对应的B应该如何处置。主要有一下三种
:destroy 假如删除 A ,那么将通过调用B的destroy函数来删除B
:delete 假如删除A, 那么将不调用B的destroy函数,而是直接从数据库删除
:restrict 假如删除A,只要A有关联的B,那么就将抛出一个ActiveRecord::DeleteRestrictionError错误
5. :foreign_key 指向一个string,你可以使用它自己直接指定外键列的名称
6. :inverse_of 显示的指出与该联系相反的model名,他与:polymorphic 不共存
class Customer < ActiveRecord::Base
has_many :orders, inverse_of: :customer
end
class Order < ActiveRecord::Base
belongs_to :customer, inverse_of: :orders
end
7. :polymorphic 参照上面说的多态连接
8. :touch 首先,你可以设置该参数为true,这样对于下面的例子,每次对orders进行save或者destroy什么的操作,同样也会更新customer的时间戳,也就是更新updated_at 或者updated_on 列。
class Order < ActiveRecord::Base
belongs_to :customer, touch: true
end
class Customer < ActiveRecord::Base
has_many :orders
end
同样,你可以指定列进行更新,如下
class Order < ActiveRecord::Base
belongs_to :customer, touch: :orders_updated_at #orders是表名字,而updated_at是列名。
end
9. :validate #设置为true,那么每次操作就要进行validation验证,默认为false
二、has_one
他与belongs_to有很多相似之处,所以相似的东西我们就不在赘述,仅列出。
四个通用函数
association(force_reload = false)
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
参数
:as
:autosave
:class_name
:dependent
:foreign_key
:inverse_of
:primary_key 自定义主键
:source
:source_type
:through
:validate
只是新加了一个source 和 source_type
source的理解可以参考下面的,讲的很清楚(source_type应该类似,不过暂时没找到资料):
壹、
Sometimes, you want to use different namesfor different associations. If the name you want to use for an association onthe model isn't the same as the assocation on the :through model, youcan use:source to specify it.
I don't think the above paragraph is much clearerthan the one in the docs, so here's an example. Let's assume we have threemodels, Pet, Dog and Dog::Breed.
class Pet < ActiveRecord::Base
has_many :dogs
end
class Dog < ActiveRecord::Base
belongs_to :pet
has_many :breeds
end
class Dog::Breed < ActiveRecord::Base
belongs_to :dog
end
In this case, we've chosen to namespacethe Dog::Breed, because we want to accessDog.find(123).breeds as anice and convenient association.
Now, if we now want to createa has_many :dog_breeds, :through => :dogs association on Pet,we suddenly have a problem. Rails won't be able to finda :dog_breeds association on Dog, so Rails can't possiblyknow which Dog association you want to use. Enter :source:
class Pet < ActiveRecord::Base
has_many :dogs
has_many :dog_breeds, :through => :dogs, :source => :breeds
end
With :source, we're telling Rails tolook for an association called :breeds on the Dog model (asthat's the model used for :dogs), and use that.
贰、
Let me expand on that example:
class User
has_many :subscriptions
has_many :newsletters, :through => :subscriptions
end
class Newsletter
has_many :subscriptions
has_many :users, :through => :subscriptions
end
class Subscription
belongs_to :newsletter
belongs_to :user
end
With this code, you can do somethinglike Newsletter.find(id).users to get a list of the newsletter'ssubscribers. But if you want to be clearer and be able totypeNewsletter.find(id).subscribers instead, you must change theNewsletter class to this:
class Newsletter
has_many :subscriptions
has_many :subscribers, :through => :subscriptions, :source => :user
end
You are renamingthe users association to subscribers. If you don't providethe :source, Rails will look for an associationcalled subscriber in the Subscription class. You have to tell it touse theuser association in the Subscription class to make the list ofsubscribers.
二、has_many
当你为你的class添加上has_many约束时,它将自动添加如下方法
collection(force_reload = false)
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=objects
collection_singular_ids
collection_singular_ids=ids
collection.clear
collection.empty?
collection.size
collection.find(...)
collection.where(...)
collection.exists?(...)
collection.build(attributes = {}, ...)
collection.create(attributes = {})
其中,collection是has_many 后面的第一个参数的符号就是会出现 has_many :collection这样的约束,注意,之前我们已经提过,has_many后面跟的应该是复数形式。collection_singular 是指collection的单数形式。有点抽象,举个例子,比如我们添加了如下的约束
class Customer < ActiveRecord::Base
has_many :orders
end
那么他将产生如下的几个函数
orders(force_reload = false)
orders<<(object, ...)
orders.delete(object, ...)
orders.destroy(object, ...)
orders=objects
order_ids
order_ids=ids
orders.clear
orders.empty?
orders.size
orders.find(...)
orders.where(...)
orders.exists?(...)
orders.build(attributes = {}, ...)
orders.create(attributes = {})
接下来我们将仔细讲解这些函数。
1. collection(force_reload= false)
他将返回一个数组,这个数组中存的是所有相关的记录。参数force_reload 是决定是否重新从数据库加载。
@orders = @customer.orders
2. collection<<(object, ...)
添加一条或多条记录
@customer.orders << @order1
3. collection.delete(object, ...)
删除一条或多条记录,它并不执行destroy函数,而是将他们的外键设为null,是否从数据库中删除,是由dependent关系决定的。这个在上面我们讲过。
@customer.orders.delete(@order1)
4. collection.destroy(object, ...)
删除一条或多条记录,它通过执行destroy函数执行,它会忽略dependent关系,直接从数据库中删除记录
@customer.orders.destroy(@order1)
5. collection=objects
修改为objects,它是通过添加和删除来保持合适的。
6. collection_singular_ids
返回一个array,里面存了collection中object的id
@order_ids = @customer.order_ids
7. collection_singular_ids=ids
使collection中只存在主键为ids中出现的记录,它是通过增加删除来实现的
8. collection.clear
清除记录,数据库中是否删除记录,决定于dependent关系。
9. collection.empty?
10. collection.size
11. collection.find(...)
查找,后面我们会仔细讲它的使用语法
@open_orders = @customer.orders.find(1)
12. collection.where(...)
这个方法根据提供的条件查找记录,但他并不是立刻查找,而是在真正使用时才进行查找。
@open_orders=@customer.orders.where(open: true) # No query yet
@open_order= @open_orders.first # Now the database will be queried
13. collection.exists?(...)
这个函数判断是否有满足条件的记录存在。
14. collection.build(attributes= {}, ...)
这个函数将返回一个或多个associated的新object,这些object是由传递的参数新建的,同时,外键联系也将被建立,但是他们还没有save。
@order=@customer.orders.build(order_date: Time.now, order_number: "A12345")
15. collection.create(attributes= {})
与上一个函数类似,只不过它会进行validation测试,假如通过了就会save。
has_many的参数
has_many参数有一下几个,上面讲过的就不在赘述
· 1. :as
它用于多态association,as是将has_many那个连接命名为polymorphic对应的连接。可以参考上面的。
· 2. :autosave
· 3. :class_name
· 4. :dependent
当它们的所有者被destroyed的时候决定该如何做,这个和上面的参数有点不同,比如A has_many B,我们说的将是A执行destroy的时候。但要注意,当你使用了:through的时候,这个选项就被忽略了。
5: :destroy 所有相关的B也将destroy,它是通过执行destroy函数执行的,也就是说它会经过回调函数
5.1 :delete_all直接从数据库中将相关的B删除,他并不通过回调函数。
5.2 :nullify 仅将外键联系设置为null,并不删除,也不通过回调函数。、
5.3 :restrict_with_exception 将抛出异常
5.4 :restrict_with_error 将抛出错误
· 6.:foreign_key
· 7. :inverse_of
· 8. :primary_key
· 9. :source
· 10. :source_type
· 11. :through
· 12. :validate
三、has_and_belongs_to_many
这个联系创建了一个多对多的联系。它是通过新建一个中间表来实现的。
它新加的函数如下,与has_many类似
· collection(force_reload=false)
· collection<<(object,...)
· collection.delete(object,...)
· collection.destroy(object,...)
· collection=objects
· collection_singular_ids
· collection_singular_ids=ids
· collection.clear
· collection.empty?
· collection.size
· collection.find(...)
· collection.where(...)
· collection.exists?(...)
· collection.build(attributes={})
· collection.create(attributes={})
它的参数如下 A has_and_belongs_to_many B
· :association_foreign_key 直接指定B的外键的名字
· :autosave
· :class_name
· :foreign_key 直接指定A的外键名字
· :join_table 指定join table的名字,也就是存放A和B外键表的名字
· :validate
· :readonly
Association Callbacks
Association 也是支持回调函数的,用法和之前讲的callback类似。
它支持的回调函数如下
· before_add
· after_add
· before_remove
· after_remove
四个的用法相同,类似于下面的形式
classCustomer <ActiveRecord::Base
has_many :orders, before_add: :check_credit_limit
defcheck_credit_limit(order)
...
end
end
你也可以采用流水式作业法,也就是说你可以在before_add前一次执行两个函数,分别进行加工,这样的逻辑性更强。用法很简单,你只要把两个函数名放在一个array中他们就会依次执行了。就如下面的例子
classCustomer < ActiveRecord::Base
has_many :orders,
before_add: [:check_credit_limit,:calculate_shipping_charges]
defcheck_credit_limit(order)
...
end
defcalculate_shipping_charges(order)
...
end
end