Active Record-Query Interface-CN
Active Record
Active Record(中文名:活动记录)是一种领域模型模式,特点是一个模型类对应关系型数据库中的一个表,而模型类的一个实例对应表中的一行记录。Active Record 和Row Gateway (行记录入口)十分相似,但前者是领域模型,后者是一种数据源模式。关系型数据库往往通过外键来表述实体关系,Active Record 在数据源层面上也将这种关系映射为对象的关联和聚集。
Active Record 适合非常简单的领域需求,尤其在领域模型和数据库模型十分相似的情况下。如果遇到更加复杂的领域模型结构(例如用到继承、策略的领域模型),往往需要使用分离数据源的领域模型,结合Data Mapper (数据映射器)使用。
Active Record 驱动框架一般兼有ORM 框架的功能,但ActivActive Recorde Record 不是简单的ORM,正如和Row Gateway 的区别。著名的例子是全栈(Full Stack)Web 开发框架Ruby on Rails ,其默认使用一个纯Ruby 写成的Active Record 框架来驱动MVC 中的模型层。
对象关系映射(ORM)提供了概念性的、易于理解的模型化数据的方法。ORM方法论基于三个核心原则:简单:以最基本的形式建模数据。传达性:数据库结构被任何人都能理解的语言文档化。精确性:基于数据模型创建正确标准化了的结构。
在Martin Fowler 的《企业应用架构模式》一书中曾详细叙述了本模式。
以下是著名的Active Record 驱动框架:
SQLObject(Python)
Ruby on Rails ActiveRecord (Ruby)
Yii Framework ActiveRecord (PHP)
Castle ActiveRecord (.NET)
Migrations
Migrations are a convenient way for you to alter移动your database in a structured and organized manner.Migrations是一种很便捷的方法让你能够以一种结构化的和有组织的方式来迁移你的数据库。You could edit fragments of SQL by hand but you would then be responsible for telling other developers that they need to go and run them.你可以手动编辑SQL片段,而且你有责任把这些告诉其他的开发人员,因为他们需要开发和使用它们。You’d also have to keep track of which changes need to be run against the production machines next time you deploy.你也可以跟踪对你部署的代码在接下来的production机器(将会)发生的变化。
Active Record tracks which migrations have already been run so all you have to do is update your source and run rake db:migrate.Active Record跟踪并迁移你已经运行过的(代码和数据),而你只需要在更新了你的源代码的时候执行rake db:migrate。Active Record will work out which migrations should be run.Active Recor将会计算出那些迁移需要被执行。It will also update your db/schema.rb file to match the structure of your database.它还会更新你的db/schema.rb文件使其于你的数据库结构相匹配。
Rails使用的是Active Record 框架来处理数据迁移,这里笔者把Active Record 框架放在一个地方学习了,如需了解Migration部分需要直接阅读Migration部分。
Active Record Validations and Callbacks 活动记录验证和回调
This guide teaches you how to hook勾子into the life cycle of your Active Record objects.这个教程指导你怎样挂接到你的Active Record objects的生存周期。You will learn how to validate the state of objects before they go into the database, and how to perform custom operations at certain points in the object life cycle.你将会学习到在将数据对象存入数据库之前怎样验证它们的状态,以及在对象生存周期的一些点上怎样执行定制操作。
Rails使用的是Active Record 框架来处理验证和回调,这里笔者把Active Record 框架放在一个地方学习了,如需了解Migration部分需要直接阅读Validations and Callbacks 部分。
A Guide to Active Record Associations
This guide covers the association features of Active Record. By referring to this guide, you will be able to:本教程涵盖了Active Record的关系部分的特性。(通过)这个教程提及的,你将能够:
- Declare associations between Active Record models 在Active Record的models中声明(它们的)关系
- Understand the various types of Active Record associations 明白各种类型的Active Record关系
- Use the methods added to your models by creating associations 通过添加方法到models(的形式)来创建关系
Active Record Query Interface(基于)活动记录的查询接口
This guide covers different ways to retrieve data from the database using Active Record. By referring to this guide, you will be able to:
这个教程涵盖了使用基于Active Record 的不同方式从数据库检索数据。同过参考这个教程,你可以掌握:
- Find records using a variety of methods and conditions 使用各种方法和条件查找记录
- Specify the order, retrieved attributes, grouping, and other properties of the found records对查找的记录指定顺序,检索属性,组,和其他属性
- Use eager急于loading to reduce the number of database queries needed for data retrieval 使用预先加载,以减少数据检索所需的数据库查询数量
- Use dynamic finders methods 使用多元搜索方法
- Check for the existence of particular records 在特定的记录部分检查存在的记录
- Perform various calculations on Active Record models 在Active Record 模型中执行各种计算
ThisGuideisbasedonRails3.0.SomeofthecodeshownherewillnotworkinotherversionsofRails.
If you’re used to using raw SQL to find database records then, generally, you will find that there are better ways to carry out进行the same operations in Rails. Active Record insulates you from the need to use SQL in most cases.如果你使用过原生的SQL(语句)来查询数据库,那么,一般情况下,你将会发现(对数据库)进行同样的操作在Rails中会有这更好的方法。Active Record在大多数情况下会让你远离你(以前)需要使用的SQL查询语句。
Code examples throughout this guide will refer to one or more of the following models:贯穿这个教材代码示例将会参考一个或多个下面的模型:
All of the following models use id as the primary key, unless specified otherwise.所有的模型都会使用id作为主键,除非指定了其他主键。
class Client < ActiveRecord::Base
has_one :address
has_many :orders
has_and_belongs_to_many :roles
end
class Address < ActiveRecord::Base
belongs_to :client
end
class Order < ActiveRecord::Base
belongs_to :client, :counter_cache => true
end
class Role < ActiveRecord::Base
has_and_belongs_to_many :clients
end
Active Record will perform queries on the database for you and is compatible兼容with most database systems (MySQL, PostgreSQL and SQLite to name a few). Regardless of which database system you’re using, the Active Record method format will always be the same.
Active Record将会为你在数据库中执行查询并且它兼容大多数的数据库系统(MySQL, PostgreSQL and SQLite这里仅仅列举这些)。不管你使用的是何种数据库系统,Active Record的方法格式通常是相同的。
1 Retrieving Objects from the Database在数据库中检索对象
To retrieve objects from the database, Active Record provides several finder methods. Each finder method allows you to pass arguments into it to perform certain queries on your database without writing raw SQL.
为了从数据库中检索对象,Active Record提供了一些查询方法。每个查询方法都运行你带入参数并在数据库中执行查询而不用写SQL自身的语句。
The methods are:
- where
- select
- group
- order
- reorder
- reverse_order 逆向排序
- limit
- offset 偏移
- joins
- includes
- lock
- readonly
- from
- having
All of the above methods return an instance of ActiveRecord::Relation.所有以上方法会返回一个ActiveRecord::Relation的实例。
Primary operation of Model.find(options) can be summarized as:
Model.find(options)主要的操作可以被概括为:
- Convert the supplied options to an equivalent SQL query. 转换提供的(查询)选项为等同的SQL查询
- Fire the SQL query and retrieve the corresponding results from the database. 开始SQL查询并且检索从数据库相应的结果
- Instantiate the equivalent Ruby object of the appropriate model for every resulting row. 把每个(数据库中原生的)结果实例化等同的Ruby对象
- Run after_find callbacks if any. 运行after_find进行回调如果需要
1.1 Retrieving a Single Object检索单个对象
Active Record lets you retrieve a single object using five different ways.
Active Record让你可以使用五种不同的方法检索单个对象。
1.1.1 Using a Primary Key使用主键查询
Using Model.find(primary_key), you can retrieve the object corresponding to the supplied primary key and matching the supplied options (if any). For example:
使用Model.find(primary_key),你可以检索对象通过提供相应的主键或者匹配提供的其他选项(如果存在)。例如:
# Find the client with primary key (id) 1.
rails console
Loading development environment (Rails 3.1.1)
irb(main):001:0> p=Post.find(1)
Post Load (9.0ms) SELECT “posts”.* FROM “posts” WHERE “posts”.“id” = ? LIMIT 1 [[“id”, 1]]
=> #<Post id: 1, name: “阿飞姐姐”, title: “接口姐姐”, content: “12342424”, created_at: “2011-11-05 15:10:41”, updated_at: “2011-11-05 15:10:41”>
SQL equivalent of the above is:
SELECT * FROM posts WHERE (posts.id = 1)
Model.find(primary_key) will raise an ActiveRecord::RecordNotFound exception if no matching record is found.
Model.find(primary_key)如果没有记录匹配则会抛出一个ActiveRecord::RecordNotFound异常。
1.1.2 first
Model.first finds the first record matched by the supplied options. For example:
Model.first找到与提供的选项匹配的第一条记录。例如:
client = Client.first
=> #<Client id: 1, first_name: “Lifo”>
irb(main):018:0> Post.first
Post Load (0.4ms) SELECT “posts”.* FROM “posts” LIMIT 1
=> #<Post id: 2, name: “jhjguxin”, title: “test console”, content: “A new post to test console”, created_at: “2011-11-05 15:55:17”, updated_at: “2011-11-05 15:55:17”
SQL equivalent of the above is:
SELECT * FROM clients LIMIT 1
Model.first returns nil if no matching record is found. No exception will be raised.
如果没有记录匹配Model.first会返回nil。不会抛出异常。
1.1.3 last
Model.last finds the last record matched by the supplied options. For example:
client = Client.last
=> #<Client id: 221, first_name: “Russel”>
SQL equivalent of the above is:上面等同的SQL语句是:
SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
##SELECT “posts”.* FROM “posts” ORDER BY “posts”.“id” DESC LIMIT 1
Model.last returns nil if no matching record is found. No exception will be raised.
如果没有记录匹配Model.last会返回nil。不会抛出异常。
1.2 Retrieving Multiple Objects检索多个目标
1.2.1 Using Multiple Primary Keys使用多个主键
Model.find(array_of_primary_key) also accepts an array of primary keys. An array of all the matching records for the supplied primary keys is returned. For example:
Model.find(array_of_primary_key)也接受一个主键数组。将会返回一个由所有与提供的主键匹配的记录组成的数组。例如:
# Find the clients with primary keys 1 and 10.
client = Client.find(1, 10) # Or even Client.find([1, 10])
=> [#<Client id: 1, first_name: => “Lifo”>, #<Client id: 10, first_name: => “Ryan”>]
SQL equivalent of the above is:
SELECT * FROM clients WHERE (clients.id IN (1,10))
Model.find(array_of_primary_key)willraiseanActiveRecord::RecordNotFoundexceptionunlessamatchingrecordisfoundforallofthesuppliedprimarykeys.
Model.find(array_of_primary_key)只要有一条记录没有找到就会抛出ActiveRecord::RecordNotFound异常
ActiveRecord::RecordNotFound:Couldn’tfindallPostswithIDs(2,4,5)(found2results,butwaslookingfor3)
1.3 Retrieving Multiple Objects in Batches分批次的检索多个目标
Sometimes you need to iterate反复重复over a large set of records. For example to send a newsletter to all users, to export some data, etc.
有时候你需要遍历大量的记录,例如发送一条业务通讯给所有的用户,输出一些数据,等等。
The following may seem very straight forward at first:
首先(通过)以下内容看起来会非常直观
# Very inefficient when users table has thousands of rows.
User.all.each do |user|
NewsLetter.weekly_deliver(user)
end
But if the total number of rows in the table is very large, the above approach may vary from being under performant to just plain平原impossible.
但是如果(数据)表单的行有非常大的量,上面的方法在执行(的时候)不可能性能(还是那么)平稳。
This is because User.all.each makes Active Record fetch the entire table, build a model object per row, and keep the entire array in the memory. Sometimes that is just too many objects and demands too much memory.
这是因为User.all.each使得Active Record获取整个表单,给每一行数据创建一个object model,并且保留整个数组在内存中。有时会有太多的对象并且需要太多的内存。
Configuring the batch size配置批次大小
Behind the scenes find_each fetches rows in batches of 1000 and yields them one by one. The size of the underlying batches is configurable via the :batch_size option.在使用find_each获取1000次记录行并且一个接一个的yield它们的情况中。(确定)下面(查找)批次的大小是通过配置:batch_size选项。
To fetch User records in batch size of 5000:
User.find_each(:batch_size => 5000) do |user|
NewsLetter.weekly_deliver(user)
end
Starting batch find from a specific primary key通过一个指定的主键开始批次查找
Records are fetched in ascending order on the primary key, which must be an integer. The :start option allows you to configure the first ID of the sequence if the lowest is not the one you need. This may be useful for example to be able to resume an interrupted batch process if it saves the last processed ID as a checkpoint.这会非常有用比如能够减少因为设置最后处理的ID作为checkpoint引起的中断。
(这里)是按照主键值的升序排列获取记录的,主键值必须是整数。:start选项允许你配置序列的开始ID如果排序最低的(记录)不是你需要的。
To send newsletters only to users with the primary key starting from 2000:
User.find_each(:batch_size => 5000, :start => 2000) do |user|
NewsLetter.weekly_deliver(user)
end
Additional options其他(附加)选项
find_each accepts the same options as the regular find method. However, :order and :limit are needed internally and hence not allowed to be passed explicitly.
find_each接受和正则find方法相同的选项。
1.3.2 find_in_batches
You can also work by chunks instead of row by row using find_in_batches. This method is analogous to find_each, but it yields arrays of models instead:
通过使用find_in_batches你也可以用chunks替代数据行。这个方法类似于 find_each,但是作为替代它(会输出)到一个数组区域:
# Works in chunks of 1000 invoices at a time.
Invoice.find_in_batches(:include => :invoice_lines) do |invoices|
export.add_invoices(invoices)
end
The above will yield the supplied block with 1000 invoices every time.上面的语句每次会提供给语句1000invoices。
2 Conditions条件
The where method allows you to specify conditions to limit the records returned, representing the WHERE-part of the SQL statement. Conditions can either be specified as a string, array, or hash.
where方法允许你指定条件限制记录返回(的内容),表示SQL部分的WHERE部分。条件可以指定为一个字符串,数组,或者hash(字典)。
2.1 Pure String Conditions纯字符串条件
If you’d like to add conditions to your find, you could just specify them in there, just like Client.where(“orders_count = ‘2’”). This will find all clients where the orders_count field’s value is 2.
Building your own conditions as pure strings can leave you vulnerable脆弱to SQL injection注入exploits漏洞. For example, Client.where(“first_name LIKE ‘%#{params[:first_name]}%’”) is not safe. See the next section for the preferred way to handle conditions using an array.
2.2 Array Conditions
Now what if that number could vary, say as an argument from somewhere? The find then becomes something like:
现在,如果这个数字可能会有所不同,(比如说)作为某个地方的一个参数?查找会变成如下:
Client.where(“orders_count = ?”, params[:orders])
Active Record will go through the first element in the conditions value and any additional elements will replace the question marks (?) in the first element.
Active Record将会在第一个元素中表示条件(语句)并且其他元素取代第一个元素中的问号。
Or if you want to specify two conditions, you can do it like:
Client.where(“orders_count = ? AND locked = ?”, params[:orders], false)
In this example, the first question mark will be replaced with the value in params[:orders] and the second will be replaced with the SQL representation of false, which depends on the adapter.
The reason for doing code like:
Client.where(“orders_count = ?”, params[:orders])
instead of:
Client.where(“orders_count = #{params[:orders]}”)
is because of argument safety. Putting the variable directly into the conditions string will pass the variable to the database as-is. This means that it will be an unescaped variable directly from a user who may have malicious intent. If you do this, you put your entire database at risk because once a user finds out he or she can exploit your database they can do just about anything to it. Never ever put your arguments directly inside the conditions string.
(这里)是基于参数安全(考虑)。将变量直接放入条件字符串将会(原封不动的传送变量)到数据库。他的意思是这个参数有可能将会是以一个来自用户的恶意的非转义的变量。如果你这样做,你就把整个数据库放在了风险之中,因为一旦有用户找到他们或者它可以利用(漏洞)对你数据库做任何操作。永远不要直接把你的参数放在条件字符串中。
For more information on the dangers of SQL injection, see the RubyonRailsSecurityGuide.
2.2.1 Placeholder Conditions条件(参数)占位符
Similar to the (?) replacement style of params, you can also specify keys/values hash in your array conditions:最简单的是使用(?)替代参数的形式,你也可以指定keys/values hash在你的条件语句数组中:
Client.where(“created_at >= :start_date AND created_at <= :end_date”,
{:start_date => params[:start_date], :end_date => params[:end_date]})
This makes for clearer readability if you have a large number of variable conditions.当你有大量的条件变量时这样表示更加简洁和可读性更好。
2.2.2 Range Conditions范围条件
If you’re looking for a range inside of a table (for example, users created in a certain timeframe) you can use the conditions option coupled with the IN SQL statement for this. If you had two dates coming in from a controller you could do something like this to look for a range:
如果你正在一个表中限定一个条件范围查找(例如,用户限定在一定的时间表中创建)你可以使用条件选项加上(这个参数的)IN SQL声明。如果有来自一个controller的两个日期,你可以做些事情查找一个范围:
Client.where(:created_at => (params[:start_date].to_date)..(params[:end_date].to_date))
This query will generate something similar to the following SQL:
SELECT “clients”.* FROM “clients” WHERE (“clients”.“created_at” BETWEEN ‘2010-09-29’ AND ‘2010-11-30’)
2.3 Hash Conditions Hash字典条件
Active Record also allows you to pass in hash conditions which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want conditionalised and the values of how you want to conditionalise them:
Active Record同样也允许你通过hash条件来增加你条件语句的可读性。在hash条件,以键作为你的条件化参数并且相应的值则是具体的条件限制。
Only equality, range and subset checking are possible with Hash conditions.
2.3.1 Equality Conditions等同条件
Client.where(:locked => true)
The field name can also be a string:
Client.where(‘locked’ => true)
2.3.2 Range Conditions
The good thing about this is that we can pass in a range for our fields without it generating a large query as shown in the preamble of this section.
Client.where(:created_at => (Time.now.midnight – 1.day)..Time.now.midnight)
This will find all clients created yesterday by using a BETWEEN SQL statement:
这将会通过使用一个BETWEEN SQL声明查找client中昨天创建的记录。
SELECT * FROM clients WHERE (clients.created_at BETWEEN ‘2008-12-21 00:00:00’ AND ‘2008-12-22 00:00:00’)
This demonstrates a shorter syntax for the examples in ArrayConditions
2.3.3 Subset Conditions子集条件
If you want to find records using the IN expression you can pass an array to the conditions hash:
如果想使用IN表达式查找记录你可以在条件hash字典中加上一个数组:
Client.where(:orders_count => [1,3,5])
This code will generate SQL like this:
SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
3 Ordering排序
To retrieve records from the database in a specific order, you can use the order method.
如果你想检索记录从数据库中并且以一种指定的方式排序,你可以使用order方法。
For example, if you’re getting a set of records and want to order them in ascending order by the created_at field in your table:
Client.order(“created_at”)
You could specify ASC or DESC as well:
Client.order(“created_at DESC”)
# OR
Client.order(“created_at ASC”)
Or ordering by multiple fields:或者按照多个fields排序:
Client.order(“orders_count ASC, created_at DESC”)
4 Selecting Specific Fields
By default, Model.find selects all the fields from the result set using select *.
默认情况中,Model.find会选择所有的fields作为结果并且(对数据库执行)select *。
To select only a subset of fields from the result set, you can specify the subset via the select method.
要想只选中fields中的一个子集作为结果,你可以通过select方法指定子集。
Iftheselectmethodisused,allthereturningobjectswillbereadonly.
For example, to select only viewable_by and locked columns:
Client.select(“viewable_by, locked”)
The SQL query used by this find call will be somewhat like:
SELECT viewable_by, locked FROM clients
Be careful because this also means you’re initializing a model object with only the fields that you’ve selected. If you attempt to access a field that is not in the initialized record you’ll receive:
ActiveModel::MissingAttributeError: missing attribute: <attribute>
Where <attribute> is the attribute you asked for. The id method will not raise the ActiveRecord::MissingAttributeError, so just be careful when working with associations because they need the id method to function properly.
You can also call SQL functions within the select option. For example, if you would like to only grab a single record per unique value in a certain field by using the DISTINCT function you can do it like this:
Client.select(“DISTINCT(name)”)
5 Limit and Offset
To apply LIMIT to the SQL fired by the Model.find, you can specify the LIMIT using limit and offset methods on the relation.
You can use limit to specify the number of records to be retrieved, and use offset to specify the number of records to skip before starting to return the records. For example
Client.limit(5)
will return a maximum of 5 clients and because it specifies no offset it will return the first 5 in the table. The SQL it executes looks like this:
SELECT * FROM clients LIMIT 5
Adding offset to that
Client.limit(5).offset(30)
will return instead a maximum of 5 clients beginning with the 31st. The SQL looks like:
将会返回最大5个clients并且从第31个开始。这个SQL看起来是:
SELECT * FROM clients LIMIT 5 OFFSET 30
6 Group
To apply a GROUP BY clause to the SQL fired by the finder, you can specify the group method on the find.
For example, if you want to find a collection of the dates orders were created on:
Order.select(“date(created_at) as ordered_date, sum(price) as total_price”).group(“date(created_at)”)
And this will give you a single Order生产者object for each date where there are orders in the database.
The SQL that would be executed would be something like this:
SELECT date(created_at) as ordered_date, sum(price) as total_price FROM orders GROUP BY date(created_at)
7 Having
SQL uses the HAVING clause短语to specify conditions on the GROUP BY fields. You can add the HAVING clause to the SQL fired by the Model.find by adding the :having option to the find.
For example:
Order.select(“date(created_at) as ordered_date, sum(price) as total_price”).group(“date(created_at)”).having(“sum(price) > ?”, 100)
The SQL that would be executed would be something like this:
SELECT date(created_at) as ordered_date, sum(price) as total_price FROM orders GROUP BY date(created_at) HAVING sum(price) > 100
This will return single order objects for each day, but only those that are ordered more than $100 in a day.
8 Overriding Conditions覆盖条件
8.1 except排除
You can specify certain conditions to be excepted by using the except method. For example:
Post.where(‘id > 10’).limit(20).order(‘id asc’).except(:order)#取消排序
The SQL that would be executed:
SELECT * FROM posts WHERE id > 10 LIMIT 20
irb(main):006:0> Post.where(‘id > 1’).limit(20).order(‘id desc’)
Post Load (0.4ms) SELECT “posts”.* FROM “posts” WHERE (id > 1) ORDER BY id desc LIMIT 20
=> [#<Post id: 4, name: “2134”, title: “2134”, content: “21343242134”, created_at: “2011-11-09 10:25:38”, updated_at: “2011-11-09 10:25:38”>, #<Post id: 2, name: “jhjguxin”, title: “test console”, content: “A new post to test console”, created_at: “2011-11-05 15:55:17”, updated_at: “2011-11-05 15:55:17”>]
irb(main):007:0> Post.where(‘id > 1’).limit(20).order(‘id desc’).except(:order)
Post Load (0.4ms) SELECT “posts”.* FROM “posts” WHERE (id > 1) LIMIT 20
=> [#<Post id: 2, name: “jhjguxin”, title: “test console”, content: “A new post to test console”, created_at: “2011-11-05 15:55:17”, updated_at: “2011-11-05 15:55:17”>, #<Post id: 4, name: “2134”, title: “2134”, content: “21343242134”, created_at: “2011-11-09 10:25:38”, updated_at: “2011-11-09 10:25:38”>]
8.2 only
You can also override conditions using the only method. For example:
Post.where(‘id > 10’).limit(20).order(‘id desc’).only(:order, :where)
The SQL that would be executed:
SELECT * FROM posts WHERE id > 10 ORDER BY id DESC
8.3 reorder
The reorder method overrides the default scope order. For example:
class Post < ActiveRecord::Base
..
..
has_many :comments, :order => ‘posted_at DESC’
end
Post.find(10).comments.reorder(‘name’)
The SQL that would be executed:
SELECT * FROM posts WHERE id = 10 ORDER BY name
In case the reorder clause is not used, the SQL executed would be:
SELECT * FROM posts WHERE id = 10 ORDER BY posted_at DESC
If no ordering clause is specified in the query, the reverse_order orders by the primary key in reverse撤销order.
Client.where(“orders_count > 10”).reverse_order
The SQL that would be executed:
SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC
This method accepts no arguments.
9 Readonly Objects
Active Record provides readonly method on a relation to explicitly disallow modification or deletion of any of the returned object. Any attempt to alter or destroy a readonly record will not succeed, raising an ActiveRecord::ReadOnlyRecord exception.
client = Client.readonly.first
client.visits += 1
client.save
As client is explicitly set to be a readonly object, the above code will raise an ActiveRecord::ReadOnlyRecord exception when calling client.save with an updated value of visits.
10 Locking Records for Update
Locking is helpful for preventing race conditions when updating records in the database and ensuring atomic updates.
Active Record provides two locking mechanisms:
- Optimistic乐观Locking
- Pessimistic悲观Locking
10.1 Optimistic Locking
Optimistic locking allows multiple users to access the same record for edits, and assumes假定a minimum of conflicts with the data. It does this by checking whether another process has made changes to a record since it was opened. An ActiveRecord::StaleObjectError exception is thrown if that has occurred发生and the update is ignored.
Optimistic locking column
In order to use optimistic locking, the table needs to have a column called lock_version. Each time the record is updated, Active Record increments the lock_version column. If an update request is made with a lower value in the lock_version field than is currently in the lock_version column in the database, the update request will fail with an ActiveRecord::StaleObjectError. Example:
c1 = Client.find(1)
c2 = Client.find(1)
c1.first_name = “Michael”
c1.save
c2.name = “should fail”
c2.save # Raises a ActiveRecord::StaleObjectError
You’re then responsible有责任for dealing处理with the conflict by rescuing the exception and either rolling back, merging, or otherwise apply the business logic needed to resolve the conflict.
Youmustensurethatyourdatabaseschemadefaultsthelock_versioncolumnto0.
This behavior can be turned off by setting ActiveRecord::Base.lock_optimistically = false.
To override the name of the lock_version column, ActiveRecord::Base provides a class method called set_locking_column:
class Client < ActiveRecord::Base
set_locking_column :lock_client_column
end
10.2 Pessimistic Locking
Pessimistic locking uses a locking mechanism provided by the underlying底层database. Using lock when building a relation obtains an exclusive lock on the selected rows. Relations using lock are usually wrapped inside a transaction for preventing deadlock conditions.
在语句块的一个流程中使用lock通常是为了防止死锁情况出现。
For example:
Item.transaction do
i = Item.lock.first
i.name = ‘Jones’
i.save
end
The above session produces the following SQL for a MySQL backend:
You can also pass raw SQL to the lock method for allowing different types of locks. For example, MySQL has an expression called LOCK IN SHARE MODE where you can lock a record but still allow other queries to read it. To specify this expression just pass it in as the lock option:
SQL (0.2ms) BEGIN
Item Load (0.3ms) SELECT * FROM items
LIMIT 1 FOR UPDATE
Item Update (0.4ms) UPDATE items
SET updated_at
= ‘2009-02-07 18:05:56’, name
= ‘Jones’ WHERE id
= 1
SQL (0.8ms) COMMIT
You can also pass raw SQL to the lock method for allowing different types of locks. For example, MySQL has an expression called LOCK IN SHARE MODE where you can lock a record but still allow other queries to read it. To specify this expression just pass it in as the lock option:
Item.transaction do
i = Item.lock(“LOCK IN SHARE MODE”).find(1)
i.increment!(:views)
end
11 Joining Tables
Active Record provides a finder method called joins for specifying JOIN clauses短语on the resulting SQL. There are multiple ways to use the joins method.
11.1 Using a String SQL Fragment
You can just supply the raw SQL specifying the JOIN clause to joins:
Client.joins(‘LEFT OUTER JOIN addresses ON addresses.client_id = clients.id’)
This will result in the following SQL:
SELECT clients.* FROM clients LEFT OUTER JOIN addresses ON addresses.client_id = clients.id
11.2 Using Array/Hash of Named Associations
ThismethodonlyworkswithINNERJOIN.
Active Record lets you use the names of the associations defined on the model as a shortcut for specifying JOIN clause for those associations when using the joins method.
For example, consider the following Category, Post, Comments and Guest models:
class Category < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :category
has_many :comments
has_many :tags
end
class Comment < ActiveRecord::Base
belongs_to :post
has_one :guest
end
class Guest < ActiveRecord::Base
belongs_to :comment
end
class Tag < ActiveRecord::Base
belongs_to :post
end
Now all of the following will produce the expected join queries using INNER JOIN:
11.2.1 Joining a Single Association
Category.joins(:posts)
This produces:
SELECT categories.* FROM categories
INNER JOIN posts ON posts.category_id = categories.id
Or, in English: “return a Category object for all categories with posts”. Note that you will see duplicate categories if more than one post has the same category. If you want unique唯一的categories, you can use Category.joins(:post).select(“distinct(categories.id)”).
或者,用英语说:“返回所有posts的所有categories到Category对象”。请注意,如果超过一个post具有相同的categories,你会看到重复的categories。如果你想要唯一的categories,你可以使用Category.joins(:post).select(“distinct(categories.id)”)。
11.2.2 Joining Multiple Associations
Post.joins(:category, :comments)#两个参数之间是与关系
This produces:
SELECT posts.* FROM posts
INNER JOIN categories ON posts.category_id = categories.id
INNER JOIN comments ON comments.post_id = posts.id
Or, in English: “return all posts that have a category and at least one comment”. Note again that posts with multiple comments will show up multiple times.
11.2.3 Joining Nested Associations (Single Level)
Post.joins(:comments => :guest)
This produces:
SELECT posts.* FROM posts
INNER JOIN comments ON comments.post_id = posts.id
INNER JOIN guests ON guests.comment_id = comments.id
Or, in English: “return all posts that have a comment made by a guest.”
11.2.4 Joining Nested Associations (Multiple Level)
Category.joins(:posts => [{:comments => :guest}, :tags])
This produces:
SELECT categories.* FROM categories
INNER JOIN posts ON posts.category_id = categories.id
INNER JOIN comments ON comments.post_id = posts.id
INNER JOIN guests ON guests.comment_id = comments.id
INNER JOIN tags ON tags.post_id = posts.id
返回post的category,并且这个post至少有一个commit是guest提交而且这个post至少有一个tag。
11.3 Specifying Conditions on the Joined Tables
You can specify conditions on the joined tables using the regular Array and String conditions. Hashconditions provides a special syntax for specifying conditions for the joined tables:
你可以在joined tables 的时候通过使用正则数组和字符串条件(表达式)来指定条件。Hash条件提供了一个特殊的语法来指定joined tables的条件:
time_range = (Time.now.midnight – 1.day)..Time.now.midnight
Client.joins(:orders).where(‘orders.created_at’ => time_range)
An alternative and cleaner syntax is to nest the hash conditions:一个更简洁的替代语法是将条件嵌入Hash条件中。
time_range = (Time.now.midnight – 1.day)..Time.now.midnight
Client.joins(:orders).where(:orders => {:created_at => time_range})
This will find all clients who have orders that were created yesterday, again using a BETWEEN SQL expression.这将会查找所有的clients,它们都有orders并且,order在昨天创建,再一次使用BETWEEN SQL语句。
12 Eager Loading Associations
Eager loading快速导入is the mechanism for loading the associated records of the objects returned by Model.find using as few queries as possible.
Eager loading快速导入是一个导入记录的关系的机器,它通过Model.find返回对象,并使用尽可能少的查询。
N + 1 queries problem
Consider the following code, which finds 10 clients and prints their postcodes:
clients = Client.limit(10)
clients.each do |client|
puts client.address.postcode
end
This code looks fine at the first sight. But the problem lies within the total number of queries executed. The above code executes 1 ( to find 10 clients ) + 10 ( one per each client to load the address ) = 11 queries in total.上面的代码执行一次(找到10个clients)+10(每一个client导入地址)
Solution to N + 1 queries problem
Active Record lets you specify in advance all the associations that are going to be loaded. This is possible by specifying the includes method of the Model.find call. With includes, Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries.
Revisiting the above case, we could rewrite Client.all to use eager load addresses:
clients = Client.includes(:address).limit(10)
clients.each do |client|
puts client.address.postcode
end
The above code will execute just 2 queries, as opposed to 11 queries in the previous case:
上面的代码将执行2查询,而不是在以前的情况下,以11查询:
SELECT * FROM clients LIMIT 10
SELECT addresses.* FROM addresses
WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
12.1 Eager Loading Multiple Associations
Active Record lets you eager load any number of associations with a single Model.find call by using an array, hash, or a nested hash of array/hash with the includes method.
Active Record可以通过includes方法加上数组,hash或者使用数组和hash嵌套的hash字典调用单个Model.find方法快速导入任何数目的关系。
12.1.1 Array of Multiple Associations
Post.includes(:category, :comments)
This loads all the posts and the associated category and comments for each post.
12.1.2 Nested Associations Hash
Category.includes(:posts => [{:comments => :guest}, :tags]).find(1)
This will find the category with id 1 and eager load all of the associated posts以及快速导入分类为id为1的post, the associated posts’ tags and comments和post的tags和comments, and every comment’s guest association并且每个comments是由guest创建的.
12.2 Specifying Conditions on Eager Loaded Associations
Even though Active Record lets you specify conditions on the eager loaded associations just like joins, the recommended way is to use joins instead.
However if you must do this, you may use where as you would normally.
Post.includes(:comments).where(“comments.visible”, true)
This would generate a query which contains a LEFT OUTER JOIN whereas the joins method would generate one using the INNER JOIN function instead.
SELECT “posts”.“id” AS t0_r0, … “comments”.“updated_at” AS t1_r5 FROM “posts” LEFT OUTER JOIN “comments” ON “comments”.“post_id” = “posts”.“id” WHERE (comments.visible = 1)
If there was no where condition, this would generate the normal set of two queries.
If, in the case of this includes query, there were no comments for any posts, all the posts would still be loaded. Byusingjoins (an INNER JOIN), the join conditions must match, otherwise no records will be returned.
13 Scopes作用域
Scoping allows you to specify commonly-used ARel(Arel is a relational algebra engine for Ruby
) queries which can be referenced as method calls on the association objects or models. With these scopes, you can use every method previously covered such as where, joins and includes. All scope methods will return an ActiveRecord::Relation object which will allow for further methods (such as other scopes) to be called on it.
作用域允许你指定通常使用的Arel(Arel是一个用于Ruby的关系代数引擎)查询它可以在对象或模型调用方法的时候被引用。通过这些作用域,你可以使用比如where,joins和includes这些方法覆盖以前的(方法)。所有的scope方法将会返回一个 ActiveRecord::Relation对象它将允许一些以后的方法(例如其他的作用域)被其调用。
To define a simple scope, we use the scope method inside the class, passing the ARel query that we’d like run when this scope is called:
class Post < ActiveRecord::Base
scope :published, where(:published => true)
end
Just like before, these methods are also chainable可链式的:
class Post < ActiveRecord::Base
scope :published, where(:published => true).joins(:category)
end
Scopes are also chainable within scopes:作用域对作用域中也是可链式的:
class Post < ActiveRecord::Base
scope :published, where(:published => true)
scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0))
end
To call this published scope we can call it on either the class:调用这个published作用域我们可以在类中调用:
Post.published => [published posts]
Or on an association consisting of Post objects:或者是在组成Post对象的关系(成员)中调用:
category = Category.first
category.posts.published => [published posts belonging to this category]
13.1 Working with times工作中的时间(字段)
If you’re working with dates or times within scopes, due to how they are evaluated, you will need to use a lambda so that the scope is evaluated every time.
如果你在工作中遇到时间(字段)或者包含有时间(字段)的作用域,由于他们的求值方式,你将需要使用一个匿名函数来使得每次调用域都会计算时间的值。
class Post < ActiveRecord::Base
scope :last_week, lambda { where(“created_at < ?”, Time.zone.now ) }
end
Without the lambda, this Time.zone.now will only be called once.
13.2 Passing in arguments
When a lambda is used for a scope, it can take arguments:
class Post < ActiveRecord::Base
scope :1_week_before, lambda { |time| where(“created_at < ?”, time)
end
This may then be called using this:
Post.1_week_before(Time.zone.now)
However, this is just duplicating复制 the functionality功能 that would be provided to you by a class method.然而,这样仅仅通过一个类方法提供给来复制(使用)这个功能。
class Post < ActiveRecord::Base
def self.1_week_before(time)
where(“created_at < ?”, time)
end
end
Using a class method is the preferred way to accept arguments for scopes. These methods will still be accessible访问 on the association objects:
使用一个类方法是一个完美的方法来让scopes获取参数。这个方法仍然访问objects的association。
category.posts.1_week_before(time)
13.3 Working with scopes使用作用域来工作
Where a relational object is required, the scoped method may come in handy. This will return an ActiveRecord::Relation object which can have further scoping applied to it afterwards. A place where this may come in handy is on associations
client = Client.find_by_first_name(“Ryan”)
orders = client.orders.scoped
With this new orders object, we are able to ascertain that this object can have more scopes applied to it. For instance, if we wanted to return orders only in the last 30 days at a later point.
orders.where(“created_at > ?”, 30.days.ago)
13.4 Applying a default scope
If we wish for a scope to be applied across all queries所有查询 to the model we can use the default_scope method within the model itself.
class Client < ActiveRecord::Base
default_scope where(“removed_at IS NULL”)
end
When queries are executed on this model, the SQL query will now look something like this:
SELECT * FROM clients WHERE removed_at IS NULL
13.5 Removing all scoping
If we wish to remove scoping for any reason we can use the unscoped method. This is especially useful if a default_scope is specified in the model and should not be applied for this particular query.
Client.unscoped.all
This method removes all scoping and will do a normal query on the table.
14 Dynamic Finders动态查询
For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called first_name on your Client model for example, you get find_by_first_name and find_all_by_first_name for free from Active Record. If you have a locked field on the Client model, you also get find_by_locked and find_all_by_locked methods.
在你表中定义的每个field(通常被看着一个属性), Active Record都提供了一个finder方法。例如,如果你有一个field名叫first_name在你的Client模型中,你会免费的从Active Record 得到find_by_first_name和find_all_by_first_name方法。如果你在Client模型中有一个locked field,你也会获得find_by_locked和find_all_by_locked方法。
You can also use find_last_by_* methods which will find the last record matching your argument.
你也可以使用find_last_by_*方法,它将会查找在机器中匹配你的参数的最后的记录。
You can specify an exclamation point (!) on the end of the dynamic finders to get them to raise an ActiveRecord::RecordNotFound error if they do not return any records, like Client.find_by_name!(“Ryan”)
你也可以在动态finders末尾指定一个感叹号来来的到查询结果或者如果他们没有返回任何参数则抛出一个 ActiveRecord::RecordNotFound错误。
If you want to find both by name and locked, you can chain these finders together by simply typing and between the fields. For example, Client.find_by_first_name_and_locked(“Ryan”, true).
Up to and including Rails 3.1, when the number of arguments passed to a dynamic finder method is lesser than the number of fields, say Client.find_by_name_and_locked(“Ryan”), the behavior is to pass nil as the missing argument.如果参数少于动态finder的fields的数目,习惯上是对缺少的fields传递一个nil。 This is unintentional and this behavior will be changed in Rails 3.2 to throw an ArgumentError.这样做是没有意义的并且这个约定将会在Rails 3.2中修改成抛出一个 ArgumentError错误。
There’s another set of dynamic finders that let you find or create/initialize objects if they aren’t found. These work in a similar fashion to the other finders and can be used like find_or_create_by_first_name(params[:first_name]). Using this will first perform a find and then create if the find returns nil. The SQL looks like this for Client.find_or_create_by_first_name(“Ryan”):
SELECT * FROM clients WHERE (clients.first_name = ‘Ryan’) LIMIT 1
BEGIN
INSERT INTO clients (first_name, updated_at, created_at, orders_count, locked)
VALUES(‘Ryan’, ‘2008-09-28 15:39:12’, ‘2008-09-28 15:39:12’, 0, ‘0’)
COMMIT
irb(main):134:0> p=Post.find_or_create_by_name_and_title_and_content(‘1111111’,‘111111111’,‘11111111’)
Post Load (0.4ms) SELECT “posts”.* FROM “posts” WHERE “posts”.“title” = ‘111111111’ AND “posts”.“content” = ‘11111111’ AND “posts”.“name” = ‘1111111’ LIMIT 1
SQL (429.0ms) INSERT INTO “posts” (“content”, “created_at”, “name”, “title”, “updated_at”) VALUES (?, ?, ?, ?, ?) [[“content”, “11111111”], [“created_at”, Fri, 02 Dec 2011 08:42:39 UTC +00:00], [“name”, “1111111”], [“title”, “111111111”], [“updated_at”, Fri, 02 Dec 2011 08:42:39 UTC +00:00]]
=> #<Post id: 5, name: “1111111”, title: “111111111”, content: “11111111”, created_at: “2011-12-02 08:42:39”, updated_at: “2011-12-02 08:42:39”>
find_or_create’s sibling, find_or_initialize, will find an object and if it does not exist will act similarly to calling new with the arguments you passed in. For example:
client = Client.find_or_initialize_by_first_name(‘Ryan’)
will either assign an existing client object with the name “Ryan” to the client local variable, or initialize a new object similar to calling Client.new(:first_name => ‘Ryan’). From here, you can modify other fields in client by calling the attribute setters on it: client.locked = true and when you want to write it to the database just call save on it.
15 Finding by SQL
If you’d like to use your own SQL to find records in a table you can use find_by_sql. The find_by_sql method will return an array of objects even if the underlying query returns just a single record. For example you could run this query:
Client.find_by_sql(“SELECT * FROM clients
INNER JOIN orders ON clients.id = orders.client_id
ORDER clients.created_at desc”)
find_by_sql provides you with a simple way of making custom calls to the database and retrieving instantiated objects.
16 select_all
find_by_sql has a close relative called connection#select_all. select_all will retrieve objects from the database using custom SQL just like find_by_sql but will not instantiate them. Instead, you will get an array of hashes where each hash indicates指示 a record.
Client.connection.select_all(“SELECT * FROM clients WHERE id = ‘1’”)
15和16 综合比较
irb(main):174:0> p=Post.find_by_sql(“SELECT * FROM posts WHERE (posts.id = 1)”) Post Load (1.7ms) SELECT * FROM posts WHERE (posts.id = 1)
=> [#<Post id: 1, name: “name111111111111111”, title: “title111111111111111”, content: “content111111111111111”, created_at: “2011-12-02 08:56:33”, updated_at: “2011-12-02 08:56:33”>]
irb(main):175:0> puts p
#<Post:0xb6bf4d64>
=> nil
irb(main):176:0> p=Post.connection.select_all(“SELECT * FROM posts WHERE id = ‘1’”)
(0.6ms) SELECT * FROM posts WHERE id = ‘1’
=> [{“name”=>“name111111111111111”, “created_at”=>“2011-12-02 08:56:33.397313”, “title”=>“title111111111111111”, “updated_at”=>“2011-12-02 08:56:33.397313”, “id”=>1, “content”=>“content111111111111111”}]
irb(main):177:0> puts p
namename111111111111111created_at2011-12-02 08:56:33.397313titletitle111111111111111updated_at2011-12-02 08:56:33.397313id1contentcontent111111111111111
=> nil
17 Existence of Objects目标是否存在
If you simply want to check for the existence of the object there’s a method called exists?. This method will query the database using the same query as find, but instead of returning an object or collection of objects it will return either true or false.
Client.exists?(1)
The exists? method also takes multiple ids, but the catch is that it will return true if any one of those records exists.
Client.exists?(1,2,3)
# or
Client.exists?([1,2,3])
It’s even possible to use exists? without any arguments on a model or a relation.
Client.where(:first_name => ‘Ryan’).exists?
The above returns true if there is at least one client with the first_name ‘Ryan’ and false otherwise.
Client.exists?
The above returns false if the clients table is empty and true otherwise.
You can also use any? and many? to check for existence on a model or relation.
# via a model
Post.any?
Post.many?
# via a named scope
Post.recent.any?
Post.recent.many?
# via a relation
Post.where(:published => true).any?
Post.where(:published => true).many?
# via an association
Post.first.categories.any?#第一个post的category是否存在
Post.first.categories.many?
18 Calculations
This section uses count as an example method in this preamble, but the options described apply to all sub-sections.
All calculation methods work directly on a model:
Client.count
# SELECT count(*) AS count_all FROM clients
Or on a relation:
Client.where(:first_name => ‘Ryan’).count
# SELECT count(*) AS count_all FROM clients WHERE (first_name = ‘Ryan’)
Post.first.tags.count
You can also use various finder methods on a relation for performing complex calculations:
Client.includes(“orders”).where(:first_name => ‘Ryan’, :orders => {:status => ‘received’}).count
#Post.includes(‘tags’).where(:id=>“2”,:tags=>{:name=>‘123’}).count
Which will execute:
SELECT count(DISTINCT clients.id) AS count_all FROM clients
LEFT OUTER JOIN orders ON orders.client_id = client.id WHERE
(clients.first_name = ‘Ryan’ AND orders.status = ‘received’)
18.1 Count
If you want to see how many records are in your model’s table you could call Client.count and that will return the number. If you want to be more specific and find all the clients with their age present in the database you can use Client.count(:age).
For options, please see the parent section, Calculations.
18.2 Average平均值
If you want to see the average of a certain number in one of your tables you can call the average method on the class that relates to the table. This method call will look something like this:
Client.average(“orders_count”)
irb(main):215:0> puts Post.average(‘created_at’)
(1.4ms) SELECT AVG(“posts”.“created_at”) AS avg_id FROM “posts”
2011.0
18.3 Minimum
If you want to find the minimum value of a field in your table you can call the minimum method on the class that relates to the table. This method call will look something like this:
Client.minimum(“age”)
For options, please see the parent section, Calculations.
18.4 Maximum
If you want to find the maximum value of a field in your table you can call the maximum method on the class that relates to the table. This method call will look something like this:
Client.maximum(“age”)
For options, please see the parent section, Calculations.
18.5 Sum
If you want to find the sum of a field for all records in your table you can call the sum method on the class that relates to the table. This method call will look something like this: