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

Ruby on rails 实战圣经:ActiveRecord

热度:17   发布时间:2023-12-09 08:46:16.0


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 应用程序一章中提到的对应概念如下:

  1. 将数据库表格(table) 对应到一个类别(class)
  2. 类别方法就是操作表格(table)
  3. 将数据库一列 (row) 对应到一个对象(object)
  4. 对象方法就是操作个别的数据(row)
  5. 将数据库字段(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,分别是savesave!createcreate!

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 )

其中createcreate!就等于new完就savesave!,有无惊叹号的差别在于validate数据验证不正确的动作,无惊叹号版本会回传布尔值(truefalse),有惊叹号版本则是验证错误会丢出例外。

何时使用惊叹号版本呢?savecreate通常用在会处理回传布尔值(true/false)的情况下(例如在 controller 里面根据成功失败决定 render redirect),否则在预期应该会储存成功的情况下,请用 save!create! 来处理,这样一旦碰到储存失败的情形,才好追踪 bug

透过 :validate => false 可以略过验证

c.save( :validate => false )

Rails3 之前的版本是 user.save(false)

如何查询

ActiveRecord 使用了 Arel 技术来实现查询功能,你可以自由组合 wherelimitselectorder 等条件。

Arel relational algebra” library。但根据 2.0 实现者 tenderlove 的说法,也可以说是一种 SQL compilerhttp://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_namelast_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

更多在线资源

  1. Active Record QueryInterface http://guides.rubyonrails.org/active_record_querying.html