Active Record基本操作与关联设计

ActiveRecord - 基本操作与关联设计

All problems in computer science can be solved by another level of indirection(abstraction) - David Wheeler…except for the problem of too many layers of indirection. - Kevlin Henney’s corollary

请注意本章内容衔接前两章,请先完成前两章内容。

ORM 与抽象渗漏法则

ORM (Object-relational mapping ) 是一种对映射关联式资料与物件资料的程式技术。物件导向和从数学理论发展出来的关联式数据库,有着显着的区别,而 ORM 正是解决这个不匹配问题所产生的工具。它可以让你使用物件导向语法来操作关联式数据库,非常容易使用、撰码十分有效率,不需要撰写繁琐的SQL语法,同时也增加了程式码维护性。

不过,有些熟悉 SQL 语法的程式设计师反对使用这样的机制,因为直接撰写 SQL 可以确保操作数据库的执行效率,毕竟有些时候 ORM 产生出来的 SQL 效率不是最佳解,而你却不一定有经验能够意识到什么时候需要担心或处理这个问题。

知名软件人 Joel Spolsky (他有两本中文翻译书值得推荐:约耳趣谈软件和约耳续谈软件,悦知出版) 有个理论:抽象渗漏法则:所有重大的抽象机制在某种程式上都是有漏洞的。有非常多程式设计其实都是在建立抽象机制,C 语言简化了组合组言的繁杂、动态语言如 Ruby 简化了 C 语言、TCP 协定简化了 IP 通讯协定,甚至车子的挡风玻璃跟雨刷也简化了下雨的事实。

但是这些抽象机制或多或少都会力有未及的地方,用 C 语言撰写的 Linux 核心也包括少量组合语言、部分 Ruby 套件用 C 语言撰写扩充来增加效能、保证讯息会抵达 TCP 讯息,碰到 IP 封包在路由器上随机遗失的时候,你也只会觉得速度很慢、即使有挡风玻璃跟雨刷,开车还是必须小心路滑。

当某人发明一套神奇可以大幅提升效率的新程式工具时,就会听到很多人说:「应该先学会如何手动进行,然后才用这个神奇的工具来节省时间。」任何抽象机制都有漏洞,而唯一能完美处理漏洞的方法,就是只去弄懂该抽象原理以及所隐藏的东西。这是否表示我们应该永远只应该使用比较低阶的工具呢?不是这样的。而是应该依照不同的情境,选择效益最大的抽象化工具。以商务逻辑为多的 Web 应用程式,选择动态语言开发就相对合适,用 C 语言开发固然执行效率极高,但是完成相同的功能却需要极高的人月开发时数。如果是作业系统,使用无法随意控制内存分配的动态语言也显然不是个好主意。

能够意识到什么时候抽象化工具会产生渗漏,正是”有纯熟经验”的程式设计师和”新手”设计师之间的差别。ORM 虽然替我们节省了工作的时间,不过对资深的程式设计师来说,学习 SQL 的时间还是省不掉的。这一切都似乎表示,即使我们拥有愈来愈高阶的程式设计工具,抽象化也做得愈来愈好,要成为一个由高阶到低阶都纯熟的程式设计专家是愈来愈困难了(也越来越稀有及宝贵)。

建立新 Model

首先,让我们再示范如何建立一个 Model:

rails g model category

这个指令会产生几个档案

category.rb
category_test.rb
categories.yml
xxxxxxxx_create_categories.rb

打开 xxxxxxxx _create_categories.rb 你可以看到资料表的定义,让我们加上几个字段吧,除了建立categories表,同时也帮_events_加上一个外部键让两个表可以关连起来,在后一章会用到:

class CreateCategories < ActiveRecord::Migration[5.1]
  def change
    create_table :categories do |t|
      t.string :name
      t.integer :position
      t.timestamps
    end

    add_column :events, :category_id, :integer
    add_index :events, :category_id
  end
end

接着执行以下指令便会产生出数据库资料表

bin/rake db:migrate

db:migrate 指令会将上述的 Ruby 程式变成以下 SQL 执行。

CREATE TABLE categories (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"name" varchar(255) DEFAULT NULL,
"position" int(4) DEFAULT NULL,
"created_at" datetime DEFAULT NULL,
"updated_at" datetime DEFAULT NULL);

接着我们打开 category.rb 你可以看到

class Category < ApplicationRecord
end

这是一个继承 ApplicationRecord 的 Category 类别,你不需要定义这个 Model 有哪些字段, Rails 会自动根据资料表纲要决定这个 Model 有哪些属性。

我们在学习 Ruby 的时候提过 irb 这个互动工具,而 Rails 也提供了特殊的 irb 接口叫做 console,让我们可以直接与 Rails 程式互动:

bin/rails console (可以简写成 bin/rails c)

透过 console,我们可以轻易的练习操作 ActiveRecord。 TODO: 需要重写一段导论> Part 1 Basic 那章有非常基本的 Query 示范(可能需要改好一点)> Part 2 Query 那章则有完整的 Query 教学> 这里需要介于中间的介绍版本:### ActiveRecord 的特点1. 宣告风格2. 查询串接3. 惯例胜于设计—>

资料表关联设计

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

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

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

一对多关联one-to-many

has_one diagram

一对多关联算是最常用的,延续 Part1 的 Event Model 范例,一个 Event 拥有很多 Attendee 。我们来新增 Attendee Model :

rails g model attendee name:string event_id:integer

执行bin/rake db:migrate产生 attendees 资料表。

分别编辑 app/models/event.rb 和 app/models/attendee.rb :

class Event < ApplicationRecord
    has_many :attendees # 复数
    #...
end

class Attendee < ApplicationRecord
    belongs_to :event # 单数
end

有个口诀可以记起来:有 Foreign Key 的 Model ,就是设定belongs_to的Model。在 attendees 资料表上有个event_id的 Foreign Key 。

同样地, 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 回呼

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

class Category < ApplicationRecord
    has_many :events
end

class Event < ApplicationRecord
  belongs_to :category, :optional => true
  # ...
end

这里多了一个参数是 :optional => true,也就是允许 event 没有 category 的情况。

一对一关联one-to-one

has_one diagram

什么时候会需要一对一关联设计呢?直接合并在 events table 只用一个 Event Model 不也可以。会这样设计通常是为了节省查询量,例如 Event 有一个很包含非常多字段的 event_detaileds table 储存细节资料,一来只有进到详细页面才会用到、二来可能不是每一笔 Event 都有这个细节资料,所以拆表之后可以让绝大部分的操作都不需要去碰到 event_detaileds table 。

一对一关联算是一对多关联的一种特例情况。假设一个 Event 拥有一个 Location 。来新增一个 Location Model ,其中的 event_id 就是外部键字段:

rails g model location name:string event_id:integer

执行bin/rake db:migrate产生 locations 资料表。

分别编辑 app/models/event.rb 和 app/models/location.rb :

class Event < ApplicationRecord
    has_one :location # 单数
    #...
end

class Location < ApplicationRecord
    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

多对多关联many-to-many

has_one diagram

has_one diagram

另一种常见的关联模式则是多对多,一笔资料互相拥有多笔资料,例如一个 Event 有多个 Group ,一个 Group 有多个 Event 。多对多关联的实作必须多一个额外关联用的资料表(又叫作 Join table),让我们来建立 Group Model 和关联用的 EventGroupship Model ,其中后者定义了两个 Foreign Keys :

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

执行bin/rake db:migrate产生这两个资料表。

分别编辑 app/models/event.rb 、 app/models/group.rb 和 app/models/event_groupship.rb :

class Event < ApplicationRecord
    has_many :event_groupships
    has_many :groups, :through => :event_groupships
end

class EventGroupship < ApplicationRecord
    belongs_to :event
    belongs_to :group
end

class Group < ApplicationRecord
    has_many :event_groupships
    has_many :events, :through => :event_groupships
end

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

belongs_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 方法也可以建立多对多关系,不过已经很少使用,在此略过不提。

常用的关连参数

以上的关联方法 belongs_to 、 has_one 和 has_many 都还有一些可以客制的参数,让我们来介绍最常用的几个参数:

增加条件范围

上述关联宣告都可以再加上条件范围,例如加上order指定顺序:

class Event < ApplicationRecord
  has_many :attendees, ->{ order("id DESC") }
  #...
end

甚至是串连where条件:

class Event < ApplicationRecord
  has_many :attendees, ->{ where(["created_at > ?", Time.now - 7.day]).order("id DESC") }
  #...
end

删除依赖的资料

可以设定当物件删除时,也会顺便删除依赖它的资料:

class Event < ApplicationRecord
  has_one :location, :dependent => :destroy
  has_many :attendees, :dependent => :destroy
end

这样的话,当该@event删除时,也会跟着删除@event.location和所有@event.attendees了。

线上参考资源