Agile Web Development with Rails 翻译(三十二)
belongs_to() 声明
belongs_to()声明给定的类是本身类的父类。尽管belongs_to可能不是我们考虑这种关系时最先想到的词,但是Active Record的约定是包含外键的表属于它引用的表。如果这有助于编码的话,你应该把references当成belongs_to使用。
我们假定父类的名字是属性名大小写混合的单数形式,外键字段是附加_id的属性名的单数形式。所以,给出下面代码
class LineItem < ActiveRecord::Base
belongs_to :product
belongs_to :invoice_item
end
Active Record把商品项目和类Product,InvoiceItem相关联。(特别注意:其中的单复数形式的变化。)其中隐含的意思是,它使用了外键product_id,invoice_item_id来引用表products,invoice_items的id列。
还可以覆写这些,其它人会假设传给belongs_to()一个选项的哈希表散列值改变这些假定。
class LineItem < ActiveRecord::Base
belongs_to :paid_order,
:class_name => "Order",
:foreign_key => "order_id",
:conditions => "paid_on is not null"
end
这个例子中,我们创建了一个paid_order的关联,它是类Order的引用(对应的表orders)。这连接是通过order_id外键建立的,但是它还是有条件限制的,如果目标行的paid_on列不为null,它将找到一个定单。这个例子中,我们并没有使用对line_items表内的单个列直接映射来建立连接。
belongs_to()方法创建许多管理连接的实例方法。这些方法都是以连接名开头来命名,例如:
item = LineItem.find(2)
# item.product is the associated Product object
puts "Current product is #{item.product.id}"
puts item.product.title
item.product = Product.new(:title => "Advanced Rails",
:description => "...",
:image_url => "http://....jpg",
:price => 34.95,
:date_available => Time.now)
item.save!
puts "New product is #{item.product.id}"
puts item.product.title
如果我们运行它(用一个适当的数据连接),可能会有以下的结果。
Current product is 2
Programming Ruby
New product is 37
Advanced Rails
我们在LIneItem类中使用方法product(),product!=()来存取和更新与product对象关联的商品项目对象。在背后,Active Record是和数据库的机制是一样的。当我们保存对应的商品项目时,它自动保存创建的新product。并把新的product的id号和商品项目连接。
belongs_to()会把方法添加到使用它的类中。这个描述是基于以下的假定,已定义的LineItem类属于类Product。
class LineItem < ActiveRecord::Base
belongs_to :product
end
在这个例子中,下面方法将被定义用于商品项目,以及它们属于的products。
1、product(force_reload=false) 返回关联的产品(如果没有关联的产品存在,则返回nil)。结果被缓存,如果这个定单先前被取出过,则数据库不会再次查询,除非将true做为一个参数传递给它。
2、product=(obj) 用给出的产品关联这个商品项目,在这个商品项目内设置外键给产品的主键。如果产品没有被保存,在商品项目被保存时它将被保存,键将在那时被连接。
3、build_product(attributes={}) 构造一个新的product对象,并用给定的属性初始化它。这个商品项目将被连接给它。Product也不会被保存。
4、create_product(attributes={}) 构建一个新的product对象,连接这个商品项目给它,然后保存这个product。
has_one() 声明
has_one声明一个给定的类(缺省是混合大小写的属性名字的单数形式)是这个类的子类。has_one声明和belongs_to一样定义同一个方法集,因此给定类定义例如:
class Order < ActiveRecord::Base
has_one :invoice
end
我们可写成
order = Order.new
invoice = Invoice.new
if invoice.save
order.invoice = invoice
end
你可以通过给has_one传递一个选项的哈希表,来改变Active Record的默认行为。另外对于:class_name,:foreign_key,和:conditions选项,我们可以看到belongs_to(),我们也可以使用:dependent和: order。
:dependent选项是指子表中的记录行不能独立于对应的父表记录行而单独存在。这就意味着如果你删除了父类的记录,而且你定义了:dependent=>true的话,Active Record将会自动删除子表中相关的记录行。
: order选项,是决定记录返回之前怎样排序。这似乎有点奇怪。我们在277页的has_many中还会讨论。
一对多关系
一对多关系允许你描述一个对象集合。例如,一个定单可能有多个关联的商品项目。在数据库中,对于一特定定单的所有商品项目行包含一个外键列来引用这个定单。
在Active Record中,父对象(逻辑上是包含子对象的一个集合)使用has_many来声明对子表的关系,子表用belongs_to来表明它的父类。在我们的例子中,类LineItembelongs_to :order和orders表has_many :line_items。
我们已经在前面说了belongs_to()关系声明。它和在一对一关系的处理上是一样的。只是has_many声明添加了一些功能给它的model。
has_many() 声明
has_many定义了一个属性,它的行为就像子对象集合一样。你可以把子对象当做一个数据来访问,查找特定的子对象,并可添加新子对象。例如,下面代码添加一些商品项目给一个定单。
order = Order.new
params[:products_to_buy].each do |prd_id, qty|
product = Product.find(prd_id)
order.line_items << LineItem.new(:product => product,:quantity => qty)
end
附加操作符(<<)不仅仅是把一个对象附加到order列表中去。它还通过设置它们的外键给这个定单的id 来将商品项目连接回到这个定单中,并且当父类定单被保存时,商品项目也会自动保存。
我们可以迭代具有has_many关系的子类--属性的行为像个数组。
order = Order.find(123)
total = 0.0
order.line_items.each do |li|
total += li.quantity * li.unit_price
end
和has_one一样,你也可以提供给has_many一组哈希表选项来改变Active Record的默认行为。选项:class_name,:foreign_key,:conditions,:class_name,:order和:dependent工作与在has_one内是一样的。has_many还有:exclusively_dependent,:finder_sql,和:counter_sql。我们也讨论:order选项,我们只是列出它,但是在has_one中讨论它。
has_one和has_many都支持:dependent选项。当你要销毁父表中的一个记录行时,:dependent就会告诉Rails销毁子表中的记录行。它通过遍历子表,在带有外键信息的每一个记录行上调用destroy(),并删除父表中的记录。
然而,如果子表只是被父表用到(也就是说,没有其他的依赖关系),并且它没有完成任何删除操作这样的钩子方法,你可以使用:exclusively_dependent来代替:dependent选项。如果这个选项被设置了,子表的记录行就要在一个SQL语句中来执行删除。
最后,你可以覆写Active Record用来获取和计数子表记录行的SQL,这要通过设置:finder_sql,:counter_sql两个选项来做到。在我们碰到where子句中使用:condition不够用时,这两个选项是用的上了。例如,你可以为一个特定产品创建所有商品项目的一个集合。
class Order < ActiveRecord::Base
has_many :rails_line_items,
:class_name => "LineItem",
:finder_sql => "select l.* from line_items l, products p " +
" where l.product_id = p.id " +
" and p.title like \'%rails%\'"
end
:counter_sql选项被用来覆盖Active Record中用来计数的查询语句。如果:finder_sql被指定而且:counter_sql没有被指定,Activer Record是使用select count(*)来替换finder SQL语句中的select部分,来合成counter的sql语句。
: order选项是指定一个SQL片断,它将为从数据库中加载的行定义一个次序后加入到集合中。如果你需要一种特殊顺序来遍历集合,你就要指定: order选项。你给出的SQL片断是简单的文本,它将出现在一个select语句的order by子句之后。它是由包含了一个或多个列名字组成的列表。这个集合将按列表中的第一个列排序。如果两个记录行有相同的列值,决定它们次序的是列表中的第二个列名,以此类推。默认的排序是升序,可以使用DESC来降序排序。
下面代码可以被用于指定一个次序的商品项目,它以数量次序被排序(最小的数量在前面)。
class Order < ActiveRecord::Base
has_many :line_items,
:order => "quantity, unit_price DESC"
end
如果两个商品项目有同样的数量,那么单价最高的被放到前面。
回过头想想,我们讨论过的has_one是,我们也提到它也支持一个: order选项。它看起来有点奇怪-如果一个父类只和一个子类关联,那当获取那个子类时,指定的是哪个order呢?
Active Record 可以不在现在的基础数据库上创建has_one关系。例如,一个customer可能有很多orders:这是一个has_many关系。但是customer将只有一个最近的order。我们可以使用has_one和: order的结合来表示这个关系:
class Customer < ActiveRecord::Base
has_many :orders
has_one :most_recent_order,
:class_name => \'Order\',
:order => \'created_at DESC\'
end
这段代码在customer的model中创建一个新的属性,most_recent_order。它将指向最近created_at时间戳的order。我们可以用这个属性来查找customer的最近order。
cust = Customer.find_by_name("Dave Thomas")
puts "Dave last ordered on #{cust.most_recent_order.created_at}"
这里的代码能起作用,是因为Active Record实际上是使用这样的SQL语句来获取有has_one关联的数据。
SELECT * FROM orders
WHERE customer_id = ?
ORDER BY created_at DESC
LIMIT 1
limit子句意思是只返回一个记录行,以符合has_one声明。order by子句确保记录行是最近的记录。