Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Active record query interface

560 views

Published on

otemachi.rb #11

Published in: Technology
  • DOWNLOAD FULL. BOOKS INTO AVAILABLE FORMAT ......................................................................................................................... ......................................................................................................................... 1.DOWNLOAD FULL. PDF EBOOK here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL. EPUB Ebook here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL. doc Ebook here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL. PDF EBOOK here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL. EPUB Ebook here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL. doc Ebook here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... ......................................................................................................................... ......................................................................................................................... .............. Browse by Genre Available eBooks ......................................................................................................................... Art, Biography, Business, Chick Lit, Children's, Christian, Classics, Comics, Contemporary, Cookbooks, Crime, Ebooks, Fantasy, Fiction, Graphic Novels, Historical Fiction, History, Horror, Humor And Comedy, Manga, Memoir, Music, Mystery, Non Fiction, Paranormal, Philosophy, Poetry, Psychology, Religion, Romance, Science, Science Fiction, Self Help, Suspense, Spirituality, Sports, Thriller, Travel, Young Adult,
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • Be the first to like this

Active record query interface

  1. 1. Active Record の クエリインタフェースについて 2018/11/21 大手町.rb #11
  2. 2. 大手町.rb #11 「Active Record クエリインタフェースについて」 1自己紹介 Tomoya Kawanishi a.k.a. @cuzic エネチェンジ株式会社 チーフエンジニア 電力会社、ガス会社を切り替えるなら、エネチェンジ経由で! 一般家庭も!法人も! Ruby関西の中の人 発表者として登壇くださる方、あとで声かけください。 第84回 Ruby関西勉強会 12月1日(土) 大手町.rb の中の人 毎月 大手町.rb の開催を予定 東京駅、各線大手町駅から直結! Ruby の初級者がメインターゲット
  3. 3. 大手町.rb #11 「Active Record クエリインタフェースについて」 Disclaimer 大手町.rb は祝!11回! 人数がとても増えてきました! 大手町.rb は(比較的)初級者向けの勉強会 とはいえ、初級者向けのままだと、ネタ切れしちゃう 同じネタをリピートするか、レベルアップするか。 大手町.rb はレベルアップしていく方向! 今のオーディエンスを大切にしていく! 2
  4. 4. 大手町.rb #11 「Active Record クエリインタフェースについて」 今日のテーマ Active Record クエリインタフェースについて Ruby on Rails を使うなら、みんな使ってる? とはいえ、時間をとって学ぶことがあまりない というのも事実 Rails ガイドを熟読すればいいのですが、 なかなか上から読むのは大変です。 今日は一緒に学んでいきましょう 3
  5. 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
  6. 6. 大手町.rb #11 「Active Record クエリインタフェースについて」 find: 主キーによるレコードの取得 find :主キーにマッチするオブジェクトを取得 複数レコードを同時に取得できる 5 clients = Client.find(10) #=> #<Client id: 10, first_name: "Ryan"> SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1 clients = Client.find([1, 10]) # Client.find(1, 10) でも同じ結果 # => [ # <Client id: 1, first_name: "Lifo">, # <Client id: 10, first_name: "Ryan"> # ] SELECT * FROM clients WHERE (clients.id IN (1,10)) 対応するレコードがないと、 ActiveRecord::RecordNotFound 例外が発生する # Client.find([1, 2], [3, 4]) のような書き方もできる
  7. 7. 大手町.rb #11 「Active Record クエリインタフェースについて」 take: 任意のレコードの取得 take :どのレコードか指定せず、1レコード取得 take(2) で最大2個のレコードを取得 モデルに1つもレコードがない場合は nil を返す 6 Client.take #=> #<Client id: 1, first_name: "Lifo">, SELECT * FROM clients LIMIT 1 clients = Client.take(2) # Client.find(1, 10) でも同じ結果 # => [ # #<Client id: 1, first_name: "Lifo">, # #<Client id: 220, first_name: "Sara"> # ] SELECT * FROM clients LIMIT 2 take! を使うと、マッチしない場合、ActiveRecord::RecordNotFound 例外が発生する
  8. 8. 大手町.rb #11 「Active Record クエリインタフェースについて」 first:最初のレコードを取得 first:主キー順で最初のレコードを取得 デフォルトスコープがあれば、 デフォルトスコープで指定された順序で最初 first(3) で3個のレコードを取得できる 7 Client.first #=> #<Client id: 1, first_name: "Lifo">, SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 clients = Client.first(3) # => [ # #<Client id: 1, first_name: "Lifo">, # #<Client id: 2, first_name: "Fifo">, # #<Client id: 3, first_name: "Filo"> # ] SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3 order を使って順序を変更すると、first はその順で最初のレコードを返す
  9. 9. 大手町.rb #11 「Active Record クエリインタフェースについて」 last:最後のレコードを取得 last:主キー順で最後のレコードを取得 デフォルトスコープがあれば、 デフォルトスコープで指定された順序で最後 last(3) で3個のレコードを取得できる 8 Client.last #=> #<Client id: 221, first_name: "Russel"> SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 clients = Client.last(3) # => [ # #<Client id: 219, first_name: "James">, # #<Client id: 220, first_name: "Sara">, # #<Client id: 221, first_name: "Russel"> # ] SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3 order を使って順序を変更すると、last はその順で最後のレコードを返す
  10. 10. 大手町.rb #11 「Active Record クエリインタフェースについて」 find_by:マッチする最初のレコードを取得 find_by:与えられた条件にマッチする最初のレコード 9 Client.find_by(first_name: 'Lifo') #=> #<Client id: 1, first_name: "Lifo"> # Client.where(first_name: 'Lifo').take と同じ意味 SELECT * FROM clients WHERE (clients.first_name = 'Lifo') LIMIT 1 find_by! を使うと、マッチするレコードが見つからなかった場合に、 ActiveRecord::RecordNotFound例外が発生する
  11. 11. 大手町.rb #11 「Active Record クエリインタフェースについて」 find_each : バッチ処理でメモリを節約 多数のレコードに対して反復処理するとき、 素朴に実装すると、メモリが大量に必要となる find_each を使うとメモリを節約できる ただし、数千件程度であれば、素朴な実装でも問題ない 10 # User.all のためのメモリを消費する User.all.each do |user| NewsMailer.weekly(user).deliver_now end # レコードを 1,000件ずつ取得する User.find_each do |user| NewsMailer.weekly(user).deliver_now end # レコードを 500件ずつ、id=2000 から id=10000 番まで順に処理 User.find_each(batch_size: 500, start: 2000, finish: 10000) do |user| NewsMailer.weekly(user).deliver_now end
  12. 12. 大手町.rb #11 「Active Record クエリインタフェースについて」 find_in_batches: レコードの配列でバッチ処理 find_each は省メモリだが、使う側は1件ずつ find_in_batches では使う側も1度に 1000件処理できる ユースケース 複数のワーカスレッド、ジョブキューなどを使っている 各ワーカスレッドに 1,000件ずつ投入するときなどにベンリ 11 # 1回あたりadd_invoicesに納品書1000通の配列を渡す Invoice.find_in_batches do |invoices| export.add_invoices(invoices) end
  13. 13. 大手町.rb #11 「Active Record クエリインタフェースについて」 ハッシュを使用した条件 1/2 where はハッシュを使うと簡潔に書けて読みやすい 12 # 等値条件 Client.where(locked: true) # (SQL) SELECT * FROM clients WHERE (clients.locked = 1) # belongs_to リレーションシップを使った関連付け Article.where(author: author) # where(author_id: author.id) と同じ意味になる # id は省略でき、簡潔に書ける # 範囲条件 Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight) # (SQL) SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
  14. 14. 大手町.rb #11 「Active Record クエリインタフェースについて」 ハッシュを使用した条件 2/2 .or の使い方が覚えにくい。 必要な都度検索し、調べれば OK 13 # サブセット条件 Client.where(orders_count: [1,3,5]) # (SQL) SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5)) # NOT条件 Client.where.not(locked: true) # OR条件 Client.where(locked: true). or(Client.where(orders_count: [1,3,5])) # (SQL) SELECT * FROM clients WHERE (clients.locked = 1 OR clients.orders_count IN (1,3,5))
  15. 15. 大手町.rb #11 「Active Record クエリインタフェースについて」 引数による条件 14 # サブセット条件 Client.where("orders_count = ?", params[:orders]) # (SQL) SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5)) # 複数の条件を一度に指定 Client.where("orders_count = ? AND locked = ?", params[:orders], false) # 個人的には Client.where(orders_count: params[:orders]). # where(locked: false) のように書くことが多い # Client.where("orders_count = #{params[:orders]}") はダメ。絶対。 # SQL インジェクション攻撃のおそれがある # プレースホルダを使って、記述することもできる Client.where("created_at >= :start_date AND created_at <= :end_date", start_date: params[:start_date], end_date: params[:end_date])
  16. 16. 大手町.rb #11 「Active Record クエリインタフェースについて」 並び順 15 Client.order(:created_at) # デフォルトは昇順 Client.order("created_at") Client.order(created_at: :desc) # 降順 Client.order(created_at: :asc) # :asc はなくてもいいけど、あってもいい Client.order("created_at DESC") # 文字列で明示的に指定することもできる Client.order("created_at ASC") Client.order(orders_count: :asc, created_at: :desc) Client.order(:orders_count, created_at: :desc) Client.order("orders_count ASC, created_at DESC") Client.order("orders_count ASC", "created_at DESC") Client.order("orders_count ASC").order("created_at DESC") # SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC
  17. 17. 大手町.rb #11 「Active Record クエリインタフェースについて」 特定のフィールドだけを取り出す 16 Client.select("viewable_by, locked") Client.select(:viewable_by, :locked) # 個人的には select は下記の場合に使うことが多い ## 1. アソシエーション先だけが欲しいとき Order.where("created_at > ?", 2.days.ago). select(:client_id).distinct.map(&:client) ## 2. 複雑な SQL を使ったフィルタをしたいとき Client.where(id: Order.complex_scope.select(:client_id).distinct) ## complex_scope のところはなんか複雑なことをしていると想像してください
  18. 18. 大手町.rb #11 「Active Record クエリインタフェースについて」 limit と offset limit と offset は1ページあたり10件表示とかで、 次のページのレコードを取得するときなどによく使う 17 Client.limit(5) # SELECT * FROM clients LIMIT 5 Client.limit(5).offset(30) # SELECT * FROM clients LIMIT 5 OFFSET 30
  19. 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. 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. 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. 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. 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. 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
  25. 25. 大手町.rb #11 「Active Record クエリインタフェースについて」 readonly: 読み取り専用オブジェクト readonly : 変更を明示的に禁止したレコードを返す 24 client = Client.readonly.first client.visits += 1 client.save # ActiveRecord::ReadOnlyRecord 例外が発生する
  26. 26. 大手町.rb #11 「Active Record クエリインタフェースについて」 楽観的ロック 楽観的ロックとは 同時編集、データの衝突があまりない利用シーンが前提 同時に同一レコードが編集された場合、あとから更新された方 について、 ActiveRecord::StaleObjectError が発生する いわゆる「先勝ち」にできる 楽観的ロックをしなければ、最初に更新したのはまったく失われ、 あとから更新した方だけが保存して残ることになる いわゆる「後勝ち」 integer 型の lock_version 列を作ると自動的にできる Ruby on Rails ベンリ フォームなどでは表示時点での lock_version 列を hidden で埋め込んでおくことが大事 詳しくは Qiita とかで検索してみてください。 update_column や update_all は lock_version の機構を使わ ないので楽観的ロックを使うときは注意が必要 25
  27. 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. 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. 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. 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. 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. 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. 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. 34. 大手町.rb #11 「Active Record クエリインタフェースについて」 複雑な includes 複数のアソシエーションの指定 ハッシュを使うことで、ネストにも対応可能、配列と組合せも OK 33 Article.includes(:category, :comments) Category.includes(articles: [{ comments: :guest }, :tags]).find(1)
  35. 35. 大手町.rb #11 「Active Record クエリインタフェースについて」 スキップ scope: よく使うクエリ条件に別名を与えられる dynamic_finders: find_by_first_name みたいなやつ。 Ruby on Rails 登場当初もてはやされたが、 いまやブームが過ぎ、あまり使われない Enums: いわゆる列挙型。スコープが自動生成され便利 メソッドチェイン:メソッドを次々つなげるやつ 検索とビルド: あれば更新なければ作成したいときに便利 34
  36. 36. 大手町.rb #11 「Active Record クエリインタフェースについて」 SQL の直接利用(select_all) select_all を使うと SQL を直接発行できる ActiveRecord ではなく connection のメソッド 私は頻繁に間違える 返り値を to_hash すると、ハッシュの配列になる 遅い ActiveRecord の生成処理が不要になる分高速化 select_all 以外にも SQL を直接利用する方法はある execute、select_one、select_rows, select_values, select_value select_all が一番使うし、ほかはあまり使わない 35 Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'").to_hash # => [ # {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"}, # {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"} # ]
  37. 37. 大手町.rb #11 「Active Record クエリインタフェースについて」 pluck、ids pluck を使うと高速化できる 下記の書き方をリファクタリングできる 36 Client.where(active: true).pluck(:id) # SELECT id FROM clients WHERE active = 1 # => [1, 2, 3] Client.distinct.pluck(:role) # SELECT DISTINCT role FROM clients # => ['admin', 'member', 'guest'] Client.pluck(:id, :name) # SELECT clients.id, clients.name FROM clients # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] Client.select(:id).map { |c| c.id } # Client.pluck(:id)、 Client.ids と同じ Client.select(:id).map(&:id) # Client.pluck(:id)、 Client.ids と同じ Client.select(:id, :name).map { |c| [c.id, c.name] } # Client.pluck(:id, :name)
  38. 38. 大手町.rb #11 「Active Record クエリインタフェースについて」 オブジェクトの存在チェック 37 # 引数のある exists? Client.exists?(id: [1,2,3]) Client.exists?(name: ['John', 'Sergei']) # 引数のない exists? Client.where(first_name: 'Ryan').exists? # via a model Article.any? # .count > 0 と同じ。 .present? よりも速い。(ARオブジェクトを生成しない) Article.many? # .count > 1 と同じ。 # 名前付きスコープを経由 Article.recent.any? Article.recent.many? # リレーション経由 Article.where(published: true).any? Article.where(published: true).many? # 関連付け経由 Article.first.categories.any? # アソシエーションでも使える Article.first.categories.many?
  39. 39. 大手町.rb #11 「Active Record クエリインタフェースについて」 まとめ 今日は Active Record クエリインタフェースについて、 ピックアップして、紹介しました。 個人的には joins や eager_load でネストして 取得ができるのが便利だと思っています。 ドキュメントを熟読しましょう! 気が付かなかった発見がたくさんあります。 私自身、たくさんありました Rails Guide と Ruby on Rails API は必読です。 https://railsguides.jp/ https://api.rubyonrails.org/ 38
  40. 40. ご清聴ありがとう ございました

×