数据库列有个带限制的类型集: integers, strings, dates, 等等。典型地,我们的应
用程序却有很多类型—我们用类定义来表现我们代码的抽象。更好的事是,如果我们能够映
射数据库内的一些列信息到我们的高级抽象中—我们以同样方式包装行数据本身在“模型”
对象中。
例如,customer 表数据可能包括用于存储消费者姓名的列—或许是姓,名,称呼。在我
们的程序内,我们想包装这些与名字有关的列到一个单独的Name 对象中;三个列被映射到一
个单独的Ruby 对象中,与其它所的customer 表字段一同包含在消费者“模型”中。而且,在我们回头写“模型”时,我们希望从Name 对象抽取数据并放回到数据库内相应的三个列中。这种功能被称为聚合(aggregation)(尽管有人称它为合成(composition)—这依赖于你是从上往下,还是从下往上看它。)并不奇怪,“活动记录”做到这些很容易。你定义一类来持有数据,并且你添加一个声明给“模型”类来告诉它映射数据库的列给持有数据类的对象,当然也可以从这个对象获取列数据。
持有合成数据的类(是例子中的Name 类)必须遵循两个标准。
首先,它必须有一个构造函数,它将接受数据库内列的数据,一个参数对应一个列。
其次,它必须提供能返回这个值的属性,一个属性对应一个列。在内部,它可以存储它需要使用的任何形式的数据,只要它能双向映射列数据。
对于我们name 例子,我们将定义一个简单的类,它持有三个合成部分做为实例变量。我
们也定义to_s()方法来格式化完整的名字为一个字符串。
class Name
attr_reader :first, :initials, :last
def initialize(first, initials, last)
@first = first
@initials = initials
@last = last
end
def to_s
[ @first, @initials, @last ].compact.join(" ")
end
end
现在们必须告诉我们的Customer“模型”类,有三个数据库的列first_name, initials,
和last_name 应该被映射到Name 对象中。我们用composed_of 声明来完成。
尽管composed_of 可以只用一个参数来调用,但首先描述完整格式的声明并且显示各种
字段如何被缺省也很容易。
composed_of :attr_name,
:class_name => SomeClass,
:mapping => mapping
attr_name 参数指定将要给到“模型”类内的合成属性的名字。如果我们定义我们的
customer 为
class Customer < ActiveRecord::Base
composed_of :name, ...
end
我们就可以使用customer 对象的name 属性来访问合成对象。
customer = Customer.find(123)
puts customer.name.first
:class_name 选项用于指定持有合成数据的类的名字。选项的值可以是个类常量,或者
是包含类名字的字符串或符号。在我们例子,类是Name,所以我们可以指定
class Customer < ActiveRecord::Base
composed_of :name, :class_name => Name, ...
end
如果类名字是简单的属性名字的大小写混合形式,它可以被忽略。
:mapping 参数告诉“活动记录”表内列如何映射到合成对象内的属性和构造函数的参数
的。传递给:mapping 的参数即可以是一个有两个元素的数组,也可以是两个元素数组的一个数组。第二个元素是相应于合成属性内的存取器的名字。出现在映射参数定义内的元素的次
序,由那个数据库内被做为参数传递给合成对象的initialize()方法的列内容的次序决定。
图15.2 显示了映射是如何工作的。如果这个选项被忽略,“活动记录”假设数据库的列和合
成对象属性两者与“模型”属性有同样名字。
对于我们Name 类,我们需要映射三个数据库的列到合成对象内。customers 表定义是这
样。
create table customers (
id int not null auto_increment,
created_at datetime not null,
credit_limit decimal(10,2) default 100.0,
first_name varchar(50),
initials varchar(20),
last_name varchar(50),
last_purchase datetime,
purchase_count int default 0,
primary key (id)
);
列first_name, initials, 和last_name 应该在Name 类中被映射为first,
initials, 和last 属性。[在真实的应用程序中,我们更愿意让属性的名字与列的名字一样。
此处使用不同的名字是帮助我们显示参数是如何被映射的。]要把这个指定给“活动记录”,
我们使用下面声明。
class Customer < ActiveRecord::Base
composed_of :name,
:class_name => Name,
:mapping =>
[ # database ruby
[ :first_name, :first ],
[ :initials, :initials ],
[ :last_name, :last ]
]
end
尽管我们花了很多时间描述选项,但事实上它对创建这些映射的影响很小。一旦我们完
成了,就可以很容易地使用它们:“模型”对象内的合成属性将是你定义的合成类的一个实
例。
name = Name.new("Dwight", "D", "Eisenhower")
Customer.create(:credit_limit => 1000, :name => name)
customer = Customer.find(:first)
puts customer.name.first #=> Dwight
puts customer.name.last #=> Eisenhower
puts customer.name.to_s #=> Dwight D Eisenhower
customer.name = Name.new("Harry", nil, "Truman")
这个代码在customers 表内,用由新Name 对象内的属性firt,initials,和last 初始化
的列first_name,initials,和last_name 列创建了一个新行。它从数据库获得此行数据并通过合成对象来访问这些字段。注意你不能在合成对象内修改这些字段。相反你必须将它们传递给一个新对象。
合成对象并不是必须要映射数据库内的多个列到一个单独的对象内;它在用于接受一个
单独列并映射它为不是integer,float,string,或date 和time 的其它类型时是很有用处
的。通常的例子是表示货币的一个数据库列:不能持有负的浮点数据,你可能想创建特定的
Money 对象,它带有你应用程序需要的属性(如四舍五入为)。
回到196 页,我们看看我们是如何使用序列化声明来在数据库内存储结构化数据的。我
们也可以使用composed_of 声明来做到这些。做为对使用YAML 来序列化数据到数据库的列中的代替,我们使用一个合成对象来完成它自己序列化。做为一个例子,让我们再看看通过一
个customer 来存储最后五位购买者序列化的例子。先前,我们持有使用Ruby 数组的列表,并初始化它为一个YAML 字符串后再存入数据库。现在让我们包装信息到一个对象中,并且这个对象以它自己的格式存储数据。在这个例子中,我们将保存产品列表为一个用逗号分隔值的正常字符串。
首先,我们创建类LastFive 来包装列表。因为数据库在一个简单的字符串内存储列表,
所以它的构造函数将也接受字符串,并且我们需要一个能在字符串内返回内容的属性。尽管
在内部我们以Ruby 数组来存储列表。
class LastFive
attr_reader :list
# 接受包含"a,b,c" 样的字符串并存储成[ 'a', 'b', 'c' ]
def initialize(list_as_string)
@list = list_as_string.split(/,/)
end
# 返回我们的内容在以逗号分隔形式的字符串中。
def last_five
@list.join(',')
end
end
我们可以声明我们的LastFive 类包装了数据库内的last_five 列。
class Purchase < ActiveRecord::Base
composed_of :last_five
end
在我们运行它时,我们可以看到last_five 属性包含一个值的数组。
Purchase.create(:last_five => LastFive.new("3,4,5"))
purchase = Purchase.find(:first)
puts purchase.last_five.list[1] #=> 4
用程序却有很多类型—我们用类定义来表现我们代码的抽象。更好的事是,如果我们能够映
射数据库内的一些列信息到我们的高级抽象中—我们以同样方式包装行数据本身在“模型”
对象中。
例如,customer 表数据可能包括用于存储消费者姓名的列—或许是姓,名,称呼。在我
们的程序内,我们想包装这些与名字有关的列到一个单独的Name 对象中;三个列被映射到一
个单独的Ruby 对象中,与其它所的customer 表字段一同包含在消费者“模型”中。而且,在我们回头写“模型”时,我们希望从Name 对象抽取数据并放回到数据库内相应的三个列中。这种功能被称为聚合(aggregation)(尽管有人称它为合成(composition)—这依赖于你是从上往下,还是从下往上看它。)并不奇怪,“活动记录”做到这些很容易。你定义一类来持有数据,并且你添加一个声明给“模型”类来告诉它映射数据库的列给持有数据类的对象,当然也可以从这个对象获取列数据。
持有合成数据的类(是例子中的Name 类)必须遵循两个标准。
首先,它必须有一个构造函数,它将接受数据库内列的数据,一个参数对应一个列。
其次,它必须提供能返回这个值的属性,一个属性对应一个列。在内部,它可以存储它需要使用的任何形式的数据,只要它能双向映射列数据。
对于我们name 例子,我们将定义一个简单的类,它持有三个合成部分做为实例变量。我
们也定义to_s()方法来格式化完整的名字为一个字符串。
class Name
attr_reader :first, :initials, :last
def initialize(first, initials, last)
@first = first
@initials = initials
@last = last
end
def to_s
[ @first, @initials, @last ].compact.join(" ")
end
end
现在们必须告诉我们的Customer“模型”类,有三个数据库的列first_name, initials,
和last_name 应该被映射到Name 对象中。我们用composed_of 声明来完成。
尽管composed_of 可以只用一个参数来调用,但首先描述完整格式的声明并且显示各种
字段如何被缺省也很容易。
composed_of :attr_name,
:class_name => SomeClass,
:mapping => mapping
attr_name 参数指定将要给到“模型”类内的合成属性的名字。如果我们定义我们的
customer 为
class Customer < ActiveRecord::Base
composed_of :name, ...
end
我们就可以使用customer 对象的name 属性来访问合成对象。
customer = Customer.find(123)
puts customer.name.first
:class_name 选项用于指定持有合成数据的类的名字。选项的值可以是个类常量,或者
是包含类名字的字符串或符号。在我们例子,类是Name,所以我们可以指定
class Customer < ActiveRecord::Base
composed_of :name, :class_name => Name, ...
end
如果类名字是简单的属性名字的大小写混合形式,它可以被忽略。
:mapping 参数告诉“活动记录”表内列如何映射到合成对象内的属性和构造函数的参数
的。传递给:mapping 的参数即可以是一个有两个元素的数组,也可以是两个元素数组的一个数组。第二个元素是相应于合成属性内的存取器的名字。出现在映射参数定义内的元素的次
序,由那个数据库内被做为参数传递给合成对象的initialize()方法的列内容的次序决定。
图15.2 显示了映射是如何工作的。如果这个选项被忽略,“活动记录”假设数据库的列和合
成对象属性两者与“模型”属性有同样名字。
对于我们Name 类,我们需要映射三个数据库的列到合成对象内。customers 表定义是这
样。
create table customers (
id int not null auto_increment,
created_at datetime not null,
credit_limit decimal(10,2) default 100.0,
first_name varchar(50),
initials varchar(20),
last_name varchar(50),
last_purchase datetime,
purchase_count int default 0,
primary key (id)
);
列first_name, initials, 和last_name 应该在Name 类中被映射为first,
initials, 和last 属性。[在真实的应用程序中,我们更愿意让属性的名字与列的名字一样。
此处使用不同的名字是帮助我们显示参数是如何被映射的。]要把这个指定给“活动记录”,
我们使用下面声明。
class Customer < ActiveRecord::Base
composed_of :name,
:class_name => Name,
:mapping =>
[ # database ruby
[ :first_name, :first ],
[ :initials, :initials ],
[ :last_name, :last ]
]
end
尽管我们花了很多时间描述选项,但事实上它对创建这些映射的影响很小。一旦我们完
成了,就可以很容易地使用它们:“模型”对象内的合成属性将是你定义的合成类的一个实
例。
name = Name.new("Dwight", "D", "Eisenhower")
Customer.create(:credit_limit => 1000, :name => name)
customer = Customer.find(:first)
puts customer.name.first #=> Dwight
puts customer.name.last #=> Eisenhower
puts customer.name.to_s #=> Dwight D Eisenhower
customer.name = Name.new("Harry", nil, "Truman")
这个代码在customers 表内,用由新Name 对象内的属性firt,initials,和last 初始化
的列first_name,initials,和last_name 列创建了一个新行。它从数据库获得此行数据并通过合成对象来访问这些字段。注意你不能在合成对象内修改这些字段。相反你必须将它们传递给一个新对象。
合成对象并不是必须要映射数据库内的多个列到一个单独的对象内;它在用于接受一个
单独列并映射它为不是integer,float,string,或date 和time 的其它类型时是很有用处
的。通常的例子是表示货币的一个数据库列:不能持有负的浮点数据,你可能想创建特定的
Money 对象,它带有你应用程序需要的属性(如四舍五入为)。
回到196 页,我们看看我们是如何使用序列化声明来在数据库内存储结构化数据的。我
们也可以使用composed_of 声明来做到这些。做为对使用YAML 来序列化数据到数据库的列中的代替,我们使用一个合成对象来完成它自己序列化。做为一个例子,让我们再看看通过一
个customer 来存储最后五位购买者序列化的例子。先前,我们持有使用Ruby 数组的列表,并初始化它为一个YAML 字符串后再存入数据库。现在让我们包装信息到一个对象中,并且这个对象以它自己的格式存储数据。在这个例子中,我们将保存产品列表为一个用逗号分隔值的正常字符串。
首先,我们创建类LastFive 来包装列表。因为数据库在一个简单的字符串内存储列表,
所以它的构造函数将也接受字符串,并且我们需要一个能在字符串内返回内容的属性。尽管
在内部我们以Ruby 数组来存储列表。
class LastFive
attr_reader :list
# 接受包含"a,b,c" 样的字符串并存储成[ 'a', 'b', 'c' ]
def initialize(list_as_string)
@list = list_as_string.split(/,/)
end
# 返回我们的内容在以逗号分隔形式的字符串中。
def last_five
@list.join(',')
end
end
我们可以声明我们的LastFive 类包装了数据库内的last_five 列。
class Purchase < ActiveRecord::Base
composed_of :last_five
end
在我们运行它时,我们可以看到last_five 属性包含一个值的数组。
Purchase.create(:last_five => LastFive.new("3,4,5"))
purchase = Purchase.find(:first)
puts purchase.last_five.list[1] #=> 4