diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 7f9d3766c3..ce33de1966 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,12 @@ +* Allow delegated_type to be specified primary_key and foreign_key. + + Since delegated_type assumes that the foreign_key ends with `_id`, + `singular_id` defined by it does not work when the foreign_key does + not end with `id`. This change fixes it by taking into account + `primary_key` and `foreign_key` in the options. + + *Ryota Egusa* + * Restore possibility of passing `false` to :polymorphic option of `belongs_to`. Previously, passing `false` would trigger the option validation logic diff --git a/activerecord/lib/active_record/delegated_type.rb b/activerecord/lib/active_record/delegated_type.rb index 4a4d27fb29..209115673c 100644 --- a/activerecord/lib/active_record/delegated_type.rb +++ b/activerecord/lib/active_record/delegated_type.rb @@ -156,8 +156,6 @@ module ActiveRecord # Entry#comment # => returns the comment record, when entryable_type == "Comment", otherwise nil # Entry#comment_id # => returns entryable_id, when entryable_type == "Comment", otherwise nil # - # The +options+ are passed directly to the +belongs_to+ call, so this is where you declare +dependent+ etc. - # # You can also declare namespaced types: # # class Entry < ApplicationRecord @@ -167,15 +165,38 @@ module ActiveRecord # Entry.access_notice_messages # entry.access_notice_message # entry.access_notice_message? + # + # === Options + # + # The +options+ are passed directly to the +belongs_to+ call, so this is where you declare +dependent+ etc. + # The following options can be included to specialize the behavior of the delegated type convenience methods. + # + # [:foreign_key] + # Specify the foreign key used for the convenience methods. By default this is guessed to be the passed + # +role+ with an "_id" suffix. So a class that defines a + # delegated_type :entryable, types: %w[ Message Comment ] association will use "entryable_id" as + # the default :foreign_key. + # [:primary_key] + # Specify the method that returns the primary key of associated object used for the convenience methods. + # By default this is +id+. + # + # Option examples: + # class Entry < ApplicationRecord + # delegated_type :entryable, types: %w[ Message Comment ], primary_key: :uuid, foreign_key: :entryable_uuid + # end + # + # Entry#message_uuid # => returns entryable_uuid, when entryable_type == "Message", otherwise nil + # Entry#comment_uuid # => returns entryable_uuid, when entryable_type == "Comment", otherwise nil def delegated_type(role, types:, **options) belongs_to role, options.delete(:scope), **options.merge(polymorphic: true) - define_delegated_type_methods role, types: types + define_delegated_type_methods role, types: types, options: options end private - def define_delegated_type_methods(role, types:) + def define_delegated_type_methods(role, types:, options:) + primary_key = options[:primary_key] || "id" role_type = "#{role}_type" - role_id = "#{role}_id" + role_id = options[:foreign_key] || "#{role}_id" define_method "#{role}_class" do public_send("#{role}_type").constantize @@ -200,7 +221,7 @@ module ActiveRecord public_send(role) if public_send(query) end - define_method "#{singular}_id" do + define_method "#{singular}_#{primary_key}" do public_send(role_id) if public_send(query) end end diff --git a/activerecord/test/cases/delegated_type_test.rb b/activerecord/test/cases/delegated_type_test.rb index b6c0680649..2228b22703 100644 --- a/activerecord/test/cases/delegated_type_test.rb +++ b/activerecord/test/cases/delegated_type_test.rb @@ -4,6 +4,9 @@ require "cases/helper" require "models/entry" require "models/message" require "models/comment" +require "models/uuid_entry" +require "models/uuid_message" +require "models/uuid_comment" class DelegatedTypeTest < ActiveRecord::TestCase fixtures :comments @@ -11,6 +14,11 @@ class DelegatedTypeTest < ActiveRecord::TestCase setup do @entry_with_message = Entry.create! entryable: Message.new(subject: "Hello world!") @entry_with_comment = Entry.create! entryable: comments(:greetings) + + if current_adapter?(:PostgreSQLAdapter) + @uuid_entry_with_message = UuidEntry.create! uuid: SecureRandom.uuid, entryable: UuidMessage.new(uuid: SecureRandom.uuid, subject: "Hello world!") + @uuid_entry_with_comment = UuidEntry.create! uuid: SecureRandom.uuid, entryable: UuidComment.new(uuid: SecureRandom.uuid, content: "comment") + end end test "delegated class" do @@ -54,4 +62,14 @@ class DelegatedTypeTest < ActiveRecord::TestCase assert_equal @entry_with_comment.entryable_id, @entry_with_comment.comment_id assert_nil @entry_with_comment.message_id end + + test "association uuid" do + skip unless current_adapter?(:PostgreSQLAdapter) + + assert_equal @uuid_entry_with_message.entryable_uuid, @uuid_entry_with_message.uuid_message_uuid + assert_nil @uuid_entry_with_message.uuid_comment_uuid + + assert_equal @uuid_entry_with_comment.entryable_uuid, @uuid_entry_with_comment.uuid_comment_uuid + assert_nil @uuid_entry_with_comment.uuid_message_uuid + end end diff --git a/activerecord/test/models/uuid_comment.rb b/activerecord/test/models/uuid_comment.rb new file mode 100644 index 0000000000..62736af32f --- /dev/null +++ b/activerecord/test/models/uuid_comment.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class UuidComment < ActiveRecord::Base + has_one :uuid_entry, as: :entryable +end diff --git a/activerecord/test/models/uuid_entry.rb b/activerecord/test/models/uuid_entry.rb new file mode 100644 index 0000000000..9bb79a0e7c --- /dev/null +++ b/activerecord/test/models/uuid_entry.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class UuidEntry < ActiveRecord::Base + delegated_type :entryable, types: %w[ UuidMessage UuidComment ], primary_key: :uuid, foreign_key: :entryable_uuid +end diff --git a/activerecord/test/models/uuid_message.rb b/activerecord/test/models/uuid_message.rb new file mode 100644 index 0000000000..904414f8de --- /dev/null +++ b/activerecord/test/models/uuid_message.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class UuidMessage < ActiveRecord::Base + has_one :uuid_entry, as: :entryable +end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index a24c190a93..573273040f 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -104,11 +104,27 @@ _SQL t.decimal :decimal_array_default, array: true, default: [1.23, 3.45] end + create_table :uuid_comments, force: true, id: false do |t| + t.uuid :uuid, primary_key: true, **uuid_default + t.string :content + end + + create_table :uuid_entries, force: true, id: false do |t| + t.uuid :uuid, primary_key: true, **uuid_default + t.string :entryable_type, null: false + t.uuid :entryable_uuid, null: false + end + create_table :uuid_items, force: true, id: false do |t| t.uuid :uuid, primary_key: true, **uuid_default t.string :title end + create_table :uuid_messages, force: true, id: false do |t| + t.uuid :uuid, primary_key: true, **uuid_default + t.string :subject + end + if supports_partitioned_indexes? create_table(:measurements, id: false, force: true, options: "PARTITION BY LIST (city_id)") do |t| t.string :city_id, null: false