Ruby on rails 实战圣经:ActiveRecord 数据表关系



Debugging istwice as hard as writing the code in the first place. Therefore, if you writethe code as cleverly as possible, you are, by definition, not smart enough todebug it. — Brian W. Kernighan

ActiveRecord可以用Associations来定义数据表之间的关联性,这是最被大家眼睛一亮ORM功能。到目前为止我们学会了用ActiveRecord来操作数据库,但是还没充分发挥关系数据库的特性,那就是透过primary keyforeign keys将数据表互相关连起来。

Primary Key主键是一张数据表可以用来唯一识别的字段,而Foreign Key外部键则是用来指向别张数据表的Primary Key,如此便可以产生数据表之间的关联关系。了解如何设计正规化关系数据库请参考附录基础

PrimaryKey这个字段在Rails中,照惯例叫做id,型别是整数且递增。而ForeignKey字段照惯例会叫做{model_name}_id,型别是整数。

出现 ActiveModel::MassAssignmentSecurity::Error错误?

Rails 3.2.8 之后的 3.2.X 版本默认将 config/application.rb config.active_record.whitelist_attributes 设定改成 true,让大量赋值(Mass assignment)功能失效(详见安全性一章),造成范例中如 l = Location.new( :event => e ) 出现 ActiveModel::MassAssignmentSecurity::Error: Can'tmass-assign protected attributes: event 的错误。解决方式有 1. 修改 config/application.rb config.active_record.whitelist_attributes 设定为 false 2. 或是分开赋值 l=Location.new; l.event = e

Rails 4 之后又没这个问题了,该config.active_record.whitelist_attributes 设定被移除,默认改回允许大量赋值(Massassignment),所以本书范例就不做修正了。

一对一关联one-to-one

has_one diagram

延续Part1Event Model范例,假设一个Event拥有一个Location。来新增一个Location Model,其中的event_id就是外部键字段:

rails g model location name:string event_id:integer

执行bundle execrake db:migrate产生locations数据表。

分别编辑app/models/event.rbapp/models/location.rb

class Event < ActiveRecord::Base
    has_one :location # 單數
    #...
end
 
class Location < ActiveRecord::Base
    belongs_to :event # 單數
end

belongs_tohas_one这两个方法,会分别动态新增一些方法到LocationEvent Model上,让我们进入rails console实际操作数据库看看,透过Associations你会发现操作关联的对象非常直觉:

范例一,建立Location对象并关联到Event
e = Event.first
l = Location.new( :name => 'Hsinchu', :event => e ) 
# 等同於 l = Location.new( :name => 'Hsinchu', :event_id => e.id )
l.save
e.location
l.event

Event.first会捞出events table的第一笔数据,如果你第一笔还在,那就会是Event.find(1)。同理,Event.last会捞出最后一笔

范例二,从Event对象中建立一个Location
e = Event.first
l = e.build_location( :name => 'Hsinchu' )
l.save
e.location
l.event
范例三,直接从Event对象中建立一个Location
e = Event.first
l = e.create_location( :name => 'Hsinchu' )
e.location
l.event

一对多关联one-to-many

has_one diagram

一对多关联算是最常用的,例如一个Event拥有很多Attendee,来新增Attendee Model

rails g model attendee name:string event_id:integer

执行bundle execrake db:migrate产生attendees数据表。

分别编辑app/models/event.rbapp/models/attendee.rb

class Event < ActiveRecord::Base
    has_many :attendees # 複數
    #...
end
 
class Attendee < ActiveRecord::Base
    belongs_to :event # 單數
end

同样地,belongs_tohas_many这两个方法,会分别动态新增一些方法到AttendeeEvent Model上,让我们进入rails console实际操作数据库看看:

范例一,建立Attendee对象并关联到Event:
e = Event.first
a = Attendee.new( :name => 'ihower', :event => e ) 
#  a = Attendee.new( :name => 'ihower', :event_id => e.id )
a.save
e.attendees # 這是陣列
e.attendees.size
Attendee.first.event
范例二,从Event对象中建立一个Attendee:
e = Event.first
a = e.attendees.build( :name => 'ihower' )
a.save
e.attendees
范例三,直接从Event对象中建立一个Attendee:
e = Event.first
a = e.attendees.create( :name => 'ihower' )
e.attendees
范例四,先建立Attendee对象再放到Event:
e = Event.first
a = Attendee.create( :name => 'ihower' )
e.attendees << a
e.attendees
范例五,根据特定的Event查询Attendee
e = Event.first
e.id # 1
a = e.attendees.find(3)
attendees = e.attendees.where( :name => 'ihower' )

这样就可以写出限定在某个Event下的条件查询,用这种写法可以避免一些安全性问题,不会让没有权限的用户搜寻到别的EventAttendee

范例六,删除
e = Event.first
e.attendees.destroy_all # 一筆一筆刪除 e  attendee,並觸發 attendee  destroy 回呼
e.attendees.delete_all # 一次砍掉 e 的所有 attendees,不會觸發個別 attendee  destroy 

有个口诀可以记起来:有Foreign KeyModel,就是设定belongs_toModel

学到这里,还记得上一章建立的Category? 它也要跟Event是一对多的关系,让我们补上程序吧:

class Category < ActiveRecord::Base
    has_many :events
end
 
class Event < ActiveRecord::Base
  belongs_to :category
  # ...
end

多对多关联many-to-many

has_one diagram

has_one diagram

另一种常见的关联模式则是多对多,一笔数据互相拥有多笔数据,例如一个Event有多个Group,一个Group有多个Event。多对多关联的实现必须多一个额外关联用的数据表(又做作Join table),让我们来建立GroupModel和关联用的EventGroupshipModel,其中后者定义了两个ForeignKeys

rails g model group name:string
rails g model event_groupship event_id:integer group_id:integer

执行bundle execrake db:migrate产生这两个数据表。

分别编辑app/models/event.rbapp/models/group.rbapp/models/event_groupship.rb

class Event < ActiveRecord::Base
    has_many :event_groupships
    has_many :groups, :through => :event_groupships
end
 
class EventGroupship < ActiveRecord::Base
    belongs_to :event
    belongs_to :group
end
 
class Group < ActiveRecord::Base
    has_many :event_groupships
    has_many :events, :through => :event_groupships
end

这个Join table笔者的命名习惯会是ship结尾,用以凸显它的关联性质。另外,除了定义Foreign Keys之外,你也可以自由定义一些额外的字段,例如记录是哪位用户建立关联

blongs_tohas_many我们见过了,这里多一种has_many :through方法,可以神奇地把EventGroup关联起来,让我们进入rails console实际操作数据库看看:

范例,建立双向关联记录:
g = Group.create( :name => 'ruby taiwan' )
e1 = Event.first
e2 = Event.create( :name => 'ruby tuesday' )
EventGroupship.create( :event => e1, :group => g )
EventGroupship.create( :event => e2, :group => g )
g.events
e1.groups
e2.groups

Rails还有一种旧式的has_and_belongs_to_many方法也可以建立多对多关系,不过已经很少使用,在此略过不提

关连的参数

以上的关联方法blongs_tohas_onehas_many都还有一些可以客制的参数,让我们来介绍几个常用的参数,完整的参数请查询API文件:

class_name

可以变更关联的类别名称,例如:

class Event < ActiveRecord::Base
    belongs_to :manager, :class_name => "User" # 外部鍵是user_id
end
foreign_key

可以变更Foreign Key的域名,例如改成manager_id

class Event < ActiveRecord::Base
    belongs_to :manager, :class_name => "User", :foreign_key => "manager_id"
end
order

has_many可以透过:order参数指定顺序:

class Event < ActiveRecord::Base
    has_many :attendees, :order => "id desc"
    #...
end
dependent

可以设定当对象删除时,也会顺便删除它的has_many对象:

class Event < ActiveRecord::Base
    has_many :attendees, :dependent => :destroy
 end

:dependent可以有三种不同的删除方式,分别是:

  • :destroy会执行attendeedestroy

  • :delete不会执行attendeedestroy

  • :nullify这是默认值,不会帮忙删除attendee

    要不要执行attendee的删除回呼效率相差不少,如果需要的话,必须一笔笔把attendee读取出来变成attendee对象,然后呼叫它的destroy。如果用:delete的话,只需要一个SQL语句就可以删除全部attendee

joins includes 查询

针对Model中的belongs_tohas_many关连,可以使用joins,也就是INNER JOIN

Event.joins(:category)
# SELECT "events".* FROM "events" INNER JOIN "categories" ON "categories"."id" = "events"."category_id"

可以一次关连多个:

 Event.joins(:category, :location)

joins主要的用途是来搭配where的条件查询:

Event.joins(:category).where("categories.name is NOT NULL")
# SELECT "events".* FROM "events" INNER JOIN "categories" ON "categories"."id" = "events"."category_id" WHERE (categories.name is NOT NULL)

透过joins抓出来的event对象是没有包括其关连对象的。如果需要其关连对象的数据,会使用includesincludes可以预先将关连对象的数据也读取出来,避免N+1问题(见性能一章)

Event.includes(:category)
# SELECT * FROM events
# SELECT * FROM categories WHERE categories.id IN (1,2,3...)

同理,也可以一次加载多个关连:

Event.includes(:category, :attendees)
# SELECT "events".* FROM "events" 
# SELECT "categories".* FROM "categories" WHERE "categories"."id" IN (1,2,3...)
# SELECT "attendees".* FROM "attendees" WHERE "attendees"."event_id" IN (4, 5, 6, 7, 8...)

includes方法也可以加上条件:

Event.includes(:category).where( :category => { :position => 1 } )

更多在线资源


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值