当前位置: 代码迷 >> 综合 >> Ruby on rails 实战圣经:ActiveRecord 数据表关系
  详细解决方案

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

热度:64   发布时间:2023-12-09 08:45:50.0


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 } )

更多在线资源

  • A Guide toActive Record Associations http://guides.rubyonrails.org/association_basics.html