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 commit fixes it by taking into account
`primary_key` and `foreign_key` in the options.
This commit is contained in:
Ryota Egusa 2020-12-18 15:06:16 +09:00
parent 9c120d7559
commit 941c5641b0
7 changed files with 85 additions and 6 deletions

View File

@ -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

View File

@ -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
# <tt>delegated_type :entryable, types: %w[ Message Comment ]</tt> association will use "entryable_id" as
# the default <tt>:foreign_key</tt>.
# [: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

View File

@ -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

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class UuidComment < ActiveRecord::Base
has_one :uuid_entry, as: :entryable
end

View File

@ -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

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class UuidMessage < ActiveRecord::Base
has_one :uuid_entry, as: :entryable
end

View File

@ -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