ActiveRecord教程
(一、ActiveRecord基础)
ActiveRecord是Rails提供的一个对象关系映射(ORM)层,从这篇开始,我们来了解Active Record的一些基础内容,连接数据库,映射表,访问数据等。
Active
Record使用基本的ORM模式:表映射成类,行映射成为对象,列映射成对象的属性。与很多大量使用配置的ORM库不同,Active
Record最小化了配置。想象一下,有一个使用Active
Record的程序把Mysql数据库中的orders表转换到类,通过制定的ID查找到order,设定order的名称,然后保存回数据库:
require "rubygems"
require_gem "activerecord"
ActiveRecord::Base.establish_connection(:adapter => "mysql",
:host => "localhost", :database => "railsdb")
class Order < ActiveRecord::Base
end
order = Order.find(123)
order.name = "Dave Thomas"
order.save
在上面的例子里不需要任何配置,Active Record为我们做了这些事情,下面我们来看看ActiveRecord是怎样工作的。
表和类
当你创建了一个ActiveRecord::Base类的子类,Active Record假定表名是复数的,而类名是单数的,当类名包括多个单词时,表名被假定为单词间带有下划线,复数形式不规则,例如:
类名 表名 类名 表名
Order orders LineItem line_items
TaxAgency tax_agencies Person people
Diagnosis diagnoses Quantity quantities
Batch batches Datum data
默认的,Active Record的表名是复数的,类名是单数的,如果你不太习惯,可以通过设置一个全局标记来禁用它,在config目录的environment.rb文件中设置:
ActiveRecord::Base.pluralize_table_names = false
单复数规则可以对付大部分情况,对于一些特殊情况,Active Record允许我们覆盖默认的生成的表名,使用set_table_name命令,例如:
class Sheep < ActiveRecord::Base
set_table_name "sheep" # Not "sheeps"
end
class Order < ActiveRecord::Base
set_table_name "ord_rev99_x" # Wrap a legacy table...
end
(二、列和属性)
ActiveRecord中的一个对象相当于数据库中表的一行,对象的属性对应于表的列,也许你会注意到我们的Order类没有提及关于
orders表的任何东西,这是因为ActiveRecord在运行时来确定这些对应关系,Active Record将数据库中的模式反应到类中。
我们的orders表可能使用下面的sql来创建:
create table orders (
id int not null auto_increment,
name varchar(100) not null,
email varchar(255) not null,
address text not null,
pay_type char(10) not null,
shipped_at datetime null,
primary key (id)
);
我们可以创建一个类来转换这个表:
require 'rubygems'
require_gem 'activerecord'
# Connection code omitted...
class Order < ActiveRecord::Base
end
当我们创建了Order类,就可以访问它的属性来获取信息,下面的代码使用columns()方法,来返回一个Columns对象的数组,在这里,我们显示了orders表中的每个列,并且显示指定字段的详细信息。
require 'pp'
pp Order.columns.map { |col| col.name }
pp Order.columns_hash['shipped_at']
运行代码,会得到下面的输出:
["id", "name", "email", "address", "pay_type", "shipped_at"]
#<ActiveRecord::ConnectionAdapters::Column:0x10e4a50
@default=nil,
@limit=nil,
@name="shipped_at",
@type=:datetime>
注意,Active Record决定了每个列的类型,在这个例子里,将shipped_at列作为datetime类型,该列的值被保存在一个ruby的Time类型的对象中,我们可以写些代码来验证该列的类型及其内容:
order = Order.new
order.shipped_at = "2005-03-04 12:34"
pp order.shipped_at.class
pp order.shipped_at
输出为:
Time
Fri Mar 04 12:34:00 CST 2005
下面的列表展示了sql和ruby间的数据类型对应关系:
SQLType Ruby Class SQLType Ruby Class
int, integer Fixnum float, double Float
decimal, numeric Float char, varchar, string String
clob, blob, text String datetime, time Time
interval, date Date Boolean 后面详细介绍
有一个潜在的可能是关于decimal的,在数据库里,使用decimal的列来存储number和fix number型,Active
Record将decimal映射成Float类的对象,尽管这样可以应用于大多数应用,浮点数是不精确的,在对这一类型的属性进行一系列操作的时候,可
能会发生舍入的错误,你也许可以使用integer类型来作为替代方案,例如,存储货币型的时候可以将元,角,分,分别存入不同的字段。做为一种选择,你
可以使用聚合(aggregations),使用多个分开的字段来构建货币类型。
(三、Boolean属性)
一些数据库支持boolean类型,而另一些则不支持,这使得Active
Record要抽象boolean类型变得困难。例如,如果数据库不支持boolean类型,有的开发者使用char(1)来替代,而内容使用“t”和
“f”来表示true和false,而另外一些开发者使用integer类型,0是false,1是true。即使数据库支持boolean类型,在内部
也许还是使用0和1来存储。
在Ruby里,在条件判断中,数字0和字符f都被认为是true值,这就意味着如果你直接使用属性的值,你的代码会被认为该列的值是true,而不是你认为的false,例如:
# 不要这样使用
user = Users.find_by_name("Dave")
if user.superuser
grant_privileges
end
当在查询条件中使用属性时,你必须在列名后添加一个问号:
# 这样是正确的
user = Users.find_by_name("Dave")
if user.superuser?
grant_privileges
end
当使用访问操作符来获取属性的值时,当值为数字0,或者字符“0”,“f”,“false”,或“”(空字符串),或nil,或一个常量false时,都被认为是false,否则,就会被认为是true。
如果你在一个遗留系统上或者非英语系统上开发,上面对true的定义也许会无法工作,在这种情况下,你可以override内建的谓词方法的定义,例如,荷兰语情况下,字段也许包含J或者N,这种情况下,你可以像下面这样:
class User < ActiveRecord::Base
def superuser?
self.superuser == 'J'
end
# . . .
end
(四、存储结构化数据)
有时,能够在某个属性中直接存储任意的ruby对象是很方便的,一种办法就是Active Record支持序列化,将一个ruby对象变为一个YMAL字符串,并且将这个字符串存储到属性对应的数据库字段中。在数据库定义中,这个字段必须为text类型。
因为Active Record将数据库中的Char型和text型映射为ruby的string型,所以如果我们需要告诉Active Record使用序列化功能,例如,我们想知道某个客户进行的最后的5次消费,我们创建一个含有text类型字段的表来保存信息:
create table purchases (
id int not null auto_increment,
name varchar(100) not null,
last_five text,
primary key (id)
);
在转换这个表的Active Record类中,我们要使用serialize()声明,来告诉Active Record要排列对象:
class Purchase < ActiveRecord::Base
serialize :last_five
# ...
end
当我们创建了一个新的Purchase对象,我们可以给last_five列赋任何值,在这个例子里,我们给last_five列设置一个字符串数组,
purchase = Purchase.new
purchase.name = "Dave Thomas"
purchase.last_five = [ 'shoes', 'shirt', 'socks', 'ski mask', 'shorts' ]
purchase.save
当我们读入它的时候,这个属性已经被设置为数组:
purchase = Purchase.find_by_name("Dave Thomas")
pp purchase.last_five
pp purchase.last_five[3]
代码的输出为:
["shoes", "shirt", "socks", "ski mask", "shorts"]
"ski mask"
尽管这个功能是很强大且便利的,但是只有当你不打算在ruby以外的项目中使用这些序列化的信息,除非那个程序也能够使用YMAL格式。特别
是,这些信息很难被SQL查询所利用,你也许会考虑使用聚合(aggregation)来替代,在后面我们会介绍这种办法来达到相同的效果。
(五主键和ID)
也许你已经注意到了,在我们前面的代码中,数据库定义里都使用了一个integer型的字段id作为主键,这是Active Record的一个约定。
或许你要问,为什么不用订单编号或者某个有意义的列来作为主键呢?使用id作为主键有一个很重要的原因,就是如果使用具有内在格式的主键的
话,随着时间推移,有可能其中的规则也会变化。例如,使用ISBN号码来给book表做主键,毕竟ISBN号码是唯一的,但是,有可能当一本书写完后,美
国的出版业已经发展了并且在所有的ISBN号码后又附加了一位数字。
如果我们使用了ISBN作为book表的主键,我们就要更新所有book表的记录来反映这个变化,而且还有一个问题,还有其他表引用了book表的主键,我们就要更新所有的引用,这还牵涉到要删除外键,所有的这一切都是非常痛苦的。
如果使用有意义的值作为主键,那么我们将收到外界业务规则的影响,如果使用id,我们可以自己完全控制,而且如果象ISBN等一些东西改变的话,将不会影响到数据库结构。
如果你从一个新的数据库结构开始,可能会遵循约定,给所有的表都使用id作为主键,但是,当你使用的是一个既存的数据库开始的时候,Active Record提供了简单的方法来让你重新给表指定主键,例如:
class BadBook < ActiveRecord::Base
set_primary_key "isbn"
end
通常,Active
Record会注意给新创建的记录生成主键值-使用自增长的整数。不管怎样,当你override表的主键名字的时候,你就需要自己负责给新建记录一个唯
一的主键值。也许有些让人惊讶,你还是设置一个id的属性来完成这件事,因为Active
Record所关心的是,主键的设置永远都使用名为id属性,set_primary_key的声明只是设置了使用的列名,下面的例子,我们使用ISBN
作为主键。
book = BadBook.new
book.id = "0-12345-6789"
book.title = "My Great American Novel"
book.save
# ...
book = BadBook.find("0-12345-6789")
puts book.title # => "My Great American Novel"
p book.attributes #=> {"isbn" =>"0-12345-6789",
"title"=>"My Great American Novel"}
也就是说,在设置主键的时候,使用id属性,其他时候,使用真实的列名。
(六、连接数据库)
Active Record抽象了数据库连接的概念,帮助应用程序来处理底层的数据库链接的细节,作为替代,Active Record使用通用的调用,将细节委托给一组数据库适配器。
可以使用establish_connection( )方法来制定连接,下面的例子创建了一个mysql数据库连接,数据库的名字是railsdb,服务器的Host名为dbserver.com,用户名为railsuser,密码为railspw。
ActiveRecord::Base.establish_connection(
:adapter => "mysql",
:host => "dbserver.com",
:database => "railsdb",
:username => "railsuser",
:password => "railspw"
)
Active Record支持DB2,MySql,Oracle,Postgres,SqlServer,以及SqlLite,每一种数据库适配器在链接的参数上都有一些细小的差别,下表列出了常用的参数:
注意Oracle适配器的名字为oci。
数据库连接和Model类是关联的,每个类都从父类那里继承了链接,ActiveRecord::Base作为所有的Active
Record类的父类,设置这里的数据库连接就给所有的活动记录类设置了链接,当然,如果需要的话,你也可以复写(override)链接配置。
下面的例子里,我们的大多数表都在MySql数据库中,库名为online,由于一些历史原因,customers表在名为backend的数据库中,
ActiveRecord::Base.establish_connection(
:adapter => "mysql",
:host => "dbserver.com",
:database => "online",
:username => "groucho",
:password => "swordfish")
class LineItem < ActiveRecord::Base
# ...
end
class Order < ActiveRecord::Base
# ...
end
class Product < ActiveRecord::Base
# ...
end
class Customer < ActiveRecord::Base
# ...
end
Customer.establish_connection(
:adapter => "mysql",
:host => "dbserver.com",
:database => "backend",
:username => "chicho",
:password => "piano")
在我们前面所写的depot程序中,我们没有使用establish_connection方法,而是在
config/database.yaml文件中指定了数据库连接的参数信息,对于大多数rails程序来说,这是首选的方式,不仅因为将配置信息和代码
分离,而且在测试和部署时也能带来方便,上面的表格里列出的参数都可以应用在YAML文件中,这一点我们在前面的配置文件一节已经有介绍。
最后,如果你通过一个标记访问establish_connection(),Rails会在database.yaml文件中查找名字对应的配置节,来获取链接的参数,这样就可以将所有的数据库连接配置从代码中分离出来。
(七、创建记录)
Active Record使得实现CRUD的数据库基本操作变得简单,在下面的几节里我们使用Mysql数据库中的orders表来进行CRUD的操作,这次先看创建(Create)。
我们假想有一个Model,名为Order:
class Order < ActiveRecord::Base
end
在面向对象的模型里,表对应类,表中的行对应类的对象。我们可以通过创建一个类的对象来创建一条记录。对orders表,我们可以使用
Order.New()方法来创建一个Order的对象,也就对应了orders表的一条记录,然后我们给该对象的每个属性赋值,最后,我们调用对象的
save()方法将数据写回数据库,如果不调用save()的话,那么这个对象仅仅在内存中存在,而不是数据库。
an_order = Order.new
an_order.name = "Dave Thomas"
an_order.email = "dave@pragprog.com"
an_order.address = "
123 Main St
"
an_order.pay_type = "check"
an_order.save
Active Record的构造器有一个可选的块(block),这个块可以将创建的Order对象做为参数,这样就不需要再创建一个Order类的对象的变量了:
Order.new do |o|
o.name = "Dave Thomas"
# . . .
o.save
end
Active Record也可以接收一组哈希(Hash)参数的值来作为可选参数,由属性的名字和相对应的值组成:
an_order = Order.new(
:name => "Dave Thomas",
:email => "dave@pragprog.com",
:address => "
123 Main St
",
:pay_type => "check")
an_order.save
注意到现在为止,我们还没有任何关于id的设置,这是因为我们使用Active
Record的默认约定,将orders表的主键为一个integer类型的列。在存入数据库的时候,Active
Record自动给新建的对象生成一个唯一的值,并且设置到id属性上,我们可以在save()之后查询id的值:
an_order = Order.new
an_order.name = "Dave Thomas"
# ...
an_order.save
puts "The ID of this order is #{an_order.id}"
new()构造函数在内存中创建了一个Order类的对象,你需要在某个时候调用save()方法来保存到数据库。Active Record还有一个约定的方法create(),下面的例子说明这个方法的用法,同时展示了创建对象和存储到数据库:
an_order = Order.create(
:name => "Dave Thomas",
:email => "dave@pragprog.com",
:address => "
123 Main St
",
:pay_type => "check")
也可以给create()方法传递哈希(hash)的数组,在数据库中创建多条记录,并且返回对应的对象数组。
orders = Order.create(
[ { :name => "Dave Thomas",
:email => "dave@pragprog.com",
:address => "
123 Main St
",
:pay_type => "check"
},
{ :name => "Andy Hunt",
:email => "andy@pragprog.com",
:address => "
456 Gentle Drive
",
:pay_type => "po"
} ] )
方法new()和create()的真正目的就是让我们可以通过一组参数就能够创建Model对象:
order = Order.create(params)
(八、读取记录)
读取记录包括指定那些特定的数据是你感兴趣的,你给Active Record指定标准,Active Record再返回给你一些对象,其中包含了符合条件的记录的数据。
在一个表中检索数据的最简单的办法就是指定主键,任何一个Model都支持find()方法,该方法支持一个或多个主键值,如果只指定了一个
主键,将会返回对应的对象,如果指定了多个主键给find方法,该方法一组相应的对象。注意,当没有任何符合条件的数据的时候,将会抛出一个
RecordNotFound异常,所以如果find方法没有抛出这个异常的话,返回的数组中的对象个数就等于给find方法指定的id数目。
an_order = Order.find(27) # find the order with id == 27
# Get a list of order ids from a form, then
# sum the total value
order_list = params[:order_ids]
orders = Order.find(order_list)
count = orders.size
通常,在查询的时候都要用到除过id以外的值,Active Record提供了一组设置来执行这些查询,我们会介绍find使用方法,从基本的查询,再到高阶些的动态查询。
到现在我们只是了解了find方法的最基本的内容,通过指定id来获取一个或一组对象。另外,我们还可以使用一些标记比如:first,:all来作为find方法的参数。
:first将返回符合条件的第一条记录,:all将返回所有符合条件的记录,下一篇我们来看看Active Record是如何处理sql的。
注:find在3.1版中建议不使用,改用where。by--biyeah
(九、行数和再加载数据)
Active Record提供了两个方法来获取符合条件的记录的条数:count()和count_by_sql()。例如:
c1 = Order.count
c2 = Order.count(["name = ?", "Dave Thomas"])
c3 = LineItem.count_by_sql("select count(*) " +
" from line_items, orders " +
" where line_items.order_id = orders.id " +
" and orders.name = 'Dave Thomas' ")
puts "Dave has #{c3} line items in #{c2} orders (#{c1} orders in all)"
在一个程序中,数据库有可能被多个进程或多个程序访问,随时都有可能获取最新的Model对象,这些对象有可能刚刚被编辑过。
从某种程度上讲,这主要应用在事务中,不管怎么说,当你需要手动刷新Model对象时,Active Record可以帮助你,只需调用reload()方法,Model对象属性的值就会被数据库中的值更新。
stock = Market.find_by_ticker("RUBY")
loop do
puts "Price = #{stock.price}"
sleep 60
stock.reload
end
(十、更新记录)
前面了解了检索的方法,这次来看看Active Record怎样更新数据库中的记录。
如果你有一个Active Record对象(或许对应于order表),你可以通过调用save方法将它写道数据库中去,如果这个对象是先前从数据库中读取出来的,save方法将会更新既有的记录,否则将会新建一条记录。
如果一条既有记录被更新,Active Record将会用它的主键和来匹配内存中的对象,Active Record对象中的属性被更新到对应的列,即使一个列中的值没有变化也会被更新,在下面的例子中,id为123的订单所有的内容都会被更新:
order = Order.find(123)
order.name = "Fred"
order.save
不管怎样,在下面的例子里,Active Record对象只包含id,name,paytype,当对象被保存的时候仅仅只有这些字段被更新,注意如果你想要把对象保存到数据库,那么在使用find_by_sql方法时,一定要包含id字段。
orders = Order.find_by_sql("select id, name, pay_type from orders where id=123")
first = orders[0]
first.name = "Wilma"
first.save
另外,Active Record还提供了update_attribute()方法,该方法可以将Model对象的某个属性保存到数据库。
order = Order.find(123)
order.update_attribute(:name, "Barney")
order = Order.find(321)
order.update_attributes(:name => "Barney",
:email => "barney@bedrock.com")
我们可以把读取和更新结合在一起,使用update()方法或update_all(),update()方法使用一个id和一组属性,如果在数据库中对应的记录,就更新指定的属性,然后返回model对象。
order = Order.update(12, :name => "Barney", :email => "barney@bedrock.com")
也可以传递一组id或者属性和值的hash给update()方法,这样会更新所有匹配的记录,并且返回一组model对象。
最后,update_all()方法允许你指定给update语句指定Where条件,下面的例子给所有标题中含有java的商品涨价10%:
result = Product.update_all("price = 1.1*price", "title like '%Java%'")
这里的返回值依赖于具体的数据库适配器,很多数据库都返回被更新的记录数目。
下面我们看看save()和save!()这两个方法。
简单的save()方法在Model对象存在并且可以的保存的情况下返回true:
if order.save
# all OK
else
# validation failed
end
这样会导致你在所有调用save方法的地方都要加上检查,但是Active Record假定save方法是在Controler的Action的上下文中的,并且视图里的代码不进行这些检查。(这部分书上看不明白,不能确定)。
不管怎样,如果你需要在上下文环境中保存Model对象,并且想确定是否所有的错误都被处理了,你可以使用save!()方法,如果Model对象不能保存,那么这个方法会抛出一个RecordInvailid异常:
begin
order.save!
rescue RecordInvalid => error
# validation failed
end