ActiveRecordに関する様々な知見

こんにちは。
デザイン・システム室の坂井です。

今回は、皆さんRailsでお馴染みのActiveRecordについて話したいと思います。

前半は基本の話で、後半は実務で使った事を話します。

ActiveRecordの理解に自信が無い方に読んで頂ければ幸いです!

そもそもActiveRecordとは(基本)

  • Ruby on Railsで採用されているOR Mapperのことです。

※OR Mapperとは
「直接SQL文を書く代わりに非常に短いコードでデータベースの読み書きを行える機能」
をORM (オブジェクトリレーショナルマッピング)といい、
OR Mapperとは、そのためのモジュールになります。

  • モデルとテーブルをつなぎ合わせることでRailsからテーブルのレコードにアクセスできるようにする役割があります。
  • Railsにデフォルトでインストールされており、実際に利用する際には ActiveRecord::Base というクラスを継承して使用します。

ActiveRecord::Baseとは(基本)

Ruby on Railsで使用されるORM(オブジェクトリレーショナルマッピング)の基底クラス(継承元のクラス)のことです。

app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base

モデルクラスはActiveRecord::Baseを継承することで、データベースとのアクセスや操作を簡単に行うことができます。

ex)app/models/user.rb

class User < ApplicationRecord

これにより以下のようなコードで、データベースからユーザーレコードを取得したり、

新しいユーザーレコードを作成することができます。

user = User.find(1)
user = User.create(name: "John Doe", email: "johndoe@example.com")

例えば、ActiveRecord::Baseには、以下のようなメソッドが定義されています。

  • find
  • create
  • update
  • destroy
  • save
  • all
  • where
  • order
  • limit

そのほかにも、

ActiveRecord::Baseを継承したクラスは、

①データベーステーブルとの関連付け(クラス名はテーブル名と対応しており、デフォルトではテーブル名はクラス名の複数形)

を行ってくれたり、

②データの妥当性を検証するためのバリデーション機能を提供してくれたり、

③データベースからのデータ検索を行ったり、

④データの作成、更新、削除などのイベント発生時にコードを実行するためのコールバック機能を提供してくれる便利なものです。

実務で扱ったActiveRecord

実務ではrails consoleにて、③のデータベースからのデータ検索 を行うことが多いです。

consoleの事例(わかりやすくするためモデルなどは改変)

  • ex) Userのidが999999かつ、現在から2ヶ月前までの投稿を検索
Post.where(user_id: 999999, published_at: Time.now.ago(2.month)..Time.now)
  • ex) 2019年5月1日より過去の投稿を検索
Post.where.not(published_at: Time.local(2019,5,1)..Float::INFINITY)

※正の無限大 Float::INFINITY を指定して、「ある日付を越えた日付を持つレコード」を表すことが出来るのですが、

負の無限大の、-::Float::INFINITY..Time.local(2019,5,1)として、2019年5月1日より以前の日付を検索すると、

ArgumentError: bad value for range

というエラーが発生するため、not を使い、あくまで正の無限大を範囲に指定してあげる必要があります。

  • ex) 大量のデータを扱うfind_eachについての事例
Data.where(device_type_id: washing_t).find_each do |washing_t_data|
  p washing_t_data.appliance_id
end

大量のデータを扱う場合は、find_eachを使って大量のレコードをループ処理する際にメモリの消費量を抑えることが出来ます。

1万件を超える大量のレコードを一度に取得してループ処理を実行すると、メモリが圧迫されてアプリケーションの動作が遅くなったり、フリーズしてしまう恐れがあります。

そのため、大量のデータを扱う場合にはfind_eachはとても重宝します。

過去に自分がハマった事例①

上に書いた様に様々な条件で検索をかけることが出来ますが、過去に自分が何故上手くいかないのかすぐに分からなかった事例を紹介します。

↓ユーザーを持たない機器で、ポイントを1ポイント以上持っている機器を検索

Device.eager_load(:users, :points).where(users: { id: nil }, points: { point: 1..Float::INFINITY })

eager_loadの箇所に、そのほかincludesだと上手く取得出来ましたが、joinsは情報を取得出来ず0件、preloadはエラーになります。

これら4つの結果の違いは調べるとすぐに出てくるのですが、joinsだけ0件となって返ってくる理由が当初分かりませんでした。(これを読んでいる方はすぐに分かりますか?)

答えはjoins はINNER JOIN(内部結合)なのが理由です。

内部結合とは、各テーブル同士で関連づけられたidが一致しているものだけを使って新しいテーブルを作ること。

つまり、今回の場合DeviceとUserとPointのそれぞれidを持っているデータで新しいテーブルが作られています。

→ そのため、where(users: { id: nil }でUserのidを持たないDeviceのデータが0件となります。

ちなみにeager_loadとincludesは左外部結合ですので、Deviceの全データを取り出してUserとPointをくっつけています。

→ そのため、where(users: { id: nil } でもDeviceのデータが返ってきます。

そして最後にpreloadはエラーを起こす理由は、結合先のテーブル(usersやpoints)で絞り込むことがそもそも出来ないからです。

過去に自分がハマった事例②

↓AX-30というパターン番号を持つ機器を検索(DeviceとDeviceTypeは多対1の関係)

Device.includes(:device_type).where(device_type: { pattern_number: "AX-30" })

これだとエラーが出ます。(ERROR: missing FROM-clause entry for table “device_type”)

何故だが分かりますか?

このエラーメッセージによってすぐに気が付ける人もいるかと思いますが、

includes(:device_type) はdevice_typeの単数で合っていますが、where(device_type が間違いで、

正しくはdevice_typesです。

includesの結合はDeviceが多でDeviceTypeが1の様に、関係性によってdevice_typeの単数にもdevice_typesの複数にもなることがありますが、

whereの指定先はテーブルを指定するため、絶対に複数形になります。

大した話ではないですが、何となく結合を行なっていると気が付かないことなのではないかと思いました。

まとめ

以上ActiveRecordに関する話でしたが、この記事を最後まで読んでくれた方には、少しでも身についた知識があれば幸いです。