ActiveRecord - 基本操作与关联设计

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

    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:

    这个指令会产生几个档案

    1. category_test.rb
    2. categories.yml
    3. xxxxxxxx_create_categories.rb

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

    1. class CreateCategories < ActiveRecord::Migration[5.1]
    2. def change
    3. create_table :categories do |t|
    4. t.string :name
    5. t.integer :position
    6. t.timestamps
    7. end
    8. add_column :events, :category_id, :integer
    9. add_index :events, :category_id
    10. end
    11. end

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

    1. bin/rake db:migrate

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

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

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

    1. class Category < ApplicationRecord

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

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

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

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

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

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

    1. rails g model attendee name:string event_id:integer

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

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

    1. class Event < ApplicationRecord
    2. has_many :attendees # 复数
    3. #...
    4. end
    5. class Attendee < ApplicationRecord
    6. belongs_to :event # 单数
    7. end

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

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

    范例一,建立Attendee物件并关联到Event:

    范例二,从Event物件中建立一个Attendee:

    1. e = Event.first
    2. a = e.attendees.build( :name => 'ihower' )
    3. a.save
    4. e.attendees

    范例三,从Event物件中建立一个Attendee,并直接存进数据库:

    1. a = e.attendees.create( :name => 'ihower' )
    2. e.attendees

    范例四,先建立Attendee物件再放到Event中:

    1. e = Event.first
    2. a = Attendee.create( :name => 'ihower' )
    3. e.attendees << a
    4. e.attendees

    范例五,根据特定的Event查询Attendee

    1. e = Event.first
    2. e.id # 1
    3. a = e.attendees.find(3)
    4. attendees = e.attendees.where( :name => 'ihower' )

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

    范例六,删除

    1. e = Event.first
    2. e.attendees.destroy_all # 一笔一笔删除 e 的 attendee,并触发 attendee 的 destroy 回呼
    3. e.attendees.delete_all # 一次砍掉 e 的所有 attendees,不会触发个别 attendee 的 destroy 回呼

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

    1. class Category < ApplicationRecord
    2. has_many :events
    3. end
    4. class Event < ApplicationRecord
    5. belongs_to :category, :optional => true
    6. # ...
    7. end

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

    has_one diagram

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

    1. rails g model location name:string event_id:integer

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

    1. class Event < ApplicationRecord
    2. has_one :location # 单数
    3. #...
    4. end
    5. class Location < ApplicationRecord
    6. belongs_to :event # 单数

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

    范例一,建立Location物件并关联到Event:

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

    范例二,从Event物件中建立一个Location:

    1. e = Event.first
    2. l = e.build_location( :name => 'Hsinchu' )
    3. l.save
    4. e.location
    5. l.event

    范例三,直接从Event物件中建立一个Location:

    1. e = Event.first
    2. e.location
    3. l.event

    has_one diagram

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

    1. rails g model group name:string
    2. rails g model event_groupship event_id:integer group_id:integer

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

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

    1. class Event < ApplicationRecord
    2. has_many :event_groupships
    3. has_many :groups, :through => :event_groupships
    4. end
    5. class EventGroupship < ApplicationRecord
    6. belongs_to :event
    7. belongs_to :group
    8. end
    9. class Group < ApplicationRecord
    10. has_many :event_groupships
    11. has_many :events, :through => :event_groupships
    12. end

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

    范例,建立双向关联记录:

    1. g = Group.create( :name => 'ruby taiwan' )
    2. e1 = Event.first
    3. e2 = Event.create( :name => 'ruby tuesday' )
    4. EventGroupship.create( :event => e1, :group => g )
    5. EventGroupship.create( :event => e2, :group => g )
    6. g.events
    7. e1.groups
    8. e2.groups

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

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

    增加条件范围

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

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

    甚至是串连where条件:

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

    删除依赖的资料

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

    1. class Event < ApplicationRecord
    2. has_one :location, :dependent => :destroy
    3. has_many :attendees, :dependent => :destroy