All problems in computerscience can be solved by another level of indirection(abstraction) - DavidWheeler ...except for the problem of too many layers of indirection. - KevlinHenney’s corollary
ActiveRecord 是 Rails 的 ORM 组件,负责与数据库沟通,让我们可以用面向对象的语法操作数据库。在”打造 CRUD 应用程序”一章中提到的对应概念如下:
- 将数据库表格(table) 对应到一个类别(class)
- 类别方法就是操作表格(table)
- 将数据库一列 (row) 对应到一个对象(object)
- 对象方法就是操作个别的数据(row)
- 将数据库字段(column) 对应到对象的属性(object attribute)
因此,数据库里面的数据表,我们用一个 Model 类别来表示,而其中的一笔数据,就是一个 Model 对象。
ActiveRecord 这个函式库实现了 Martin Fowler 的 Active Record 设计模式(Design Pattern) http://martinfowler.com/eaaCatalog/activeRecord.html
ORM 与抽象渗漏法则
ORM (Object-relationalmapping ) 是一种对映设关系型数据与对象数据的程序技术。面向对象和从数学理论发展出来的关系数据库,有着显著的区别,而 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 你可以看到数据表的定义,让我们加上几个字段吧,除了建立categiries
表,同时也帮events加上一个外部键让两个表可以关连起来,在后几章会用到:
class CreateCategories < ActiveRecord::Migration
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
接着执行以下指令便会产生出数据库数据表
bundle exec 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 < ActiveRecord::Base
end
这是一个继承 ActiveRecord::Base 的 Category 类别。
我们在学习 Ruby 的时候提过 irb 这个互动工具,而Rails 也提供了特殊的 irb 接口叫做 console,让我们可以直接与Rails 程序互动:
rails console (
可以簡寫成
rails c)
透过 console,我们可以轻易的练习操作 ActiveRecord。
观看Log
不像 rails server 可以直接看到 log,在 Rails 控制台下必须透过观察 log 档案。我们可以透过log 观察到 Rails 产生出来的 SQL 长的如何。
tail -f log/development.log
Windows 上没有这个指令,可以安装Tail for Win32这个工具来实时观察 log 档案。或是安装GNU utilities for Win32来获得 tail 指令。
基础操作
如何新增
ActiveRecord提供了四种API,分别是save、save!、create和create!:
a = Category.new( :name => 'Ruby', :position => 1 )
a.save
b = Category.new( :name => 'Perl', :position => 2 )
b.save!
Category.create( :name => 'Python', :position => 3 )
c = Category.create!( :name => 'PHP', :position => 4 )
其中create和create!就等于new
完就save和save!,有无惊叹号的差别在于validate数据验证不正确的动作,无惊叹号版本会回传布尔值(true或false),有惊叹号版本则是验证错误会丢出例外。
何时使用惊叹号版本呢?save和create通常用在会处理回传布尔值(true/false)的情况下(例如在 controller 里面根据成功失败决定 render 或redirect),否则在预期应该会储存成功的情况下,请用 save!或create! 来处理,这样一旦碰到储存失败的情形,才好追踪 bug。
透过 :validate => false 可以略过验证
c.save( :validate => false )
在Rails3 之前的版本是 user.save(false)
如何查询
ActiveRecord 使用了 Arel 技术来实现查询功能,你可以自由组合 where、limit、select、order 等条件。
Arel 是relational algebra” library。但根据 2.0 实现者 tenderlove 的说法,也可以说是一种 SQL compiler。http://engineering.attinteractive.com/2010/12/architecture-of-arel-2-0/
first, last 和 all
这三个方法可以分别拿出数据库中的第一笔、最后一笔及全部的数据:
c1 = Category.first
c2 = Category.last
categories = Category.all #
這會是一個陣
列
如果数据量较多,请不要在正式上线环境中执行.all 把所有数据拿出来,这样会耗费非常多的内存。请用分页或缩小查询范围。
find
已知数据的主键 ID 的值的话,可以使用 find 方法:
c3 = Category.find(1)
c4 = Category.find(2)
find 也可以接受数组参数,这样就会一次找寻多个并回传数组:
arr = Category.find([1,2])
#
或是
arr = Category.find(1,2)
如果找不到数据的话,会丢 ActiveRecord::RecordNotFound 例外。如果是 find_by_id 就不会丢出例外,而是回传 nil。
find_by_sql
如果需要手动撰写 SQL,可以使用 find_by_sql,例如:
c8 = Category.find_by_sql("select * from categories")
不过在绝大多数的情况,是不需要手动写 SQL 的。
where 查询条件
where 可以非常弹性的组合出 SQL 查询,例如:
c9 = Category.where( :name => 'Ruby', :position => 1 )
c10 = Category.where( [ "name = ? or position = ?", 'Ruby', 2] )
其中参数有两种写法,一种是 Hash,另一种是 Array。前者的写法虽然比较简洁,但是就没办法写出 or 的查询。注意到不要使用字符串写法,例如
Category.where("name = #{params[:name]}") #
請不要這樣
寫
这是因为字符串写法会有 SQL injection 的安全性问题,请改用数组写法。
另外,where 是 lazy loading,也就是直到真的需要取值的时候,才会跟数据库拿数据。如果需要立即触发,可以接着使用 .all, .first, .last,例如
c11 = Category.where( :name => 'Ruby', :position => 1 ).all
limit
limit 可以限制笔数
c = Category.limit(5).all
c.size # 5
order
order 可以设定排序条件
Category.order("position")
Category.order("position DESC")
Category.order("position DESC, name ASC")
如果要消去order条件,可以用reorder
:
Category.order("position").reorder("name") #
改用
name
排序
Category.order("position").reorder(nil) #
取消所有排
序
offset
offset 可以设定忽略前几笔不取出,通常用于数据分页:
c = Category.limit(2)
c.first.id # 1
c = Category.limit(2).offset(3)
c.first.id # 4
select
默认的 SQL 查询会取出数据的所有字段,有时候你可能不需要所有数据,为了性能我们可以只取出其中特定字段:
Category.select("id, name")
例如欄位中有 Binary 資料時,你不會希望每次都讀取出龐大的 Binary 資料佔用記憶體,而只希望在使用者要下載的時候才讀取出來。
readonly
c = Category.readonly.first
如此查詢出來的c
就無法修改或刪除,不然會丟出ActiveRecord::ReadOnlyRecord
例外。
group 和 having
group
運用了資料庫的group_by
功能,讓我們可以將計算後的結果依照某一個欄位分組後回傳,例如說今天我有一批訂單,裡面有分店的銷售金額,我希望能這些金額全部加總起來變成的各分店銷售總金額,這時候我就可以這麼做:
Order.select("store_name, sum(sales)").group("store")
這樣會執行類似這樣的SQL:
SELECT store_name, sum(sales) FROM orders GROUP BY store_name
having
則是讓group
可以再增加條件,例如我們想為上面的查詢增加條件是找出業績銷售超過10000的分店,那麼我可以這麼下:
Order.select("store_name, sum(sales)").group("store").having("sum(sales) > ?", 10000)
所執行的SQL便會是:
SELECT store_name, sum(sales) FROM orders GROUP BY store_name HAVING sum(sales) > 10000
串接寫法
以上的 where, order , limit,offset, joins, select 等等,都可以自由串接起來組合出最終的 SQL 條件:
c12 = Category.where( :name => 'Ruby' ).order("id desc").limit(3)
find_each 批次處理
如果資料量很大,但是又需要全部拿出來處理,可以使用find_each 批次處理
Category.where("position > 1").find_each do |category|
category.do_some_thing
end
預設會批次撈 1000 筆,如果需要設定可以加上 :batch_size 參數。
重新載入
如果已經讀取的 AR 資料,需要重新載入,可以用 reload 方法:
p = Category.first
p.reload
如何刪除
一種是先抓到該物件,然後刪除:
c12 = Category.first
c12.destroy
另一種是直接對類別呼叫刪除,傳入 ID 或條件:
Category.delete(2)
Category.delete_all(conditions = nil)
Category.destroy_all(conditions = nil)
delete 不會有 callback 回呼,destroy有 callback 回呼。什麼是回呼詳見下一章。
統計方法
Category.count
Category.average(:position)
Category.maximum(:position)
Category.sum(:position)
其中我們可以利用上述的 where 條件縮小範圍,例如:
Category.where( :name => "Ruby").count
如何更新
c13 = Category.first
c13.update_attributes(attributes)
c13.update_attributes!(attributes)
c13.update_attribute(attribute_name, value)
注意update_attribute 會略過 validation 資料驗證注意 mass assign 安全性問題,可以透過 attr_protected 或 attr_accessor 設定,詳見安全性一章。
Scopes 作用域
Model Scopes是一項非常酷的功能,它可以將常用的查詢條件宣告起來,讓程式變得乾淨易讀,更厲害的是可以串接使用。例如,我們編輯app/models/event.rb,加上兩個Scopes:
class Event < ActiveRecord::Base
scope :public, where( :is_public => true )
scope :recent_three_days, where(["created_at > ? ", Time.now - 3.days ])
end
Event.create( :name => "public event", :is_public => true )
Event.create( :name => "private event", :is_public => false )
Event.create( :name => "private event", :is_public => true )
Event.public
Event.public.recent_three_days
串接的順序沒有影響
接著,我們可以設定一個預設的Scope,通常會拿來設定排序:
class Event < ActiveRecord::Base
default_scope order('id DESC')
end
unscoped
方法可以暫時取消預設的default_scope:
Event.unscoped do
Event.all
# SELECT * FROM events
end
最後,Scope也可以接受參數,例如:
class Event < ActiveRecord::Base
scope :recent, lambda{ |date| where(["created_at > ? ", date ]) }
#
或
scope :recent, Proc.new{ |t| where(["created_at > ? ", t ]) }
end
Event.recent( Time.now - 7.days )
不過,筆者會推薦上述這種帶有參數的Scope,改成如下的類別方法,可以比較明確看清楚參數是什麼,特別是你想給預設值的時候:
class Event < ActiveRecord::Base
def self.recent(t=Time.now)
where(["created_at > ? ", t ])
end
end
Event.recent( Time.now - 7.days )
這樣的效果是一樣的,也是一樣可以和其他Scope做串接。
scoped
方法可以將Model轉成可以串接的形式,方便依照參數組合出不同查詢,例如
fruits = Fruit.scoped
fruits = fruits.where(:colour => 'red') if options[:red_only]
fruits = fruits.limit(10) if limited?
可以呼叫to_sql
方法觀察實際ORM轉出來的SQL,例如Event.public.recent_three_days.to_sql
虚拟属性(VirtualAttribute)
有时候窗体里操作的属性数据,不一定和数据库的字段完全对应。例如数据表分成first_name和last_name两个字段好了,但是窗体输入和显示的时候,只需要一个属性叫做full_name,这时候你就可以在model里面定义这样的方法:
def full_name
"#{self.first_name} #{self.last_name}"
end
def full_name=(value)
self.first_name, self.last_name = value.to_s.split(" ", 2)
end
更多在线资源
- Active Record QueryInterface http://guides.rubyonrails.org/active_record_querying.html