🌟

SQLアンチパターン ポリモーフィック関連について

2023/08/06に公開

ポリモーフィック関連 とは?

ポリモーフィック関連は、1つのモデルが複数の他のモデルと関連することをいいます。これにより、1つのテーブルで複数の異なるモデルを参照できます。

ただし、この設計方法は、外部キー制約を使用することができないため、SQLアンチパターンとされているので、使用するには注意が必要です。

解決策として、①交差テーブル(中間テーブル)の作成 ②共通の親テーブルの作成 などが挙げられます。(書籍『SQLアンチパターン』第6章 ポリモーフィック関連 参照)

rails ではポリモーフィック関連はサポートされており、 polymorphic:as: を使用することで、ある1つのモデルが他の複数のモデルに属していることを、1つの関連付けだけで表現することができます。(ドキュメント

たとえば、写真(picture)モデルがあり、このモデルを従業員(employee)モデルと製品(product)モデルの両方に従属させたいとします。

この場合は以下のように実装することで関連付けすることができます。

class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end

class Employee < ApplicationRecord
  has_many :pictures, as: :imageable
end

class Product < ApplicationRecord
  has_many :pictures, as: :imageable
end

少し解説すると、polymorphic:オプションは、1つのモデルが複数の異なるモデルと関連付けられることを宣言する際に使用されます。このオプションを使用することで、1つの外部キーで異なるモデルと関連付けることができます。

また、as:オプションは、逆向きのポリモーフィック関連を定義する際に使用します。つまり、関連先のモデルがどのような名前でポリモーフィック関連を持っているのかを示します。

ただし、ポリモフィック関連したデータに対しては、 joins や eager_load メソッドは データを取得・参照できないので注意が必要です。

例えば、下記のようにポリモーフィック関連付けされたモデルがあるとします。joins,eager_load,preload,includesメソッドを使用し、Commentに紐づくArticle,Opinionを取得しようとすると以下のようになりました。

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

# app/models/article.rb
class Article < ApplicationRecord
  has_many :comments, as: :commentable
end

# app/models/opinion.rb
class Opinion < ApplicationRecord
  has_many :comments, as: :commentable
end

irb(main):003:0> Comment
=> Comment(id: integer, body: text, user_id: integer, commentable_type: string, commentable_id: integer, created_at: datetime, updated_at: datetime)

irb(main):004:0> Article
=> Article(id: integer, created_at: datetime, updated_at: datetime, title: string, body: text)

irb(main):005:0> Opinion
=> Opinion(id: integer, created_at: datetime, updated_at: datetime, title: string, body: text, user_id: integer, clip_id: integer)

irb(main):006:0> Comment.joins(:commentable)
Traceback (most recent call last):
ActiveRecord::EagerLoadPolymorphicError (Cannot eagerly load the polymorphic association :commentable)

irb(main):007:0> Comment.eager_load(:commentable)
Traceback (most recent call last):
ActiveRecord::EagerLoadPolymorphicError (Cannot eagerly load the polymorphic association :commentable)

irb(main):008:0> Comment.preload(:commentable)
  Comment Load (0.4ms)  SELECT "comments".* FROM "comments" /* loading for inspect */ LIMIT ?  [["LIMIT", 11]]
  Article Load (0.2ms)  SELECT "articles".* FROM "articles" WHERE "articles"."id" = ?  [["id", 1]]
  Opinion Load (0.3ms)  SELECT "opinions".* FROM "opinions" WHERE "opinions"."id" = ?  [["id", 1]]
=> #<ActiveRecord::Relation [#<Comment id: 1, body: "artcle body", user_id: 1, commentable_type: "Article", commentable_id: 1, created_at: "2023-08-05 23:03:27.417240000 +0000", updated_at: "2023-08-05 23:03:27.417240000 +0000">, #<Comment id: 2, body: "opinion body", user_id: 1, commentable_type: "Opinion", commentable_id: 1, created_at: "2023-08-06 00:00:02.851373000 +0000", updated_at: "2023-08-06 00:00:02.851373000 +0000">]>

irb(main):009:0> Comment.includes(:commentable)
  Comment Load (0.2ms)  SELECT "comments".* FROM "comments" /* loading for inspect */ LIMIT ?  [["LIMIT", 11]]
  Article Load (0.2ms)  SELECT "articles".* FROM "articles" WHERE "articles"."id" = ?  [["id", 1]]
  Opinion Load (0.2ms)  SELECT "opinions".* FROM "opinions" WHERE "opinions"."id" = ?  [["id", 1]]
=> #<ActiveRecord::Relation [#<Comment id: 1, body: "artcle body", user_id: 1, commentable_type: "Article", commentable_id: 1, created_at: "2023-08-05 23:03:27.417240000 +0000", updated_at: "2023-08-05 23:03:27.417240000 +0000">, #<Comment id: 2, body: "opinion body", user_id: 1, commentable_type: "Opinion", commentable_id: 1, created_at: "2023-08-06 00:00:02.851373000 +0000", updated_at: "2023-08-06 00:00:02.851373000 +0000">]>

上記の時、ポリモーフィック関連を実装したデータに対して joins,eager_loadメソッドを使用し、データを取得することができませんでした。

理由は、joins,eager_loadメソッドがデータベースの結合(JOIN)を使用しデータを取得するためです。(JOINについてはこちらを参照)

joinseager_loadメソッドは、Owner.joins(:cats)Company.eager_load(:users) のように事前に結合条件を指定する必要があり、どちらも関連するテーブルのデータが明確でなければいけません。

しかし、ポリモーフィック関連では関連するテーブルが動的、かつ、実行時にどのテーブルと結合するかが決まるため、静的な結合条件を指定することができません。

よって、ActiveRecord::EagerLoadPolymorphicError (Cannot eagerly load the polymorphic association :commentable) → ActiveRecord::EagerLoadPolymorphicError (ポリモーフィック関連付けを積極的にロードできません:commentable) というエラーが表示されます。

(ActiveRecord::EagerLoadPolymorphicError に関してはこちらを参照。)

一方、includespreloadメソッドは、関連するデータを取得する際にデータベースの結合(JOIN)を使わずに、クエリを発行することで関連データを取得します。これにより、ポリモーフィック関連のように関連先が動的な場合でもうまく動作します。

なので、ポリモーフィック関連したデータを取得するときは、preload もしくは includesメソッドを使用することでデータを取得することができます。

ただし、joinsメソッドであっても、joins メソッドの中で結合するテーブルを明示的にするSQLステートメントを指定することでデータを取得することができます。(参考

irb(main):025:0> Comment.joins("INNER JOIN articles ON comments.commentable_id = articles.id AND comments.commentable_type = 'Article'")
  Comment Load (0.6ms)  SELECT "comments".* FROM "comments" INNER JOIN articles ON comments.commentable_id = articles.id AND comments.commentable_type = 'Article' /* loading for inspect */ LIMIT ?  [["LIMIT", 11]]

=> #<ActiveRecord::Relation [#<Comment id: 1, body: "artcle body", user_id: 1, commentable_type: "Article", commentable_id: 1, created_at: "2023-08-05 23:03:27.417240000 +0000", updated_at: "2023-08-05 23:03:27.417240000 +0000">]>

 

参考

SQLアンチパターン ポリモフィック関連
https://shiro-secret-base.com/sqlアンチパターン:ポリモーフィック関連につい/
https://qiita.com/dai329/items/1db8fbe37f43a465d801

 
rails ポリモフィック関連
https://railsguides.jp/association_basics.html#ポリモーフィック関連付け

 
joins,eager_load,preload,includesメソッド
https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58
https://tech.stmn.co.jp/entry/2020/11/30/145159
https://moneyforward-dev.jp/entry/2019/04/02/activerecord-includes-preload-eagerload/
https://zenn.dev/tomoya_pama/articles/85a37b3f1e6119

 
その他
https://spice-factory.co.jp/development/has-and-belongs-to-many-table/
https://skillhub.jp/courses/145/lessons/1006
https://qiita.com/itkrt2y/items/32ad1512fce1bf90c20b
https://qiita.com/joker1007/items/9da1e279424554df7bb8
https://blog.agile.esm.co.jp/entry/rails-polymorphic-story
https://qiita.com/kamohicokamo/items/c13f72d720040cfd796d
https://obel.hatenablog.jp/entry/20200423/1587585600
https://techracho.bpsinc.jp/hachi8833/2020_03_11/89510
https://sakaishun.com/2021/03/13/eagerload-preload-includes/
https://qiita.com/ostk0069/items/23beb870adf785506be2

Discussion