今天我们会用部落格跟使用者的关系来讲解关联,首先先做设定,部落格跟使用者的关系为
class Blog < ApplicationRecord
belongs_to :user
end
class User < ApplicationRecord
has_many :blogs
end
今天我们的主题是
一对一
& 一对多
之间的关系join
, preload
, includes
, left_joins
, eager_load
, sub query
的用法在关联式资料库的关系里面,一共有1对1
、1对多
、多对多
三种关联,而我们先从1对多
开始破题
我们看id=3
的使用者对应的部落格
user = User.find 3
user.blogs
#=> Blog Load (1.9ms) SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`user_id` = 3 LIMIT 11
#======== 上面的意思与下面的相同
Blog.where(user: user)
#=> Blog Load (1.9ms) SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`user_id` = 3 LIMIT 11
Blog.where(user_id: user.id)
#=> Blog Load (1.9ms) SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`user_id` = 3 LIMIT 11
第1篇部落格对应的使用者
blog = Blog.first
> blog.user
#=> SELECT `blogs`.* FROM `blogs` ORDER BY `blogs`.`id` ASC LIMIT 1
#=> SELECT `users`.* FROM `users` WHERE `users`.`id` = 3 LIMIT 1
当我们要在id=3的使用者创建一笔新的部落格
user = User.find 3
#==== 有bang! ➡️ 引发错误
user.blogs.create!(title: '标题4', content: '内文4')
#=> ActiveRecord::RecordInvalid (校验失败: Genre 不能为空白)
#==== 没有bang! ➡️ 不会引发错误
user.blogs.create(title: '标题4', content: '内文4')
#=> #<Blog id: nil, title: "标题4", content: "内文4", user_id: 3, genre: nil, created_at: nil, updated_at: nil>
#====== 创建成功
user.blogs.create!(title: '标题4', content: '内文4', genre: :technology)
# (16.8ms) BEGIN
#==== Blog Create (171.3ms) INSERT INTO `blogs` (`title`, `content`, `user_id`, `genre`, `created_at`, `updated_at`) VALUES ('标题4', '内文4', 3, 2, '2021-0915:25:27.228691', '2021-09-24 15:25:27.228691')
# (76.9ms) COMMIT
=> #<Blog id: 4, title: "标题4", content: "内文4", ...">
我们举一个1对1的例子,如订单跟发票的关系。
class Invoice < ApplicationRecord
belongs_to :order
end
class Order < ApplicationRecord
has_one :invoice
end
当我们要建立发票的写法时,1对1
的写法会不一样,1对1
写法为加入前缀 create_
order = Order.first
#==== correct
order.create_invoice!(customer_name: 'han', customer_phone: '0912559059', customer_email: '[email protected]', price: 100)
#==== incorrect
order.invoices.create!(customer_name: 'han', customer_phone: '0912559059', customer_email: '[email protected]', price: 100)
若我们要找订单的发票,或者是找发票的订单,可以用以下写法
order = Order.first
order.invoice
#=> SELECT `invoices`.* FROM `invoices` WHERE `invoices`.`order_id` = 2 LIMIT 1
invoice = Invoice.first
invoice.order
#=> SELECT `orders`.* FROM `orders` WHERE `orders`.`id` = 1106 LIMIT 1
想到自己曾经在面试的时候,有被询问到关於1对1关联,两者之间有何区别?
order.invoice
invoice.order
我们看SQL Statement
,可以看出有一个不同的地方在於invoice
已经存取了order.id
,因此可以直接用id=1106
去找到订单,而order
要找invoice
必须用invoice_id
去查找。
下列举若干比较复杂的例子说明
当我们要查有哪些使用者写部落格文章
#===== 找出有写部落格的使用者
User.joins(:blogs).where('blogs.id IS NOT NULL')
#=> User Load (6.0ms) SELECT `users`.* FROM `users` INNER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE (blogs.id IS NOT NULL) LIMIT 11
使用Join
会不小心捞到多笔资料。以下面的例子来说,由於有三篇文章都关联到某一个作者,因此会捞到3笔同一个作者的资料
User.joins(:blogs).where('blogs.id IS NOT NULL').count #=> 3
User.joins(:blogs).where('blogs.id IS NOT NULL').uniq.count #=> 1
要如何解决这个方式?除了uniq
,我们可以使用distinct
,distinct
为Orm
的语法
User.joins(:blogs).where('blogs.id IS NOT NULL').distinct
#=> SELECT DISTINCT `users`.* FROM `users` INNER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE blogs.id IS NOT NULL LIMIT 11
我们看到ransack
在是以 SubQuery
来处理。明天会讲到ransack
User.ransack(blogs_id_not_null: true).result
#=> User Load (1.0ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IN (SELECT `blogs`.`user_id` FROM `blogs` WHERE `blogs`.`user_id` = `users`.`id` AND `blogs`.`id` IS NOT NULL) LIMIT 11
=> #<ActiveRecord::Relation [#<User id: 3, ...>]>
外面的 query
包覆里面的 query
的写法,统称sub query
-- first query
SELECT `blogs`.`user_id` FROM `blogs` WHERE `blogs`.`user_id` = `users`.`id` AND `blogs`.`id` IS NOT NULL
-- second query
SELECT `users`.* FROM `users` WHERE `users`.`id` IN ( FIRST_QUERY ) LIMIT 11
除了join
以外,还可以使用includes
。
User.includes(:blogs).where('blogs.id IS NOT NULL')
# User Load (8.3ms) SELECT `users`.* FROM `users` WHERE blogs.id IS NOT NULL
# ActiveRecord::StatementInvalid (Mysql2::Error: Unknown column 'blogs.id' in 'where clause')
User.includes(:blogs).reference(:blogs).where('blogs.id IS NOT NULL')
# SQL (5.8ms) SELECT DISTINCT `users`.`id` FROM `users` LEFT OUTER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE `blogs`.`id` IS NOT NULL LIMIT 11
# SQL (15.5ms) SELECT `users`.`id` AS t0_r0, ..., `blogs`.`id` AS t1_r0, ... FROM `users` LEFT OUTER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE (blogs.id IS NOT NULL) AND `users`.`id` = 3
#=> #<ActiveRecord::Relation [#<User id: 3, ...>]>
User.includes(:blogs).where.not(blogs: { id: nil })
# SQL (5.8ms) SELECT DISTINCT `users`.`id` FROM `users` LEFT OUTER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE `blogs`.`id` IS NOT NULL LIMIT 11
# SQL (15.5ms) SELECT `users`.`id` AS t0_r0, ..., `blogs`.`id` AS t1_r0, ... FROM `users` LEFT OUTER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE (blogs.id IS NOT NULL) AND `users`.`id` = 3
#=> #<ActiveRecord::Relation [#<User id: 3, ...>]>
我们可以先这看到原本预设为preload
方法的includes
,有时也可以适时转换成left outer join && distinct
此为关联搭配聚合aggregate
的实际范例,这里使用的是group
搭配having
User.joins(:blogs).where('count(blogs) > 0')
#===== ActiveRecord::StatementInvalid (Mysql2::Error: Invalid use of group function)
User.joins(:blogs).group('blogs.id')
# SELECT `users`.* FROM `users` INNER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` GROUP BY blogs.id
#
#=> #<ActiveRecord::Relation [
#<User id: 3, ...>,
#<User id: 3, ...>,
#<User id: 3, ...>,
#<User id: 3, ...]>
User.joins(:blogs).group('blogs.id').pluck(:id)
#=> [3, 3, 3, 3]
User.joins(:blogs).group('blogs.id').having('count(id) > 2')
#===== ActiveRecord::StatementInvalid (Mysql2::Error: Column 'id' in having clause is ambiguous)
#===== 错误: 捞不到 users,原因是 group 的地方写错了
User.joins(:blogs).group('blogs.id').having('count(blogs.id) > 2')
# SELECT `users`.* FROM `users` INNER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` GROUP BY blogs.id HAVING (count(users.id) > 2) LIMIT 11
#=> #<ActiveRecord::Relation []>
#===== 正确: 捞到 users
User.joins(:blogs).group('users.id').having('count(blogs.id) > 2')
# SELECT `users`.* FROM `users` INNER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` GROUP BY users.id HAVING (count(blogs.id) > 2) LIMIT 11
#=> #<ActiveRecord::Relation [#<User id: 3, ...>]>
User.joins(:blogs).group('users.id').having('count(blogs.genre = ?) > 2', Blog.genres[:life])
# SELECT `users`.* FROM `users` INNER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE `users`.`banned` = FALSE GROUP BY users.id HAVING (count(blogs.genre = 0) > 2) LIMIT 11
#=> #<ActiveRecord::Relation [#<User id: 3, ...>]>
我们前面提到的 inner join,一共捞到了4笔资料。
User.joins(:blogs).where('blogs.id IS NOT NULL')
# User Load (6.0ms) SELECT `users`.* FROM `users` INNER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE (blogs.id IS NOT NULL) LIMIT 11
#=> #<ActiveRecord::Relation [
#<User id: 3, ...>,
#<User id: 3, ...>,
#<User id: 3, ...>,
#<User id: 3, ...>]>
前面我们提到,这种状况可以用 distinct
去除多余的资料,但没有交代会捞到4笔的原因。资料库一共有4篇id=3
的使用者写的文章,因此透过inner join
会捞到4笔。
(详情可以参考Inner Join)
users.id | blogs.id |
---|---|
3 | 1 |
3 | 2 |
3 | 3 |
3 | 4 |
资料库一共有4笔使用者id=3
的部落格,因此join
以後有4篇
将以上的inner join,改为left (outer) join,我们可以看到多捞了一笔id=1
的使用者
User.left_joins(:blogs).where('blogs.id IS NOT NULL')
# User Load (6.0ms) SELECT `users`.* FROM `users` LEFT OUTER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE (blogs.id IS NOT NULL) LIMIT 11
#=> #<ActiveRecord::Relation [
#<User id: 1, ...>,
#<User id: 3, ...>,
#<User id: 3, ...>,
#<User id: 3, ...>,
#<User id: 3, ...>]>
此外left_joins
也可以称为left_outer_joins
User.left_outer_joins(:blogs).where('blogs.id IS NOT NULL')
# User Load (6.2ms) SELECT `users`.* FROM `users` LEFT OUTER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE (blogs.id IS NOT NULL) LIMIT 11
捞到5笔资料的原因为资料库里面有2个使用者,除了上述的4笔部落格对应同个使用者以外,另外一名没有发文也会被left join
到,因此会有五笔。
(详情可以参考Left join)
users.id | blogs.id |
---|---|
3 | 1 |
3 | 2 |
3 | 3 |
3 | 4 |
1 |
资料库一共有4笔使用者id=3
的部落格,加上因为left join
的关系捞到了没有发文id=1
的使用者,因此join
以後有5篇
接着我们看类似的方法 ➡️ eager_load
User.eager_load(:blogs).where('blogs.id IS NOT NULL')
# SQL (34.7ms) SELECT DISTINCT `users`.`id` FROM `users` LEFT OUTER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE (blogs.id IS NOT NULL) LIMIT 11
# SQL (15.5ms) SELECT `users`.`id` AS t0_r0, ..., `blogs`.`id` AS t1_r0, ... FROM `users` LEFT OUTER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE blogs.id IS NOT NULL AND `users`.`id` = 3
# => #<ActiveRecord::Relation [#<User id: 3, ...>]>
有别於left_joins
,eager_loads
内建distinct
,因此不会找到重复的笔数。
跟Join不一样,preload
为预载入资料,而另外一个 Rails 工程师比较常看到的 includes
的预设动作也是预载入。
User.preload(:blogs)
# User Load (96.0ms) SELECT `users`.* FROM `users`
# Blog Load (28.8ms) SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`user_id` IN (1, 2, 3, 4, 5, 1151, 1152, 1153, 1154, 1155, 1156)
User.includes(:blogs)
# User Load (4.6ms) SELECT `users`.* FROM `users`
# Blog Load (13.1ms) SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`user_id` IN (1, 2, 3, 4, 5, 1151, 1152, 1153, 1154, 1155, 1156)
只不过比起 preload
,includes
来的还要更嫩Q
✅ 若要找使用者电子信相关键字含有k44
的资料
User.preload(:blogs).where("users.email like '%k44%'")
# User Load (11.7ms) SELECT `users`.* FROM `users` WHERE (users.email like '%k44%') LIMIT 11
# Blog Load (1.2ms) SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`user_id` IN (2093, 2094, 2095, 2096, 2097, 2098, 2099, 2100, 2101, 2102, 2103)
❌ 若要找部落格标题含有标题
关键字 ➡️ 错误原因为 users
资料表没有title
的栏位
User.preload(:blogs).where("blogs.title like '%标题%'")
# User Load (44.8ms) SELECT `users`.* FROM `users` WHERE blogs.title like '%标题%' LIMIT 11
# Traceback (most recent call last):
# ActiveRecord::StatementInvalid (Mysql2::Error: Unknown column 'blogs.title' in 'where clause')
❌ 使用includes
找部落格标题含有标题
关键字 ➡️ 错误原因,还是因为 users
资料表没有title
的栏位
User.includes(:blogs).where("blogs.title like '%标题%'")
# User Load (44.8ms) SELECT `users`.* FROM `users` WHERE blogs.title like '%标题%' LIMIT 11
# Traceback (most recent call last):
# ActiveRecord::StatementInvalid (Mysql2::Error: Unknown column 'blogs.title' in 'where clause')
⭐️ 补充一下,参考BigBinary文章,原本预期上述的sql statement
预期是可以执行成功,不过结果不行。
➡️ 似乎在有半sql
语法的就不能这样用,像如果我要找部落格标题等於标题1
时
#==== 半 sql 语法找不到
User.includes(:blogs).where("blogs.title = '标题1'")
# ActiveRecord::StatementInvalid (Mysql2::Error: Unknown column 'blogs.title' in 'where clause')
#==== 全 orm 语法找得到
User.includes(:blogs).where(blogs: {title: '标题1'})
# SELECT DISTINCT `users`.`id` FROM `users` LEFT OUTER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE `users`.`banned` = FALSE AND `blogs`.`title` = '标题1' LIMIT 11
# SQL (5.8ms) SELECT `users`.`id` AS t0_r0, ... FROM `users` LEFT OUTER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE `users`.`banned` = FALSE AND `blogs`.`title` = '标题1' AND `users`.`id` = 3
✅ 使用includes
找部落格标题含有标题
关键字
➡️ 使用 preload
会失败,因为 preload
只会预载入资料
User.preload(:blogs).references(:blogs).where("blogs.title like '%标题%'")
# User Load (3.0ms) SELECT `users`.* FROM `users` WHERE blogs.title like '%标题%' LIMIT 11
# Traceback (most recent call last):
# ActiveRecord::StatementInvalid (Mysql2::Error: Unknown column 'blogs.title' in 'where clause')
➡️ 使用 includes
会成功,因为 includes
可以使用joins
User.includes(:blogs).references(:blogs).where("blogs.title like '%标题%'")
# SQL (0.7ms) SELECT DISTINCT `users`.`id` FROM `users` LEFT OUTER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE `users`.`banned` = FALSE AND blogs.title like '%标题%' LIMIT 11
# SQL (15.5ms) SELECT `users`.`id` AS t0_r0, ..., `blogs`.`id` AS t1_r0, ... FROM `users` LEFT OUTER JOIN `blogs` ON `blogs`.`user_id` = `users`.`id` WHERE blogs.id IS NOT NULL AND `users`.`id` = 3
以下方的例子,看不出来id
是blogs
还是users
的,因此会跳此错误。
User.joins(:blogs).group('blogs.id').having('count(id) > 2')
#===== ActiveRecord::StatementInvalid (Mysql2::Error: Column 'id' in having clause is ambiguous)
通常会都是在关联资料表之後,才可能触发 Ambiguous Error
假设A表有B表的内容需要关联,则A.join(“LEFT_OUTER_JOIN B on …”).select(…).where(…欲搜寻条件…)
。A表跟B表有共同名字的栏位,假设叫status,若在where(…欲搜寻条件…)里面没有写status是哪一个栏位,就会造成上述的错误。
这种错误发生的机率还是会有,所以必须留意。曾经遇到的状况是有一段query写在scope里面,这段scope的某个撞名栏位,这栏位并没有指定是哪个资料表而造成错误。
这里用了很简单的例子介绍了很多种情境与用法,以及可能会发生的错误。
今天所介绍的东西,是面试中很常被拿来当作考题的情境,所以各位读者若可以的话,今天的文章提到的观念都要搞清楚喔!
实作上若要关联多个资料表,可以择优使用includes
搭配 references
,因为includes & references
的用法是最实用的。
>>: Day 15 : 笔记篇 02 — 使用「渐进式总结」来写笔记,逐步萃取出高含金量的知识内容
容器化是应用程序级别的虚拟化, 允许单个内核上有多个独立的用户空间实体, 而这些实体称为容器。 20...
大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 ...
递回 在函式里面可以呼叫函式本身 下面例子中,fun() 会先将参数 x 显示到视窗上,之後判断 x...
arrary很别好理解,一维arrary就是线,二维arrary就是面,三维arrary就是立方体,...
前面在讲要查询 commit 时要看他的 SHA-1 码,这个 SHA-1 到底是什麽!今天就来补充...