14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails】エラー「ArgumentError: Relation passed to #or must be structurally compatible. 」とorメソッドについて

Last updated at Posted at 2024-10-27

はじめに

ActiveRecordで関連先のテーブルのデータを取得したい時があると思います。カラムを絞り込む時の条件も複数指定しなければいけない時もあると思います。

そんな時にもActiveRecordのorメソッドを使うことができます。
その際の注意点について発生したエラーも参考にしながら、紐解いて説明していきます。

前提条件

リレーション

  • Userモデル: ユーザーを表すモデル
  • Postモデル: 投稿を表すモデル
  • 1人のユーザーが複数の投稿を持つ(1対多の関係)
models/user.rb
class User < ApplicationRecord
  has_many :posts
end
models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end

エラー内容

ArgumentError: Relation passed to #or must be structurally compatible. Incompatible values: [:includes]

和訳すると、「orに渡されるリレーションは構造的に互換性がある必要があります」
言い換えると、「orで複数の条件をする場合、同じテーブルの同じ種類のデータを参照する必要があります」

該当するコード

  def has_posted
    user = User.includes(:posts)
      .where(posts: { status: 'posted' })
      .or(where(posts: { id: nil })
  end

usersテーブルに関連づいているpostsテーブルの、投稿済み(posted)のレコードを持つユーザー
または投稿が1件もないユーザーをincludesで取得しています

原因

上記1つ目のwhere条件は、User.includes(:posts)でモデルを参照していたのに対して、
2つ目のorメソッドの中のwhere条件は、モデルの参照が行われていない。
つまり、両方の条件で参照するデータの構造が不一致のために発生しています。

解決方法

orメソッド内のwhereメソッドにも関連付けを明示的に指定することで解決しました。

修正後のコード

  def most_or_not_posted_user
    user = User.includes(:posts)
      .where(posts: { status: 'posted' })
      .or(includes(:posts).where(posts: { id: nil }))
  end

代替案

上記修正後のコードで簡潔に書けますが、includesメソッドを2回使用しているため、冗長で非効率かもしれません。

whereメソッドの中に直接SQLを書く

user = User.includes(:posts)
      .where("posts.status = 'posted' OR posts.id IS NULL")

whereの中にSQLで直接OR条件を書く手間はかかりますが、クエリのパフォーマンスが向上して効率的にデータを取得できます。

補足

orメソッドについて

2つのリレーションをまたいでOR条件を使いたい場合は、1つ目のリレーションでorメソッドを呼び出し、そのメソッドの引数に2つ目のリレーションを渡すことで実現できます。

Customer.where(last_name: 'Smith').or(Customer.where(orders_count: [1, 3, 5]))

Railsガイドより引用

Q. なぜorメソッドの引数に2つ目のリレーションを入れる必要があるのか?

A. 異なるテーブルへのアクセスするため

他のモデルとの関連付け(リレーション)が定義されている場合、orで結合するwhere条件の中にその関連付けを明示的に指定する必要があります。

関連付けを指定することで、異なるモデルの属性に対する条件をor条件に含めることができます。

orメソッド内部の挙動

理由を追求するにあたって、公式ドキュメントからorメソッドの中身を見に行きました。

activerecord/lib/active_record/relation/query_methods.rb
    def or(other)
      if other.is_a?(Relation)
        spawn.or!(other)
      else
        raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead."
      end
    end

    def or!(other) # :nodoc:
      incompatible_values = structurally_incompatible_values_for(other)

      unless incompatible_values.empty?
        raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}"
      end

      self.where_clause = self.where_clause.or(other.where_clause)
      self.having_clause = having_clause.or(other.having_clause)
      self.references_values |= other.references_values

      self
    end

ポイント

  1. or(other)メソッドで、otherがRelationのインスタンスかどうかをis_a?メソッドで確認している(Relationが今回のpostsにあたる)
  2. Relationのインスタンスであれば、おそらくspawn.or!という内部のメソッドでクエリを結合している
  3. インスタンスがなければ、raiseでArgumentErrorの例外エラーを発生させている
  4. 今回発生したエラー内容を見る限り、or!(other)メソッドの中でincludesで取得したリレーション構造に互換性がないため、raiseで発生した例外エラーというのが読み取れます

上記のコードはRailsドキュメント(or)のページに記載されているソースコードのリンク先のページで見れます。

まとめ

ActiveRecordのorメソッドでor条件を指定するときは、モデルや関連先のテーブルも合わせて明示的に指定する。

参考記事

14
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?