Skip to content

MONGOID-5734 Custom polymorphic types #5845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/mongoid/association/accessors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ def __build__(name, object, association, selected_fields = nil)
#
# @return [ Proxy ] The association.
def create_relation(object, association, selected_fields = nil)
type = @attributes[association.inverse_type]
key = @attributes[association.inverse_type]
type = key ? association.resolver.model_for(key) : nil
target = if t = association.build(self, object, type, selected_fields)
association.create_relation(self, t)
else
Expand Down
15 changes: 14 additions & 1 deletion lib/mongoid/association/nested/one.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,25 @@ def initialize(association, attributes, options)
@attributes = attributes.with_indifferent_access
@association = association
@options = options
@class_name = options[:class_name] ? options[:class_name].constantize : association.klass
@class_name = class_from(options[:class_name])
@destroy = @attributes.delete(:_destroy)
end

private

# Coerces the argument into a class, or defaults to the association's class.
#
# @param [ String | Mongoid::Document | nil ] name_or_class the value to coerce
#
# @return [ Mongoid::Document ] the resulting class
def class_from(name_or_class)
case name_or_class
when nil, false then association.klass
when String then name_or_class.constantize
else name_or_class
end
end

# Extracts and converts the id to the expected type.
#
# @return [ BSON::ObjectId | String | Object | nil ] The converted id,
Expand Down
15 changes: 15 additions & 0 deletions lib/mongoid/association/referenced/belongs_to.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,21 @@ def polymorphic?
@polymorphic ||= !!@options[:polymorphic]
end

# Returns the object responsible for converting polymorphic type references into
# class objects, and vice versa. This is obtained via the `:polymorphic` option
# that was given when the association was defined.
#
# See Mongoid::ModelResolver.resolver for how the `:polymorphic` option is
# interpreted here.
#
# @raise KeyError if no such resolver has been registered under the given
# identifier.
#
# @return [ nil | Mongoid::ModelResolver ] the resolver to use
def resolver
@resolver ||= Mongoid::ModelResolver.resolver(@options[:polymorphic])
end

# The name of the field used to store the type of polymorphic association.
#
# @return [ String ] The field used to store the type of polymorphic association.
Expand Down
8 changes: 7 additions & 1 deletion lib/mongoid/association/referenced/belongs_to/binding.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ def bind_one
binding do
check_polymorphic_inverses!(_target)
bind_foreign_key(_base, record_id(_target))
bind_polymorphic_inverse_type(_base, _target.class.name)

# set the inverse type (e.g. "#{name}_type") for new polymorphic associations
if _association.inverse_type && !_base.frozen?
key = _association.resolver.default_key_for(_target)
bind_polymorphic_inverse_type(_base, key)
end

if inverse = _association.inverse(_target)
if set_base_association
if _base.referenced_many?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def execute_query(object, type)
end

def query_criteria(object, type)
cls = type ? type.constantize : relation_class
cls = type ? (type.is_a?(String) ? type.constantize : type) : relation_class
crit = cls.criteria
crit = crit.apply_scope(scope)
crit.where(primary_key => object)
Expand Down
17 changes: 9 additions & 8 deletions lib/mongoid/association/referenced/has_many.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require 'mongoid/association/referenced/has_many/proxy'
require 'mongoid/association/referenced/has_many/enumerable'
require 'mongoid/association/referenced/has_many/eager'
require 'mongoid/association/referenced/with_polymorphic_criteria'

module Mongoid
module Association
Expand All @@ -15,6 +16,7 @@ module Referenced
class HasMany
include Relatable
include Buildable
include WithPolymorphicCriteria

# The options available for this type of association, in addition to the
# common ones.
Expand Down Expand Up @@ -131,13 +133,20 @@ def type
# @param [ Class ] object_class The object class.
#
# @return [ Mongoid::Criteria ] The criteria object.
#
# @deprecated in 9.0.x
#
# It appears as if this method is an artifact left over from a refactoring that renamed it
# `with_polymorphic_criterion`, and made it private. Regardless, this method isn't referenced
# anywhere else, and is unlikely to be useful to external clients. We should remove it.
def add_polymorphic_criterion(criteria, object_class)
if polymorphic?
criteria.where(type => object_class.name)
else
criteria
end
end
Mongoid.deprecate(self, :add_polymorphic_criterion)

# Is this association polymorphic?
#
Expand Down Expand Up @@ -222,14 +231,6 @@ def query_criteria(object, base)
with_ordering(crit)
end

def with_polymorphic_criterion(criteria, base)
if polymorphic?
criteria.where(type => base.class.name)
else
criteria
end
end

def with_ordering(criteria)
if order
criteria.order_by(order)
Expand Down
11 changes: 3 additions & 8 deletions lib/mongoid/association/referenced/has_one/buildable.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# frozen_string_literal: true
# rubocop:todo all

require 'mongoid/association/referenced/with_polymorphic_criteria'

module Mongoid
module Association
module Referenced
class HasOne

# The Builder behavior for has_one associations.
module Buildable
include WithPolymorphicCriteria

# This method either takes an _id or an object and queries for the
# inverse side using the id or sets the object after clearing the
Expand Down Expand Up @@ -57,14 +60,6 @@ def execute_query(object, base)
query_criteria(object, base).take
end

def with_polymorphic_criterion(criteria, base)
if polymorphic?
criteria.where(type => base.class.name)
else
criteria
end
end

def query?(object)
object && !object.is_a?(Mongoid::Document)
end
Expand Down
41 changes: 41 additions & 0 deletions lib/mongoid/association/referenced/with_polymorphic_criteria.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module Mongoid
module Association
module Referenced
# Implements the `with_polymorphic_criteria` shared behavior.
#
# @api private
module WithPolymorphicCriteria
# If the receiver represents a polymorphic association, applies
# the polymorphic search criteria to the given `criteria` object.
#
# @param [ Mongoid::Criteria ] criteria the criteria to append to
# if receiver is polymorphic.
# @param [ Mongoid::Document ] base the document to use when resolving
# the polymorphic type keys.
#
# @return [ Mongoid::Criteria] the resulting criteria, which may be
# the same as the input.
def with_polymorphic_criterion(criteria, base)
if polymorphic?
# 1. get the resolver for the inverse association
resolver = klass.reflect_on_association(as).resolver

# 2. look up the list of keys from the resolver, given base
keys = resolver.keys_for(base)

# 3. use equality if there is just one key, `in` if there are multiple
if keys.many?
criteria.where(type => { :$in => keys })
else
criteria.where(type => keys.first)
end
else
criteria
end
end
end
end
end
end
3 changes: 2 additions & 1 deletion lib/mongoid/attributes/nested.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ def accepts_nested_attributes_for(*args)
re_define_method(meth) do |attrs|
_assigning do
if association.polymorphic? and association.inverse_type
options = options.merge!(:class_name => self.send(association.inverse_type))
klass = association.resolver.model_for(send(association.inverse_type))
options = options.merge!(:class_name => klass)
end
association.nested_builder(attrs, options).build(self)
end
Expand Down
2 changes: 2 additions & 0 deletions lib/mongoid/composable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require "mongoid/collection_configurable"
require "mongoid/encryptable"
require "mongoid/findable"
require 'mongoid/identifiable'
require "mongoid/indexable"
require "mongoid/inspectable"
require "mongoid/interceptable"
Expand Down Expand Up @@ -44,6 +45,7 @@ module Composable
include Attributes
include Evolvable
include Fields
include Identifiable
include Indexable
include Inspectable
include Matchable
Expand Down
2 changes: 2 additions & 0 deletions lib/mongoid/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
require 'mongoid/association'
require 'mongoid/composable'
require 'mongoid/touchable'
require 'mongoid/model_resolver'

module Mongoid
# This is the base module for all domain objects that need to be persisted to
Expand All @@ -31,6 +32,7 @@ module Document

included do
Mongoid.register_model(self)
Mongoid::ModelResolver.register(self)
end

# Regex for matching illegal BSON keys.
Expand Down
28 changes: 28 additions & 0 deletions lib/mongoid/identifiable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

require 'mongoid/model_resolver'

module Mongoid
# Implements the "identify_as" interface (for specifying type aliases
# for document classes).
module Identifiable
extend ActiveSupport::Concern

class_methods do
# Specifies aliases that may be used to identify this document
# class in polymorphic situations. By default, classes are identified
# by their class names, but alternative aliases may be used instead,
# if desired.
#
# @param [ Array<String | Symbol> ] aliases the list of aliases to
# assign to this class.
# @param [ Mongoid::ModelResolver::Interface | Symbol | :default ] resolver the
# resolver instance to use when registering the type. If :default, the default
# `ModelResolver` instance will be used. If any other symbol, it must identify a
# previously registered ModelResolver instance.
def identify_as(*aliases, resolver: :default)
Mongoid::ModelResolver.resolver(resolver).register(self, *aliases)
end
end
end
end
Loading
Loading