4. 大手町.rb #11 「Active Record クエリインタフェースについて」
今日のテーマ
Active Record クエリインタフェースについて
Ruby on Rails を使うなら、みんな使ってる?
とはいえ、時間をとって学ぶことがあまりない
というのも事実
Rails ガイドを熟読すればいいのですが、
なかなか上から読むのは大変です。
今日は一緒に学んでいきましょう
3
5. 大手町.rb #11 「Active Record クエリインタフェースについて」
前提となるモデル
Rails ガイドの内容をベースに紹介します。
もろもろ、Rails の命名規則に従っている前提です。
id が主キー
4
class Role < ApplicationRecord
has_and_belongs_to_many :clients
end
class Address < ApplicationRecord
belongs_to :client
end
class Order < ApplicationRecord
belongs_to :client, counter_cache: true
end
class Client < ApplicationRecord
has_one :address
has_many :orders
has_and_belongs_to_many :roles
end
19. 大手町.rb #11 「Active Record クエリインタフェースについて」
グループ(カウント) 18
# 日付ごとの合計金額
# Order の配列が返る
Order.select("date_trunc(created_at, 'date') as ordered_date,
sum(price) as total_price").group("date(created_at)")
# (SQL) SELECT date(created_at) as ordered_date, sum(price) as
total_price FROM orders GROUP BY date(created_at)
# ステータスごとの件数
# ステータスがキーで、件数が値のハッシュが返る
Order.group(:status).count
# => { 'awaiting_approval' => 7, 'paid' => 12 }
# (SQL) SELECT COUNT (*) AS count_all, status AS status
FROM "orders" GROUP BY status
20. 大手町.rb #11 「Active Record クエリインタフェースについて」
グループ(平均、最大、最小、合計) 19
# count 以外にも average、maximum、minimum、sum がある
# ステータスがキーで、値が平均、最大等のハッシュが返る
Order.group(:status).average(:price)
Order.group(:status).maximum(:price)
Order.group(:status).minimum(:price)
Order.group(:status).sum(:price)
# 複数の列で集約するときは、Ruby on Rais 4系では group を2回書く
# キーが client_id と status の配列、値が price の合計値のハッシュが返る
Order.group(:client_id).group(:status).sum(:price)
# Ruby on Rails 5系では下記のように書ける
# Order.group(:client_id, :status).sum(:price)
21. 大手町.rb #11 「Active Record クエリインタフェースについて」
グループ having 20
# having を使うと、集約関数の計算結果でフィルタできる
Order.select("date(created_at) as ordered_date, sum(price) as
total_price").group("date(created_at)").having("sum(price) > ?", 100)
# (SQL) SELECT date(created_at) as ordered_date, sum(price) as
total_price FROM orders GROUP BY date(created_at) HAVING sum(price) >
100
## group と having を複雑に組み合わせた例
### 3件よりも件数がある場合だけを返す
Order.group(:client_id).group(:status).having("count(1) >
3").sum(:price)
22. 大手町.rb #11 「Active Record クエリインタフェースについて」
条件を上書きする 1/2
unscope: 条件を取り除くことができる
only: 使用する条件を特定のものに限定できる
reorder: デフォルトスコープの並び順を上書きできる
21
class Article < ApplicationRecord
has_many :comments, -> { order('posted_at DESC') }
end
article = Article.find(10)
article.comments.reorder('name')
# (SQL) SELECT * FROM comments WHERE article_id = 10 ORDER BY name
article.comments.order('name')
# (SQL) SELECT * FROM comments WHERE article_id = 10 ORDER BY
posted_at DESC, name
article.comments
# (SQL) SELECT * FROM comments WHERE article_id = 10 ORDER BY name
23. 大手町.rb #11 「Active Record クエリインタフェースについて」
条件を上書きする 2/2
reverse_order:並び順を逆にする
rewhere: 既存の where 条件を上書きする
22
Article.where(trashed: true).rewhere(trashed: false)
# (SQL) SELECT * FROM articles WHERE `trashed` = 0
Article.where(trashed: true).where(trashed: false)
# (SQL) SELECT * FROM articles WHERE `trashed` = 1 AND `trashed` = 0
24. 大手町.rb #11 「Active Record クエリインタフェースについて」
none: Nullリレーション
none メソッドは、結果として空のリレーションを返す
リレーションを返すことが必要でかつ、結果を返したく
ない場合にベンリ。
23
# visible_articles メソッドはリレーションを返すことが期待されている
@articles = current_user.visible_articles.where(name: params[:name])
def visible_articles
case role
when 'Country Manager'
Article.where(country: country)
when 'Reviewer'
Article.published
when 'Bad User'
Article.none # => []またはnilを返すと、このコード例では呼び出し元のコード
を壊してしまう
end
end
27. 大手町.rb #11 「Active Record クエリインタフェースについて」
悲観的ロック
データベースが提供するロック機構を使う
ロックが取得できない場合は、ロックできるまで待つ
多くのDBには行単位でロックできる
with_lockメソッドを使うのが一般的
26
i = Item.first
i.with_lock do
i.name = 'Jones'
i.save!
end
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
28. 大手町.rb #11 「Active Record クエリインタフェースについて」
テーブルの結合(SQLフラグメント文字列)
SQL フラグメント文字列を使う方法
27
Author.joins("INNER JOIN posts ON posts.author_id = authors.id AND
posts.published = 't'")
SELECT authors.* FROM authors INNER JOIN posts ON posts.author_id =
authors.id AND posts.published = 't'
29. 大手町.rb #11 「Active Record クエリインタフェースについて」
ここからのモデル 28
class Category < ApplicationRecord
has_many :articles
end
class Article < ApplicationRecord
belongs_to :category
has_many :comments
has_many :tags
end
class Comment < ApplicationRecord
belongs_to :article
has_one :guest
end
class Guest < ApplicationRecord
belongs_to :comment
end
class Tag < ApplicationRecord
belongs_to :article
end
30. 大手町.rb #11 「Active Record クエリインタフェースについて」
テーブルの結合(joins) 29
Category.joins(:articles)
SELECT categories.* FROM categories
INNER JOIN articles ON articles.category_id = categories.id
Article.joins(:category, :comments)
SELECT articles.* FROM articles
INNER JOIN categories ON categories.id = articles.category_id
INNER JOIN comments ON comments.article_id = articles.id
Article.joins(comments: :guest)
SELECT articles.* FROM articles
INNER JOIN comments ON comments.article_id = articles.id
INNER JOIN guests ON guests.comment_id = comments.id
31. 大手町.rb #11 「Active Record クエリインタフェースについて」
テーブルの結合(複数かつネストがある場合)
複雑かつネストがある場合
結合されたテーブルについて条件を書きたい場合
30
Category.joins(articles: [{ comments: :guest }, :tags])
SELECT categories.* FROM categories
INNER JOIN articles ON articles.category_id = categories.id
INNER JOIN comments ON comments.article_id = articles.id
INNER JOIN guests ON guests.comment_id = comments.id
INNER JOIN tags ON tags.article_id = articles.id
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.joins(:orders).where(orders: { created_at: time_range })
32. 大手町.rb #11 「Active Record クエリインタフェースについて」
テーブルの結合(左外部結合)
左外部結合を使った例
joins だとゼロ件のときをうまく扱えないが、
left_outer_joins を使うことで正しくゼロ件も含めて
カウントできる
31
Author.left_outer_joins(:posts).distinct.select('authors.*,
COUNT(posts.*) AS posts_count').group('authors.id')
SELECT DISTINCT authors.*, COUNT(posts.*) AS posts_count
FROM "authors"
LEFT OUTER JOIN posts ON posts.author_id = authors.id
GROUP BY authors.id
33. 大手町.rb #11 「Active Record クエリインタフェースについて」
N + 1 問合せ問題と includes
素朴に書くと、 N+1回(下記の場合、10+1 = 11回)の
DB 問合せが実行される
includes を使うことで、2回に減らすことができる
32
clients = Client.limit(10)
clients.each do |client|
puts client.address.postcode
end
clients = Client.includes(:address).limit(10)
clients.each do |client|
puts client.address.postcode
end
SELECT * FROM clients LIMIT 10
SELECT addresses.* FROM addresses
WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
34. 大手町.rb #11 「Active Record クエリインタフェースについて」
複雑な includes
複数のアソシエーションの指定
ハッシュを使うことで、ネストにも対応可能、配列と組合せも OK
33
Article.includes(:category, :comments)
Category.includes(articles: [{ comments: :guest }, :tags]).find(1)