有些数据库支持boolean列类型,而有些不支持。这使得Active Record很难抽象化boolean。例如,如果基础数据库没有boolean类型,一些开发者使用char(1)列包含”t”或”f”来表示true或false。而其它开发者使用整数列,0是false,1是true。即使数据库直接支持boolean类型(如MySQL和它的bool列类型),它们也只是在内部存储0或1。
问题在Ruby中数字0和字符”f”两者在条件句中被解释为true值。[Ruby有个true的简单定义。不是nil或常量false的所有值都是true。]这意味着如果你直接使用列的值,你的代码在你认为它是false的时候,将其解释列为true.。
# DON'T DO THIS
user = Users.find_by_name("Dave")
if user.superuser
grant_privileges
end
要在一个条件句内查询一个列,你必须附加一个问号给列的名字。(译注:在Ruby中称这为?句。)
# INSTEAD, DO THIS
user = Users.find_by_name("Dave")
if user.superuser?
grant_privileges
end
这种形式的存取器查看列的值。只有在数字零的情况下,它才被解释为fals;一个字符串” 0”,”f”,”false”,或者””(空字符串);nil;或者常量false。否则它被解释为true。
如果你用先前的schema或者不是英语的数据库工作,在先前段落中定义的true,可能会不可靠。这些情况下,你可以覆写内置predicate方法的定义。例如,在Dutch语言中,字段可能包含J或N(Ja 或Nee)。在这个例子,你可以写:
class User < ActiveRecord::Base
def superuser?
self.superuser == 'J'
end
# . . .
end
Storing Structured Data
有时候这样做很方便,存储包含任意Ruby对象的属性直接到数据库的表中。Active Record通过序列化Ruby对象到一个字符串中(使用YAML格式)来支持这种方式,并且存储这个字符串到数据库内对应此属性的列中。在schema中,这个列必须被定义为text类型。
因为Active Record通常将映射一个character或text列为一个纯Ruby字符串,在你想获得这个功能的优势时,你需要告诉Active Record使用序列化。例如,我们想记录最后五位购买者做为我们客户。我们将创建一个包含一个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要marshal对象到这个列中。
class Purchase < ActiveRecord::Base
serialize :last_five
# ...
end
当我们创建新的Purchase对象时,我们可以给last_five列赋值任何Ruby对象。在这个例子中,我们设置它为一个字符串数组。
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应用程序的外部来使用序列化内的信息还有些问题。除非应用程序理解YAML格式,因为列内容对它是不透明的。事实上,很难在SQL查询的内部使用结构(structure)。相反,你可能考虑使用对象聚合来代替,它描述在247页15.2节,来达到同样的效果。
14.3 主键与ID
你可能已经注意到了我们的例子数据库内的所有表都定义有一个整数列叫id,做为表的主键。这是Active Record的约定。
“请等等!”你喊道。“我的order表的主键应该是定单号或一些意义的列?为什么使用一个没有意义的主键,比如id?”
原因是实践—外部数据的格式可能随着时间而更改。例如,你可能想一本书的ISBN应该是books表的最好主键。毕竟,ISBN是唯一的。但是像现在写的这本书,美国的发行行业做了修改,给所有的ISBN一个额外的数字。
如果我们已使用ISBN做books表的主键,我们必须更改每一行以反映这种改变。但是会有另一个问题。数据库内所有的通过books表的主键引用它的其它表呢。我们不能修改books表内这些键,除非我们先遍历它,然后更新所有这些引用。而且这还包括使用的外键约束,更新表,更新books表,最后重建约束。所有这些,是多大的痛苦。
如果我们使用我们自己的内部值做为主键,则事情会工作的更好。没有第三方跟着,也不会武断的要求我们修改什么—我们控制我们自己的键空间。如果有些事如ISBN需要修改,它的修改不会影响到数据库内任何其它现有的关联。实际小,我们已经将这些行内数据的外部表示与行的关系减到了最小。
现在,没有什么理由说我们不能给我们的终用户使用id值了。在order表内,我们可以称它为一定单的id,并在所有工作簿上打印它。但是做这些事要小心—在任何时候一些调整可能发生并要求定单的id必须遵循一个外部施加的格式,并且你要返回你开始的地方。
如果你给一个Rails应用程序创建了一个新schema,你或许想与工作流配合,并给你的所有表一个id列做为它们主键。[我们稍后会看到,join表不包含这一点—它们不应该有一个id列。]如果你需要用一个现有的schema工作,Active Record给你一个简单的途径来为一个表重写主键的缺省名字。
class BadBook < ActiveRecord::Base
set_primary_key "isbn"
end
通常,Active Record接受创建的新主键值给你创建的记录并添加到数据库中—它们将升序的整数(或许是有一定间隔的序列)。但是,如果你重写主键列的名字,你也要接受一个职责,在你保存新行之前,你要设置主键一个唯一值。或许令人惊讶的是,你还设置一个属性叫id来做这些。直到Active Record被关心,主键属性总是被设置为一个叫id的属性。set_primary_key声明设置用于表的列名字。下面例子代码中,我们使用一个叫id的属性,即使数据库内的主键是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"}
刚才做的事情有些乱,model对象的属性有个isbn列和title列—id没有出现。当我们需要设置主键时,使用id。在所有其它时候,使用实际的列名。
14.4 连接数据库
Active Record把数据库连接的概念抽象出来,有助于程序处理各种特殊数据库的底层细节。相反,Active Record应用程序使用通常的调用,代理了一组数据库适配器的细节。(This abstraction breaks down slightly when code starts to make SQL-based queries, as we’ll see later.)
指定连接的一种方式是使用establish_connection()类方法。[在Rails应用程序中,另一种特殊的连接方式,我们在178页讨论。]举例来说,下面调用在服务器dbserver.com上使用给定的用户名字和口令,创建了一个对名为railsdb的MySQL数据库的连接。这是所有的model类共用的缺省连接。
ActiveRecord::Base.establish_connection(
:adapter => "mysql",
:host => "dbserver.com",
:database => "railsdb",
:username => "railsuser",
:password => "railspw"
)
Active Record支持DB2,MySQL,Oracle,Postgres,SqlServer和SQLite数据库。每个适配器都接受有些不同的连接参数,像图14.2显示的。注意:Oracle适配器被称为oci。
连接和model类相关,每个类都从父类中继承了它的父类的连接。ActiveRecord::base是所有Active Record类的基类,对于你定义的所有Active Record类设置一个缺省的连接。但是,在你需要时,你可以覆写它。
下面的例子,我们大多数应用程序的表基本上是在MySQL中的online数据库。但由于某种原因,customer表在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.yml文件内指定连接参数。对大多数Rails应用程序来说,这会很好地工作。这并不只是在代码外部保持了所有连接信息,它对Rails测试和开也会很好地工作。图14.2中的所有参数也可用在YAML文件中。它的更多细节在178页的13.3节中。
最后,你可以结合这两种方式。如果你传递一个符号给establish_connection(),Rails查看database.yml文件内段的名字,并以找到参数进行连接。这样你可以在你的代码外面保持所有的连接细节。
14.5 CRUD—Create, Read, Update, Delete
Active Record对完成数据库表四个基本的操作变得很容易:create,read,update,delete。
这一章,我们将操作MySQL数据库中的orders表。下面例子假设我们有一个针对这个表的基本Active Record model。
class Order < ActiveRecord::Base
end
创建新行
在对象-关系的范式中,表被表现为类,并且表内的行被对应于那个类的对象。因此我们通过创建相应类的对象来创建表中的行,是顺理成章的事。我们可以通过调用Order.new()来创建一个对象,表示orders表中的行。然后我们可以填充各种属性的值(对应表的各个列)。最后,我们调用对象的save()方法存储回order到数据库中。没有这个方法调用的话,order只会存在于本机的内存中。
an_order = Order.new
an_order.name = "Dave Thomas"
an_order.email = "dave@pragprog.com"
an_order.address = "
an_order.pay_type = "check"
an_order.save
Active Record的构造函数可以有一个可选的块参数。如果出现的话,这个块会将新创建的order作为自己的一个参数被调用。这种用法很有好处,如果你想创建并保存一个order,又不想创建一个临时变量时,就会用到。
Order.new do |o|
o.name = "Dave Thomas"
# . . .
o.save
end
最后,Active Record还有一种形式的构造函数,它接受一个属性值的哈希表做为一个可选参数。这个哈希表内每个条目都对应属性集中的每个名字和值。在本书稍后我们会看到,这种用法在把HTML表格中的值存储到数据库行中时非常有用的。
an_order = Order.new(
:name => "Dave Thomas",
:email => "dave@pragprog.com",
:address => "
:pay_type => "check")
an_order.save
注意上面的所有代码中,我们都没有对新行设置id这个属性值。因为我们使用Active Record默认的integer列作为主键。Active Record自动创建一个唯一值,并且只在存储该行之前设置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对象。我们必须记得在某个时间点上要存储它。Active Record还有一个方便的方法create()。它是既实例化model对象又把它存储到数据库中。
an_order = Order.create(
:name => "Dave Thomas",
:email => "dave@pragprog.com",
123 Main St ", 123 Main St "