[Rails] Active Record Queries

资料来源:Rails Guide

Guide

-“查询”就是根据条件查找记录。
-“查询操作”多种多样:select, where, order, etc
-“方法链”:将一系列查询操作的方法通过.串链在一起
-“预载入”eager load通过加载关联对象,以减少查询次数
-如何检查查询的记录是否存在,或关联的对象是否存在

1. Why

为什么要使用关联?
-(1) 可增强代码可读性
-(2) 不依赖于特定的数据库
-(3) 嘿嘿最爽的,不用写复杂的 SQL
Tips: 某些特殊情况为了提高效率你还是得写 :(

2. What

查询 通过指定的条件得到你想要的结果

2.1 查询步骤

-(1) 将查询方法转化为 SQL 查询语句
-(2) 执行 SQL 查询语句,返回数据库中相关记录
-(3) 将相关记录转化为关联模型的 Ruby 对象
-(4) 执行after_findafter_initialize 回调

2.2 查询结果通常可分为两类

-(1) 若查找一条记录,返回的是模型对象的一个实例;
-(2) 若查找结果为多个模型对象实例的集合,返回ActiveRecord::Relation对象
Tip: 区分··关联查找返回的是ActiveRecord::Association::CollectionProxy对象
Tip: 若是通过关联在进行查询,返回ActiveRecord::AssociationRelation对象

2.2.1 返回单个对象

查询方法:find, find_by, take, first, last
Tip1: find找不到时抛出ActiveRecord::RecordNotFound,其他方法返回nil
Tip2: 若想让其他方法也抛出ActiveRecord::RecordNotFound,则使用爆破方法
Tip3: 这些方法也可以返回多条记录(数组),find后面跟一组id组成的数组可返回多个记录,其他方法加数量参数也可返回多个记录;注意find_by方法无法返回数组,属性对应数组的话只是增加查询所要满足的条件

2.2.2 返回多个对象(不含条件)

-(1) all载入所有记录到内存中,当数据量很大时会爆炸!
-(2) find_each将记录分成大小相等的块,一块一块地载入,代码块中处理单个记录
-(3) find_in_batch同上,分块载入,代码块中处理整个载入的块 (数组)
-分块载入方法添加选项 batch_size, start, finish 分别指定大小,开始结束位置

Tip: the 'find_each' and 'find_in_batch' methods are intended for use in the batch processing of large number of records that wouldn't fit in memory all at once. if you just need to loop over a thousand records the regular find methods (all, where...) are the preferred option.

2.2.3 返回多个对象(含条件)

2.2.3.1 where

条件可以是纯字符串 (存在SQL注入),数组,占位符,哈希键值对
一般情况下建议使用哈希的形式 (Rails way),若不能满足使用数组或占位符
哈希键值对的使用可分为三种情况: (1) Equality (2) Range (3) Subset
Tip: not跟在where后面是用表示不等于。

2.2.3.2 order

order对查找的结果排序 (:asc 升序, :desc 降序),默认为升序

2.2.3.3 select

默认情况下,会查询记录中所有字段,使用select可以指定要查找的字段
distinct query = Client.select(:id, :name).distinct <~> query.distinct(false)
Tip1: 后面接distinct方法查找单个记录,也可使用distinct(false)去除限制
Tip2: 当要查找的字段不存在时会抛出ActiveRecord::MissingAttributeError
Tip3: 若指定的字段不含id则关联将会失效
Tip4: 只查找个别字段会提高查询的效率

2.2.3.4 limit & offset

limit 来指定你想得到多少条记录
offset 来制定你想跳过多少条记录后开始查找
query = Client.limit(5).offset(30) # start with 31st get 5 client records

2.2.3.5 group & having

group 用来把属性相同的记录组织在一起,用于计算等
having 用来对使用group之后计算得到的值来限制条件

.group("date(created_at)").having("sum(price) > ?", 100)```
**Tip**: ```group```方法后面加```count```可以的到每组对应的记录个数 (Hash)
```Order.group(:status).count # => { 'awaiting_approval' => 7, 'paid' => 12 }```

#### 2.2.3.6 Overriding Conditions
```unscope``` 移除之前定义过的某个查询方法或某个条件
```Article.where('id > 10').limit(20).order('id asc').unscope(:order)```
```Article.where(id: 10, trashed: false).unscope(where: :id)```
```only``` 用来限制指定的查询方法是有效的,其他无效
```reorder``` 用来重新指定排序的字段
```reverse_order``` 与之前的排序相反
```rewhere``` 重新指定查询的条件

#### 2.2.3.7 none
只要查询方法链中包含```none```就返回空的查询结果 ```#<ActiveRecord::Relation []>```
```User.none if user.in_blacklist? # => #<ActiveRecord::Relation []> ```
**Tip**: 当你在特定情况下,如该用户被拉入黑名单,他的查询操作结果应该为空

#### 2.2.3.8 readonly
一个查询结果后添加```readonly```用来明确指出该记录是不可被修改的
若你强行修改它系统会抛出```ActiveRecord::ReadOnlyRecord```.

#### 2.2.3.9 joins
```joins``` 后面可直接加 SQL 语句来进行查询,或接关联
```joins``` 还可以连接多个关联,可以连接嵌套关联
```left_outer_joins``` 用来处理左连接,即使关联为空,也会放入查询结果
```ruby
Author.left_outer_joins(:posts).distinct
      .select('authors.*, COUNT(posts.*) AS posts_count')
      .group('authors.id')

=> (sql) SELECT DISTINCT authors.*, COUNT(posts.*) AS posts_count FROM "authors"
         LEFT OUTER JOIN posts ON posts.author_id = authors.id GROUP BY authors.id

3. Existence of Objects

比较这些方法:present?, exists?, any?, many?
-(1) 避免使用 present? 查看结果是否存在,效率较低:
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? [["user_id", 1]]
-(2) exists?, any?查看结果是否存在,效率较高 (查到一条记录存在就停止)
SELECT 1 AS one FROM "posts" WHERE "posts"."user_id"=? LIMIT ? [..., limit 1]
-(3) 使用 many? 查看结果是否有不止一个 (查询语句使用count):
SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = ? [["user_id", 1]]]
Tip: 和any?相比,exists?后面可以加条件参数,查看满足条件的记录是否存在
Client.exists?(name: ['Wende', 'Zhaobo', 'Junda'])
Client.where(name: ['Wende', 'Zhaobo', 'Junda']).exists?

4. Locking

给记录上锁是为了避免多人同时操作同一条记录可能产生的冲突。

4.1 Optimistic Locking

  • 乐观锁允许多个用户获取并操作同一记录,在操作完成并提交更新时通过查看该记录的版本来判断是否会发生冲突,若版本不一致抛出ActiveRecord::StaleObjectError并默认无视所更新的内容。对于失败的情况我们要处理这种异常,回滚,合并或是进行其他逻辑操作来弥补。(实现方式通过添加lock_version自动实现)
  • 乐观锁用来处理竞争关系,谁都可以获取记录的操作权,谁先完成,谁就可以更新记录,其他的更新将会在本次竞争中失败。所以少了等待的过程。

4.2 Pessimistic Locking

  • 悲观锁是当某个用户准备操作某条记录之前,给这个记录或表上锁,这样其他人在他没完成操作之前都无法打开这个锁,不存在竞争关系,必定是这个上锁的人完成更新操作。(悲观锁是数据库底层自带的锁机制,使用lock, lock!, with_lock来上锁)
  • 悲观锁可确保第一个获取记录操作权的人完成更新。并且避免大量用户操作同个记录中只有一人更新其他则做无用功的情况,在操作完成时更新比较记录锁的版本号也是要耗费性能的。

5. Eager Loading Association

通过使用includes方法解决 n +1 queries 问题
Category.includes(articles: [ { comments: :guest}, :tags ]).find(1)
上面的例子会找到 id 为 1 的类别记录,并且加载他所有关联的文章,文章相关的标签和评论,还有相关评论人,这样你在对这些数据进行操作时就不必在进行其他查询了

6. Scopes

Scope 可以让你把常用的关联查询语句构建成为一个类方法,供类和关联对象使用
它的返回值统一为relation这样便于调用链方法,就好像是自己构建的一个查询方法
Tips: scope 和类方法的区别?scope 返回一个ActiveRecord::Relation对象,即使当查询条件不满足时也会返回一个空的relation。反观类方法当结果不满足时你可以返回任何你想返回的值,如nil,而这就会导致查询链断裂,因为无法对一个空的对象在进行更多查询了。

7. Enums

enum availability: [:available, :unavailable]
使用enum宏方法来匹配类型为integer的字段,已达到枚举不同状态的效果。
使用它非常方便清晰,会自动拥有许多帮助方法,当情况更加复杂时可以使用状态机。

8. Dynamic Finders

find_by_first_name('wende'), find_by_last_name('lu'), find_by_age(25)
find_by_first_name_and_last_name_and_age('wende', 'lu', 25)

9. Method Chain

在方法链末尾使用where来过滤得到一组记录。
在方法链末尾使用find_by来过滤得到一条记录。

10. Find or Build a new Object

find_or_create_by, find_or_initialize_by 创建或初始化一条记录如果它不存在。
Tip1: 使用created_with或代码块在创建时赋值,注意在查询时该语句将会被忽略。
Tip2: 使用persisted?, new_record?来判断记录是否存在在数据库中。

11. Find by SQL

find_by_sql 使用自定义的 SQL 将查询结果初始化为对象存到数组中。
connection#select_all 自定义查询,获取由哈希键值对所组成的数组 (不初始化对象)。
pluck 接收一系列列名作为参数并且返回包含记录中该列的值的数组。
Client.pluck(:id) 等价于 Client.select(:id).map(&:id)
Client.pluck(:id, :name) 等价于 Client.select(:id, :name).map {|c| [c.id, c.name]}
Tips: 在一个已查询的relation上使用pluck并不会重新去查询数据库。
Tips: ids 相当于使用pluck获得所有主键id的数组。
Tips: select_all, pluck 直接把数据库查询的结果转化为数组,并不会创建模型对象。对于操作量打并且查询次数频繁的查询操作,可以提高效率。

12. Calculations

count, average, minimum, maximum, sum
Tips: count后可以添加列名,表示查找该列名存在的记录个数。
Tips: 这些计算操作都是在数据库层执行 SQL 计算得到的,需要对数据库操作。
为提高效率,我么希望先获得查询结果,转化为普通数组在进行操作,以减少查询。

13. Explain

relation使用explain方法可以查看该查询语句的详细信息,用于调优。
不同数据库都有自己的EXPLAIN方法,查看并掌握他们十分有用。
Tips: 对relation使用to_sql可以查看该查询方法链所生成的 SQL 语句。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容