`
biyeah
  • 浏览: 200496 次
  • 来自: ...
社区版块
存档分类
最新评论

[转]ActiveRecord教程系列2

 
阅读更多
出处:http://syue.com/Software/Language/Ruby/740.html
(十一、并发处理)
在学习Rails中的并发处理的处理前,我们先简单了解下并发处理的概念。
  在有多个处理同时访问同一个数据库的应用程序中,可能会出现这样的情况,因为一个处理更新了数据库中的行,而使得另一个处理中持有的数据变得陈旧了。例如,A和B先后从数据库中提取了相同的数据,并都做了修改,这时B先将自己的修改更新会数据库,稍后,A将自己的修改更新回数据库,这时将会覆盖B所作的修改,当B再次提取数据库后,看到的是A修改的结果,而不是自己的。
  一个解决办法就是将更新的表或者行进行锁定,防止其他程序进行更新或者访问,锁定可以完全避免并发的问题,也常称作悲观锁,但是在Web项目中,这个做法是行不通的,因为在同一个时间点上,可能会有很多个用户需要访问数据。
  乐观锁并没有将数据锁定的外在表现,作为替代,在将修改写回到数据库之前,进行检查来确定一条记录是否已经被修改了。在Rails里的实现方法是,每和行都包含有一个版本号(version number),不管什么时候行被更新了,版本号就会被增加,当你在你的程序中进行一个更新操作时,Active Record检查行和Model的版本号,如果不匹配,将会放弃修改并抛出一个异常。
  对于任何一个包含有integer型的字段lock_version的表,乐观锁都会默认的被启用,你可以把新行的这个字段初始为0,但是,如果你忘记了,Active Record也会替你完成。
  我们来看看怎样实现乐观锁,我们创建一个名为counters的表:
create table counters (
id int not null auto_increment,
count int default 0,
lock_version int default 0,
primary key (id)
);
  然后我给这个表创建一个行,将这个行读入到不同的Model中,并且在其中尝试更新,
class Counter < ActiveRecord::Base
end
Counter.delete_all
Counter.create(:count => 0)
count1 = Counter.find(:first)
count2 = Counter.find(:first)
count1.count += 3
count1.save
count2.count += 4
count2.save
  当我们运行上面的代码,会得到一个异常,Rails会放弃count2所作的修改,因为其中包含的已经是脏数据了。
  如果使用了乐观锁,就需要在程序中处理这些异常,也可以禁用乐观锁:
  ActiveRecord::Base.lock_optimistically = false
  关于乐观锁,可以参考Martin Flower所著的《企业应用解决方案及模式》一种中关于并发处理和工作单元(Work Unit)模式的内容,相信对Rails的并发处理机制会有更深的了解。

(十二、删除记录)
Active Record提供了两种方式进行删除操作。首先,有两个类级别的方法,delete和delete_all,这两个操作处在数据库层面上,delete()方法接收一个或一组和数据库对应的id,delete_all()方法删除所有符合指定条件的记录,如果没有指定条件,就会删除所有的记录。方法的返回值和具体的数据库适配器相关,例如oracle返回被影响的行数。如果没有记录被删除,也不会抛出异常。
Order.delete(123)
User.delete([2,3,4,5])
Product.delete_all(["price > ?", @expensive_price])
  另外,destory方法删除和数据库中行相对应的Model对象,这样会冻结这些对象,并且不能修改对象的值。
order = Order.find_by_name("Dave")
order.destroy
  有两个类级别的destory方法,destory()方法接收一个或一组id,destory_all()方法接收删除条件。这两个方法都从数据库中读出对应的记录到Model对象,并且对这个对象调用实例级别的destory()方法,而且不返回有意义的信息。
Order.destroy_all(["shipped_at < ?", 30.days.ago])
  为什么我们同时需要delete和destory方法呢?delete方法绕过了一些Active Record的回调(callback)和验证函数,而使用destory则不会,通常我们使用destory方法来确保我们的数据库是一致的,并且不会破坏Model中所包含的业务逻辑。

(十三、表关联)
很多程序使用的数据库都包含有多个表,而且通常一些表之间还有关联关系,订单常含有多个条目,而一个条目又关联到一种商品,一个商品可能又属于多个商品分类,一个商品分类里又包含有多个不同的商品。
  在数据库中,这些关联表现为使用主键值把表关联起来,也就是外键,但是这属于底层的范畴,我们需要处理Model对象间的关联,而不是数据库中的列和键。如果一个订单含有多个条目,我们需要有办法来维持,处理它们的关系,如果一个条目引用到一种商品,我们或许想这样做:
price = line_item.product.price
  而不愿像下面这样麻烦:
product_id = line_item.product_id
product = Product.find(product_id)
price = product.price
  Active Record可以帮助我们,作为ORM的一部分,Active Record将低级别的数据库中的外键转换成高级别的对象间的映射。处理了三种基本情况:
  A表中的一条记录和B表的零条或一条记录相关联。
  A表中的一条记录和B表的任意多条记录相关联。
  A表中的任意多条记录和B表的任意多条记录相关联。
  下面我们来看看怎样创建外键(Foreign Key),我们使用下面的DDL来创建表,它们之间指定了关联:
create table products (
id int not null auto_increment,
title varchar(100) not null,
/* . . . */
primary key (id)
);
create table orders (
id int not null auto_increment,
name varchar(100) not null,
/* ... */
primary key (id)
);
create table line_items (
id int not null auto_increment,
product_id int not null,
order_id int not null,
quantity int not null default 0,
unit_price float(10,2) not null,
constraint fk_items_product foreign key (product_id) references products(id),
constraint fk_items_order foreign key (order_id) references orders(id),
primary key (id)
);
  在上面的DDL中,订单和条目关联,条目又关联到具体的商品。注意这里的命名约定,外键的名字product_id,product是products表的单数形式,然后再加上表的主键名字_id构成外键名。
  上面的DDL中,订单和条目是一对多的关系,还有一种是多对多关系,例如,一种商品属于多个商品分类,一个商品分类又含有多种商品。对这种情况,通常我们使用第三个表,叫做结合表,这个表只包含两个要关联的表的id:
create table products (
id int not null auto_increment,
title varchar(100) not null,
/* . . . */
primary key (id)
);
create table categories (
id int not null auto_increment,
name varchar(100) not null,
/* ... */
primary key (id)
);
create table categories_products (
product_id int not null,
category_id int not null,
constraint fk_cp_product foreign key (product_id) references products(id),
constraint fk_cp_category foreign key (category_id) references categories(id)
);
  注意结合表的命名,在这里Rails的约定为两个表名,中间用下划线分开,表名按照字母排序,Rails会自动找到categories_products表将categories表和products链接起来,如果你没有按照约定,那么就要自己声明,以便Rails能够找到它。

(十四、指定关联关系)
Rails支持三种表间关联关系,一对一,一对多,多对多,你需要在Model中加入声明来标识这些关联:has_one,has_many,belongs_to,has_and_belongs_to_many。
  一对一关联关系可能存在于象订单和发票这样的关系,一个订单只能有一个发票,在Rails中,我们这样指明:
class Order < ActiveRecord::Base
has_one :invoice
. . .
class Invoice < ActiveRecord::Base
belongs_torder
. . .
  订单和条目之间的关系为一对多,我们这样声明:
class Order < ActiveRecord::Base
has_many :line_items
. . .
class LineItem < ActiveRecord::Base
belongs_torder
. . .
  我们也许会对商品进行分类,一种商品可能会归入几个商品类别下,而一个商品类别下有可能会有多种商品,商品和分类的关系就为多对多,Rails中我们这样声明:
class Product < ActiveRecord::Base
has_and_belongs_to_many :categories
. . .
class Category < ActiveRecord::Base
has_and_belongs_to_many :products
. . .
  上面的这些标识关联关系的定义都会添加一些方法给Model,用来在关联的对象中进行导航,后面我们会分别对上面的三种关联关系展开讨论。

(十五、一对一关联关系)
一对一关联,或者更正确的说是一对零或一对一关联,是通过外键引用到另外一张表中的至多一条记录实现的,下图描述了orders表和invoices表的关系:

  在Active Record中,要表示这样的关系需要在Order类中添加has_one:Invoice声明,并且同时在Invoice类中添加声明belongs_to:order,事实上,我们可以把这种关联关系看作是相互的,我们可以使Invoice有一个Order,也可以使Order有一个Invoice,不同的是,在将对象存储到数据库时,如果我们给一个对象赋予has_one关联给另一个既存对象,关联的对象将会自动被保存。例如:
an_invoice = Invoice.new(...)
order.invoice = an_invoice # invoice gets saved
  如果我们给一个对象赋予belongs_to关联到另一个对象,那么它将不会自动被保存,例如:
order = Order.new(...)
an_invoice.order = order # Order will not be saved
  还有另外一个不同点,当你给一个对象赋予has_one关联时,如果指向一个既存的子对象,这个既存对象的外键关联将会被移除,也就是清零,如下图:

  还有一个危险的地方,如果子记录不能被保存(没有通过验证等),Active Record也不会有抱怨,你也不会得到任何信息来指示该记录没有添加到数据库,所以,我们强烈推荐使用下面的方法:
invoice = Invoice.new
# fill in the invoice
unless invoice.save!
an_order.invoice = invoice
  因为save!方法在失败的时候会抛出异常,这样我们就知道发生了什么。
  belongs_to声明
  belongs_to声明给一个类指定父关联,Active Record约定在这个表中包含有引用到另一个表的外键,父类的名字假定为混合大小写,且单数,外键字段为单数,并且在末尾添加_id,所以,下面的代码:
class LineItem < ActiveRecord::Base
belongs_to :product
belongs_to :invoice_item
end
  Active Record关联line item到类Product和InvoiceItem,在底层,使用外键product_id和invoice_item_id关联到products和invoice_items表的id列。也可以像下面这样,给belongs_to一个哈希(hash):
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类,通过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=(),来访问和更新关联到line item对象上的product对象。在背后,Active Record保存数据库的步调一致,在我们保存line item对象的时候自动保存关联的product对象,并且将具有新的id的product对象和line item对象关联起来。
  在这种情况下,下面的方法将被生成到line item对象中:
product(force_reload=false):
  返回关联的product(如果没有关联的对象就返回nil),同时,结果将被缓存,对于相同的查询,将不会到数据库再次执行,除非force_reload参数为true。
  product=(obj)
  将指定的对象关联到line item,设置line item对象的外键到product对象的主键,如果product对象还没有保存,那么会在line item对象保存的同时,对product对象进行保存。
  build_product(attributes={})
  使用指定的attribute,构建一个新的product对象,line item对象将链接到该对象,而且,该对象还没有保存。
  Create_product(attributes={})
  和上面的build_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
  我们也可以通过传递一组参数来改变Active Record的默认行为,例如::class_name,:foreign_key和:conditions,就和前面介绍belongs_to时的一样,也可以使用:dependent和:order。
  :dependent的含义是,在从表中的记录不能独立于主表中的记录而存在,也就是说,如果你删除了父记录,而且你定义了:dependent= true,Active Record将自动删除从表中关联的记录。
  :order指定了在记录被返回前,怎样进行排序,我们会在后面关于has_many的内容里详细讨论。

(十六、一对多关联关系)
一对多关联可以使我们表示一组对象,例如,一个order可以包含有任意多个line item,在数据库中,所有的line item记录都通过外键关联到特定的order。
  在Active Record中,通过在父对象中的has_many来定义到子对象的关联,在子对象中使用belongs_to来指定父对象。我们已经在上一篇中了解了belongs_to声明,实际上,在一对多的情况下,和一对一是相同的,所以我们来了解has_many声明。

  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的line_items列表中追加一个对象,并且和设置了line_item对象外键值为order对象主键值,而且在order对象保存的时候,会同时保存line_item对象。
  我们可以像对数组一样对has_many关联进行循环:
order = Order.find(123)
total = 0.0
order.line_items.each do |li|
total += li.quantity * li.unit_price
end
  和has_one一样,我们可以改变Active Record的默认形式,我们可以给has_many一组设定,:class_name,:foreign_key,:conditions,:order和:dependent和has_one中的是一样的,has_many还有:exclusively_dependent,:finder_sql,:counter_sql。
  has_one和has_many都支持:dependent,这告诉Rails在删除主表中记录的同时删除从表中对应的记录。也就是对所有外键为删除的主记录的id的子对象调用它们destory方法。
  无论如何,如果从表仅仅和主表关联,而没有其他表关联,而且没有任何钩子方法在删除的时候执行操作,你可以使用:exclusively_dependent来代替:dependent,在这种情况下,对于所有的子记录会使用一条sql语句来删除,这样执行速度会快一点。
  你还可以通过使用:finder_sql和:counter_sql设定来复写Active Record用来对子记录进行查询和计数的sql。在:conditions设定不足够的情况下,这两个设定就很有用了。例如:
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用作计算行数的sql,如果:finder_sql被指定了,但是:counter_sql没有指定,Active Record将会根据查询sql来替换计算行数的sql。
  :order设定指定了从数据库中选出的记录的排序所使用的sql,如果你在遍历记录集的时候需要有特定的排序,你就需要指定:order,也就是设定sql语句的order by部分,默认情况下,所有的字段都是升序。例如:
class Order < ActiveRecord::Base
has_many :line_items,
:order => "quantity, unit_price DESC"
end
  现在再回到has_one声明,我们前面提到过也支持:order,但是一条父记录最多只有一条子记录,那么为什么要支持:order来允许指定排序条件呢?想象一下这样的场景,一个用户可能有多条订单,但是,如果我们想查看该用户的最新一条订单呢?这时候,我们就可以使用has_one声明了:
class Customer < ActiveRecord::Base
has_manyrders
has_one :most_recent_order,
:class_name => 'Order',
:order => 'created_at DESC'
end
  上面的代码创建了一个新属性:most_recent_order,将会引用到该用户的最新一条订单,我们可以这样使用它:
cust = Customer.find_by_name("Dave Thomas")
puts "Dave last ordered on #{cust.most_recent_order.created_at}"
  实际上,Active Record执行了下面的sql:
SELECT * FROM orders
WHERE customer_id = ?
ORDER BY created_at DESC
LIMIT 1
  根据前面我们学习的find方法,可以看到只取了排序后的第一条记录。
  has_many添加的方法
  就像belongs_to和has_one,has_many也添加了一组属性相关的方法在它所在的类中,下面我们来看看这些方法,我们的声明是这样
class Customer < ActiveRecord::Base
has_manyrders
end
  l     orders(force_reload=false)=
  返回一个和用户关联的订单的数组,结果集是被缓存的,对于相同的查询,不会再次从数据库提取数据,除非force_reload = true。
  l     orders <<order
  添加订单到指定用户的订单的列表中。
  l     orders.push(order1, ...)
  添加一个或多个订单对象到用户的订单列表中,concat()是该方法的别名。
  l     orders.delete(order1, ...)
  从用户的订单列表中删除一个或多个订单,但是不会删除数据库中对应的记录,只是将它们的customer_id外键设置为null,断开和用户的关联。
  l     orders.clear
  分离用户和订单,就像delete(),但是如果订单被指明为:dependent,数据库中对应的记录就会被删除。
  l     orders.find(options...)
  发出一个find()调用,但是仅返回用户关联的订单。
  l     orders.build(attributes={})
  构造一个新的order对象,用给定的属性初始化,并且关联到customer,该对象没有保存。
  l     orders.create(attributes={})
  构造并且保存一个新的order对象,用给定的属性初始化,并且关联到customer。

(十七、多对多关联关系)
在Rails中多对多关联通过在关联表对应的类中声明has_and_belongs_to_many来实现。
  在数据库中,多对多关联使用中间表来实现,表中包括关联表的主键,Active Record假定这个中间表的名字是由关联表的名字根据字母的顺序串联起来得到的。例如,关联表为categories和products,中间表的名字就是categories_products。

  注意我们的关联表没有id列,有两个原因,首先,不需要一个唯一的标识来识别两个外键之间的连接,我们定义表的语句像下面这样:
create table categories_products (
category_id int not null,
product_id int not null,
constraint fk_cp_category foreign key (category_id) references categories(id),
constraint fk_cp_product foreign key (product_id) references products(id),
primary key (category_id, product_id)
);
  第二个原因在中间表中不包括一个id列,Active Record在访问某个行时会自动包含所有的列。如果包含了一个id列,那么这个id列就会复写掉在关联表中的id列。
  The has_and_belongs_to_many() 声明
  has_and_belongs_to_many在很多方面很像has_many,has_and_belongs_to_many创建了本质上是一个集合的属性,该属性支持和has_many相同的方法。
  也许我们使用Rails来写一个社区站点,在这里用户可以阅读文章。这里有很多的用户和文章,而且任何一个用户都可以阅读多个文章,为了跟踪,我们希望知道谁读了哪些文章,每篇文章有谁阅读过,我们也希望知道用户最后一次在什么时间阅读了哪篇文章,我们会这样设计表:

  我们这样设置两个Model类互相关联:
class Article < ActiveRecord::Base
has_and_belongs_to_many :users
# ...
end
class User < ActiveRecord::Base
has_and_belongs_to_many :articles
# ...
end
  这样我们就可以列出所有阅读过文章123的用户和名为pragdave的用户阅读的所有文章:
# Who has read article 123?
article = Article.find(123)
readers = article.users
# What has Dave read?
dave = User.find_by_name("pragdave")
articles_that_dave_read = dave.articles
  当我们的程序通知某个人阅读了某篇文章的时候,将user记录和article记录建立关联,我们调用下面的方法:
class User < ActiveRecord::Base
has_and_belongs_to_many :articles
def read_article(article)
articles.push_with_attributes(article, :read_at => Time.now)
end
# ...
end
  方法push_with_attributes( )和<<方法的作用一样,都是给两个Model之间设置连接,而且还赋值给中间表记录什么人在什么时间阅读了文章。
  注:如果该方法难以理解,可以想象一下C#中使用反射给某个对象的字段赋值,我们需要提供对象,对象的字段名,字段对应的值来进行操作。
  作为一种的关联方法,has_and_belongs_to_many支持一系列声明来复写Active Record的默认设置::class_name, :foreign_key和:conditions,和其他的has_方法一样(:foreign_key设置中间表中的外键的名字)。进一步说,has_and_belongs_to_many支持复写中间表的名字,外键列的名字,find,insert,delete中使用的SQL,详细请参考Rdoc。

(十八、自关联)
或许存在这样的情况,在一个表中,一条记录关联到表中的另一条记录,例如,公司中的每个雇员都有上级和下级,而他们同时又是雇员,在Rails中你可以这样使用Employee类:
class Employee < ActiveRecord::Base
belongs_to :manager,
:class_name => "Employee",
:foreign_key => "manager_id"
belongs_to :mentor,
:class_name => "Employee",
:foreign_key => "mentor_id"
has_many :mentored_employees,
:class_name => "Employee",
:foreign_key => "mentor_id"
has_many :managed_employees,
:class_name => "Employee",
:foreign_key => "manager_id"
end
  让我们使用一些数据,这里雇员Clem和Dawn都有上级和下级:
Employee.delete_all
adam = Employee.create(:id => 1, :name => "Adam")
beth = Employee.create(:id => 2, :name => "Beth")
clem = Employee.new(:name => "Clem")
clem.manager = adam
clem.mentor = beth
clem.save!
dawn = Employee.new(:name => "Dawn")
dawn.manager = adam
dawn.mentor = clem
dawn.save!
  现在我们可以通过关联,来回答“X的下属是谁?”,“Y的上级是谁?”。
p adam.managed_employees.map {|e| e.name} # => [ "Clem", "Dawn" ]
p adam.mentored_employees # => []
p dawn.mentor.name # => "Clem"
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics