#215 Advanced Queries in Rails 3
- Download:
- source codeProject Files in Zip (105 KB)
- mp4Full Size H.264 Video (17.5 MB)
- m4vSmaller H.264 Video (10.2 MB)
- webmFull Size VP8 Video (30.1 MB)
- ogvFull Size Theora Video (27.6 MB)
在这一集我们将接着第202集的视频,展示Rails 3中将要使用的高级查询,第202集Railscast【视频,文本】视频中我们展示了Rails 3中使用的查询。
用类方法代替Scopes
在我们要来展示的应用程序中包括两个模型:Product
和 Category
,其中,Product属于Category,并且在product模型中有一个named scope: discontinued
, 用来表示已经停产和价钱低于一个给定的值的产品。
class Product < ActiveRecord::Base belongs_to :category scope :discontinued, where(:discontinued => true) scope :cheaper_than, lambda { |price| where("price < ?", price) } end
其中,在第二个named scope的描述中我们用到了lambda,如果你使用过named scope特别是有需要大量传入参数或者是scope本身逻辑复杂的情况下,都会考虑要把scope抽出来写到方法里,虽然,我们当前的例子并没有什么复杂的,为了演示我们同样抽成方法如下:
class Product < ActiveRecord::Base belongs_to :category scope :discontinued, where(:discontinued => true) def self.cheaper_than(price) where("price < ?", price) end end
如上抽出方法的scope和原来的scope有同样的功能,虽然在Rails 2中同样支持这样的操作,然而,在rails 3中这样的方法抽象会有更完善的功能。假设我们有另外一个scope Cheap表示价钱低于5的产品,我们就可以重用之前为了使用scope而创建的方法,构造如下:
scope :cheap, cheaper_than(5)
然而,这里有一个潜在的陷阱,就是我们的scope方法的定义要放到其他的类定义方法的后面,也就是说带方法的scope会在普通scope的位置靠后。
在rails 控制台我们可以看到scope对应的SQL如下:
ruby-1.8.7-p249 > Product.cheap.to_sql => "SELECT \"products\".* FROM \"products\" WHERE (price < 5)"
直接调用定义的scope会显示符合scope条件的产品。
>Product.cheap => [#<Product id: 1, name: "Top", price: 4.99, discontinued: nil, category_id: 3, created_at: "2010-05-24 21:01:59", updated_at: "2010-05-24 21:01:59">, #<Product id: 2, name: "Milk", price: 2.99, discontinued: nil, category_id: 2, created_at: "2010-05-24 21:02:38", updated_at: "2010-05-24 21:02:38">]
关联
在Rails控制台下我们可以展示另外一个通过关联使用scope的技巧。我们之前提到Product和category有belongs_to的关联,所以,也就是说我们可以使用joins
来返回SQL的join表的查询
ruby-1.8.7-p249 > Category.joins(:products).to_sql => "SELECT \"categories\".* FROM \"categories\" INNER JOIN \"products\" ON \"products\".\"category_id\" = \"categories\".\"id\""
下面是关于符合特定scope的查询的写法,比如我们希望查询至少有一个产品的价值是低于5的所有种类。那么我们可以有下面的两种写法,一种是用merge
如下:
> Category.joins(:products).merge(Product.cheap) => [#<Category id: 3, name: "Games", created_at: "2010-05-24 21:00:57", updated_at: "2010-05-25 18:30:18">, #<Category id: 2, name: "Groceries", created_at: "2010-05-24 21:00:50", updated_at: "2010-05-25 18:30:39">]
同样的我们也可以使用和merge
有相同含义的&
来表达如下
> Category.joins(:products) & Product.cheap => [#<Category id: 3, name: "Games", created_at: "2010-05-24 21:00:57", updated_at: "2010-05-25 18:30:18">, #<Category id: 2, name: "Groceries", created_at: "2010-05-24 21:00:50", updated_at: "2010-05-25 18:30:39">]
使用这样的方法我们可以关联一些不在自己model的检索条件,比如我们希望查询所有产品的价值都小于5的类别如下,并且我们可以使用to_sql
方法查看对应的mysql的语句。
> (Category.joins(:products) & Product.cheap).to_sql => "SELECT \"categories\".* FROM \"categories\" INNER JOIN \"products\" ON \"products\".\"category_id\" = \"categories\".\"id\" WHERE (price < 5)"
更为强大的是,我们可以在named scope定义中使用是定义好的scope就如同我们刚刚用到的。比如,创建一个包括所有cheap 产品的类别,如下:
class Category < ActiveRecord::Base has_many :products scope :with_cheap_products, joins(:products) & Product.cheap end
这儿named scope会返回和我们之前的查询相同的数据:
> Category.with_cheap_products => [#<Category id: 3, name: "Games", created_at: "2010-05-24 21:00:57", updated_at: "2010-05-25 18:30:18">, #<Category id: 2, name: "Groceries", created_at: "2010-05-24 21:00:50", updated_at: "2010-05-25 18:30:39">]
有一个值得注意的问题是关于多表关联查询中的表名问题,这一点可以通过查看上面我们建立的scope的sql看到:实际上在where查询中我们并没有写明特定的表名.
> Category.with_cheap_products.to_sql => "SELECT \"categories\".* FROM \"categories\" INNER JOIN \"products\" ON \"products\".\"category_id\" = \"categories\".\"id\" WHERE (price < 5)"
当两个关联的表都有相关的字段的时候,这就会是问题。所以,我们应该在Product
模型从新定义并且指定表名,这样就不怎么出错了。
def self.cheaper_than(price) where("products.price < ?", price) end
也就是说,在我们自己写SQL条件的时候,为了防止不同的表之间有重复字段的问题就应该在关联查询中写明表名。当然如果是按照Rails的哈希条件写,比如在scope里我们定义查询,那么就不要我们自己指明属于哪个表,Rails会自动加上。
通过Named Scopes创建记录
我们可以通过named scopes创建新的记录,例如在Product
模型上有一个叫discontinued的named scope
> Product.discontinued => [#<Product id: 3, name: "Some DVD", price: 13.49, discontinued: true, category_id: 1, created_at: "2010-05-25 19:45:05", updated_at: "2010-05-25 19:45:05">]
因为named scope是用的Hash所以,我们可以通过调用build
方法创建discontinued
属性为真的记录。
> p = Product.discontinued.build => #<Product id: nil, name: nil, price: nil, discontinued: true, category_id: nil, created_at: nil, updated_at: nil>
这种创建的方式类似于通过Rails的关联方式的创建(译者晓夜注:例如has_many的关联)这样默认的情况下就会有一些限定通过外键设置好。我们通过scope创建记录是用过where语句进行的限定。
Arel
最后我们以Arel对的介绍结束本文。Arel是一种简化的检索方式,在Rails 3中使用很简单,你只需要取一个model的arel_table
属性。如下:
> t = Product.arel_table
变量t代表product表,我们可以通过如下方式访问字段属性:
>t[:price] => <Attribute price>
通过对属性调用方法等于执行条件查询,例如,我们要找到价值为2.99美元的所有商品:
> t[:price].eq(2.99) => #<Arel::Predicates::Equality:0x1040dd9f0 @operand1=<Attribute price>, @operand2=2.99>
这将返回一个predicate对象代表着查询条件。还有一些其他条件我们可以用,比如matches
代表Like查询。我们也可以通过to_sql
来查看这个查询对应的sql语句
> t[:name].matches('%lore').to_sql => "\"products\".\"name\" LIKE '%lore'"
同时我们可以使用or方法将两个predicate对象连在一起进行组合查询,比如查询价值2.99美元或者产品名是lore的产品:
t[:price].eq(2.99).or(t[:name].matches('%lore'))
这将生成如下Sql语句,我们可以通过to_sql
看到:
> t[:price].eq(2.99).or(t[:name].matches('%lore')).to_sql => "(\"products\".\"price\" = 2.99 OR \"products\".\"name\" LIKE '%lore')"
因为predicate对象对应一个查询条件,那么我们就可以把predicate作为一个参数传给ActiveRecord的where方法。例如把上面的predicate传给Product.where
,就会返回价值是2.99美元或者名字以lore结尾的记录集。
> Product.where(t[:price].eq(2.99).or(t[:name].matches('%lore'))) => [#<Product id: 2, name: "Milk", price: 2.99, discontinued: nil, category_id: 2, created_at: "2010-05-24 21:02:38", updated_at: "2010-05-24 21:02:38">, #<Product id: 4, name: "Knight Lore", price: 2.99, discontinued: nil, category_id: nil, created_at: "2010-05-26 19:36:02", updated_at: "2010-05-26 19:36:02">]
这里我们只是介绍了Arel最基本的知识。实际上,Arel还可以做更多的事。现在也有很多基于Arel的方便的插件,例如MetaWhere,就可以提供如下方式查询:
Product.where(:price.eq => 2.99, :name.matches => '%lore')
如果需要更采用灵活的方式进行更加复杂的查询,你可以再进一步了解Arel和MetaWhere相关的知识