【rails6.1新機能】delgated_typeを使用する方法を紹介します

今回は、rails6.1の新機能を紹介します。

delegated_typeは、複数テーブルをrails側で簡単に扱えるようにした機能です。

僕もまだまだ理解できていないですが、せっかくなのでportfolioで使ってみました。

versions
  • ruby 2.6.5
  • rails 6.1.1

3つのテーブルを準備しよう

今回は、profilesテーブルとuser_profilesテーブル、philosopher_profilesテーブルを用意します。

抽象的なテーブル(profiles)を用意することで、各テーブル内のカラムがすっきりします。

class CreateProfiles < ActiveRecord::Migration[6.1]
  def change
    create_table :profiles do |t|
      t.text :content
      t.references :user, null: false, foreign_key: true
      t.string :profile_type
      t.bigint :profile_id
      t.timestamps
    end
  
    create_table :philosopher_profiles do |t|
      t.string :affiliation
      t.text :research

      t.timestamps
    end

    create_table :user_profiles do |t|
      t.text :kleshas

      t.timestamps
    end
  end
end

profilesテーブルには、共通項となるプロフィールの内容を記載しています。

その他二つのテーブルは、個別で必要な情報をカラムとして用意しています。

テーブル設計も念のため載せておきます。

delegated_typeを実装していこう

まずは、アソシエーションです。

今回は、同じ処理を2つのモデル(user_profileとphilosopher_profile)に記述するので、concernディレクトリに処理をまとめます。

module Pro
  extend ActiveSupport::Concern

  included do
    has_one :profile, as: :profile, touch: true, dependent: :destroy
    delegate :content, to: :profile
  end
end

モデルにincludeするのですが、profile.rbと名前が重複してしまうので、Proというわかりづらい名前になっています。。。
後のリファクタリンで名前解決します。

「delegate :content, to: :profile」の部分は、profileのインスタンスに対して、contentがメソッドチェーンで呼び出せる様にしています。

delegateは日本語訳すると、「委任する」となるので、まさに「content」をprofileのインスタンスに委任しています。

ポイント

「as: :profile」がないとprofileテーブルにそれぞれのモデルのidがないというエラーが出る

モジュールは完成したので、それをincludeしていきます。

class UserProfile < ApplicationRecord
  include Pro
end
class PhilosopherProfile < ApplicationRecord
  include Pro
end

ここまででほぼ完成です。

最後にdelegated_typeをprofile.rbに記述していきます。

class Profile < ApplicationRecord
  delegated_type :profile, types: %w[UserProfile PhilosopherProfile], dependent: :destroy
end

ここでは、profileのインスタンスに対して委任されるtypeを指定しています。

UserProfile、PhilosopherProfileになりますね。

ポイント

「 delegated_type :profile」は、profilesテーブルにある〇〇_type、〇〇_idの〇〇を記述する

これで準備は完了です。

実行してみよう

最後に実行です。

rails cを入力して以下を実行します。

irb(main):001:0> Profile.create(content: "test", user_id: @user.id, profile: UserProfile.new(kleshas: "test"))

僕の場合はuserもアソシエーションで組んでいるので、そこだけご留意ください。

 TRANSACTION (0.4ms)  BEGIN
  User Load (0.8ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
  UserProfile Create (1.2ms)  INSERT INTO `user_profiles` (`kleshas`, `created_at`, `updated_at`) VALUES ('test', '2021-02-03 10:34:56.282272', '2021-02-03 10:34:56.282272')
  Profile Load (0.5ms)  SELECT `profiles`.* FROM `profiles` WHERE `profiles`.`profile_id` = 8 AND `profiles`.`profile_type` = 'UserProfile' LIMIT 1
  Profile Create (0.6ms)  INSERT INTO `profiles` (`content`, `user_id`, `profile_type`, `profile_id`, `created_at`, `updated_at`) VALUES ('test', 2, 'UserProfile', 8, '2021-02-03 10:34:56.288475', '2021-02-03 10:34:56.288475')
  TRANSACTION (5.0ms)  COMMIT
=> #<Profile id: 5, content: "test", user_id: 2, profile_type: "UserProfile", profile_id: 8, created_at: "2021-02-03 10:34:56.288475000 +0000", updated_at: "2021-02-03 10:34:56.288475000 +0000">

どうやらtransaction処理が働く様です。

ついでなので削除もやってみましょう。

irb(main):001:0> p = Profile.first
irb(main):002:0> p.destroy
  TRANSACTION (0.4ms)  BEGIN
  Profile Destroy (0.7ms)  DELETE FROM `profiles` WHERE `profiles`.`id` = 3
  UserProfile Load (0.4ms)  SELECT `user_profiles`.* FROM `user_profiles` WHERE `user_profiles`.`id` = 7 LIMIT 1
  Profile Load (0.4ms)  SELECT `profiles`.* FROM `profiles` WHERE `profiles`.`profile_id` = 7 AND `profiles`.`profile_type` = 'UserProfile' LIMIT 1
  Profile Destroy (0.4ms)  DELETE FROM `profiles` WHERE `profiles`.`id` = 4
  UserProfile Destroy (0.5ms)  DELETE FROM `user_profiles` WHERE `user_profiles`.`id` = 7
  TRANSACTION (1.1ms)  COMMIT
=> #<Profile id: 3, content: "test", user_id: 2, profile_type: "UserProfile", profile_id: 7, created_at: "2021-02-03 06:57:14.834095000 +0000", updated_at: "2021-02-03 06:58:06.406713000 +0000">

「dependent: :destroy」のおかげでprofileもuser_profileも共にテーブルから削除されていますね。

transaction処理が働くのはとてもありがたいです。

まとめ

  • delegated_typeはrails6.1の新機能である
  • 複数テーブルをrails側から簡単に操作できる
  • delegateは日本語訳で「委任する」
  • transaction処理が働く

rails6.1の新機能はどうでしたでしょうか?

僕も勉強がてら調べていて、とても便利そうだったので、実装をしてみました。

改めて手を動かすこと、興味を持つことの大切さを痛感しました。

参考

Rails6.1で追加されたdelegated typeとGraphQLのUnion typesは相性よさそうなので試したみた

delegateを使って、別クラスへの切り出し