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 key和foreign 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
延续Part1的Event Model范例,假设一个Event拥有一个Location。来新增一个Location Model,其中的event_id就是外部键字段:
rails g model location name:string event_id:integer
执行bundle execrake db:migrate
产生locations数据表。
分别编辑app/models/event.rb和app/models/location.rb:
class Event < ActiveRecord::Base
has_one :location #
單數
#...
end
class Location < ActiveRecord::Base
belongs_to :event #
單數
end
belongs_to和has_one这两个方法,会分别动态新增一些方法到Location和Event 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
一对多关联算是最常用的,例如一个Event拥有很多Attendee,来新增Attendee Model:
rails g model attendee name:string event_id:integer
执行bundle execrake db:migrate
产生attendees数据表。
分别编辑app/models/event.rb和app/models/attendee.rb:
class Event < ActiveRecord::Base
has_many :attendees #
複數
#...
end
class Attendee < ActiveRecord::Base
belongs_to :event #
單數
end
同样地,belongs_to和has_many这两个方法,会分别动态新增一些方法到Attendee和Event 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下的条件查询,用这种写法可以避免一些安全性问题,不会让没有权限的用户搜寻到别的Event的Attendee。
范例六,删除
e = Event.first
e.attendees.destroy_all #
一筆一筆刪除
e
的
attendee
,並觸發
attendee
的
destroy
回呼
e.attendees.delete_all #
一次砍掉
e
的所有
attendees
,不會觸發個別
attendee
的
destroy
回
呼
有个口诀可以记起来:有Foreign Key的Model,就是设定belongs_to
的Model。
学到这里,还记得上一章建立的Category
吗? 它也要跟Event
是一对多的关系,让我们补上程序吧:
class Category < ActiveRecord::Base
has_many :events
end
class Event < ActiveRecord::Base
belongs_to :category
# ...
end
多对多关联many-to-many
另一种常见的关联模式则是多对多,一笔数据互相拥有多笔数据,例如一个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.rb、app/models/group.rb和app/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_to和has_many我们见过了,这里多一种has_many :through方法,可以神奇地把Event和Group关联起来,让我们进入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_to、has_one和has_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
会执行attendee的destroy回呼 -
:delete
不会执行attendee的destroy回呼 -
:nullify
这是默认值,不会帮忙删除attendee要不要执行attendee的删除回呼效率相差不少,如果需要的话,必须一笔笔把attendee读取出来变成attendee对象,然后呼叫它的destroy。如果用
:delete
的话,只需要一个SQL语句就可以删除全部attendee。
joins 和 includes 查询
针对Model中的belongs_to
和has_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对象是没有包括其关连对象的。如果需要其关连对象的数据,会使用includes
。includes可以预先将关连对象的数据也读取出来,避免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 } )
更多在线资源
-
A Guide toActive Record Associations http://guides.rubyonrails.org/association_basics.html