Skip to content

Commit 607a199

Browse files
authored
MONGOID-5734 Custom polymorphic types (#5845)
* first pass at a global resolver registry * tests * fix problem with interpreting nested attribute data * need to register subclasses, too * raise custom exceptions when failing to resolve models * fix specs to implement functional around(:context) * trailing white space
1 parent 8e2b57b commit 607a199

File tree

25 files changed

+791
-42
lines changed

25 files changed

+791
-42
lines changed

lib/config/locales/en.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,22 @@ en:
680680
resolution: "The _type field is a reserved one used by Mongoid to determine the
681681
class for instantiating an object. Please don't save data in this field or ensure
682682
that any values in this field correspond to valid models."
683+
unrecognized_model_alias:
684+
message: "Cannot find any model with type %{model_alias}"
685+
summary: "A document is trying to load a polymorphic association, but the data refers to a type of object that can't be resolved (%{model_alias}). It might be that you've renamed the target class."
686+
resolution: "Register the old name as an alias on the refactored target object, using `identify_as`. This will allow Mongoid to find the target type even if the name no longer matches what was stored in the database."
687+
unrecognized_resolver:
688+
message: "The model resolver %{resolver} was referenced, but never registered."
689+
summary: "A polymorphic association has been configured to use a resolver
690+
named %{resolver}, but that resolver has not yet been registered. This
691+
might be a typo. Currently registered resolvers are: %{resolvers}."
692+
resolution: "Register custom resolvers with
693+
`Mongoid::ModelResolver.register_resolver` before attempting to query
694+
a polymorphic association."
695+
unregistered_class:
696+
message: "The class %{klass} is not registered with the resolver %{resolver}."
697+
summary: "A polymorphic association using the resolver %{resolver} has tried to link to a model of type %{klass}, but the resolver has no knowledge of any such model. This can happen if the association is configured to use a different resolver than the target mode."
698+
resolution: "Make sure the target model is registered with the same resolver as the polymorphic association, using `identify_as`."
683699
unsaved_document:
684700
message: "Attempted to save %{document} before the parent %{base}."
685701
summary: "You cannot call create or create! through the

lib/mongoid/association/accessors.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ def __build__(name, object, association, selected_fields = nil)
4242
#
4343
# @return [ Proxy ] The association.
4444
def create_relation(object, association, selected_fields = nil)
45-
type = @attributes[association.inverse_type]
45+
key = @attributes[association.inverse_type]
46+
type = key ? association.resolver.model_for(key) : nil
4647
target = if t = association.build(self, object, type, selected_fields)
4748
association.create_relation(self, t)
4849
else

lib/mongoid/association/nested/one.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,25 @@ def initialize(association, attributes, options)
5353
@attributes = attributes.with_indifferent_access
5454
@association = association
5555
@options = options
56-
@class_name = options[:class_name] ? options[:class_name].constantize : association.klass
56+
@class_name = class_from(options[:class_name])
5757
@destroy = @attributes.delete(:_destroy)
5858
end
5959

6060
private
6161

62+
# Coerces the argument into a class, or defaults to the association's class.
63+
#
64+
# @param [ String | Mongoid::Document | nil ] name_or_class the value to coerce
65+
#
66+
# @return [ Mongoid::Document ] the resulting class
67+
def class_from(name_or_class)
68+
case name_or_class
69+
when nil, false then association.klass
70+
when String then name_or_class.constantize
71+
else name_or_class
72+
end
73+
end
74+
6275
# Extracts and converts the id to the expected type.
6376
#
6477
# @return [ BSON::ObjectId | String | Object | nil ] The converted id,

lib/mongoid/association/referenced/belongs_to.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,21 @@ def polymorphic?
103103
@polymorphic ||= !!@options[:polymorphic]
104104
end
105105

106+
# Returns the object responsible for converting polymorphic type references into
107+
# class objects, and vice versa. This is obtained via the `:polymorphic` option
108+
# that was given when the association was defined.
109+
#
110+
# See Mongoid::ModelResolver.resolver for how the `:polymorphic` option is
111+
# interpreted here.
112+
#
113+
# @raise KeyError if no such resolver has been registered under the given
114+
# identifier.
115+
#
116+
# @return [ nil | Mongoid::ModelResolver ] the resolver to use
117+
def resolver
118+
@resolver ||= Mongoid::ModelResolver.resolver(@options[:polymorphic])
119+
end
120+
106121
# The name of the field used to store the type of polymorphic association.
107122
#
108123
# @return [ String ] The field used to store the type of polymorphic association.

lib/mongoid/association/referenced/belongs_to/binding.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ def bind_one
2323
binding do
2424
check_polymorphic_inverses!(_target)
2525
bind_foreign_key(_base, record_id(_target))
26-
bind_polymorphic_inverse_type(_base, _target.class.name)
26+
27+
# set the inverse type (e.g. "#{name}_type") for new polymorphic associations
28+
if _association.inverse_type && !_base.frozen?
29+
key = _association.resolver.default_key_for(_target)
30+
bind_polymorphic_inverse_type(_base, key)
31+
end
32+
2733
if inverse = _association.inverse(_target)
2834
if set_base_association
2935
if _base.referenced_many?

lib/mongoid/association/referenced/belongs_to/buildable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def execute_query(object, type)
3333
end
3434

3535
def query_criteria(object, type)
36-
cls = type ? type.constantize : relation_class
36+
cls = type ? (type.is_a?(String) ? type.constantize : type) : relation_class
3737
crit = cls.criteria
3838
crit = crit.apply_scope(scope)
3939
crit.where(primary_key => object)

lib/mongoid/association/referenced/has_many.rb

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require 'mongoid/association/referenced/has_many/proxy'
77
require 'mongoid/association/referenced/has_many/enumerable'
88
require 'mongoid/association/referenced/has_many/eager'
9+
require 'mongoid/association/referenced/with_polymorphic_criteria'
910

1011
module Mongoid
1112
module Association
@@ -15,6 +16,7 @@ module Referenced
1516
class HasMany
1617
include Relatable
1718
include Buildable
19+
include WithPolymorphicCriteria
1820

1921
# The options available for this type of association, in addition to the
2022
# common ones.
@@ -131,13 +133,20 @@ def type
131133
# @param [ Class ] object_class The object class.
132134
#
133135
# @return [ Mongoid::Criteria ] The criteria object.
136+
#
137+
# @deprecated in 9.0.x
138+
#
139+
# It appears as if this method is an artifact left over from a refactoring that renamed it
140+
# `with_polymorphic_criterion`, and made it private. Regardless, this method isn't referenced
141+
# anywhere else, and is unlikely to be useful to external clients. We should remove it.
134142
def add_polymorphic_criterion(criteria, object_class)
135143
if polymorphic?
136144
criteria.where(type => object_class.name)
137145
else
138146
criteria
139147
end
140148
end
149+
Mongoid.deprecate(self, :add_polymorphic_criterion)
141150

142151
# Is this association polymorphic?
143152
#
@@ -222,14 +231,6 @@ def query_criteria(object, base)
222231
with_ordering(crit)
223232
end
224233

225-
def with_polymorphic_criterion(criteria, base)
226-
if polymorphic?
227-
criteria.where(type => base.class.name)
228-
else
229-
criteria
230-
end
231-
end
232-
233234
def with_ordering(criteria)
234235
if order
235236
criteria.order_by(order)

lib/mongoid/association/referenced/has_one/buildable.rb

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
# frozen_string_literal: true
22
# rubocop:todo all
33

4+
require 'mongoid/association/referenced/with_polymorphic_criteria'
5+
46
module Mongoid
57
module Association
68
module Referenced
79
class HasOne
810

911
# The Builder behavior for has_one associations.
1012
module Buildable
13+
include WithPolymorphicCriteria
1114

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

60-
def with_polymorphic_criterion(criteria, base)
61-
if polymorphic?
62-
criteria.where(type => base.class.name)
63-
else
64-
criteria
65-
end
66-
end
67-
6863
def query?(object)
6964
object && !object.is_a?(Mongoid::Document)
7065
end
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Association
5+
module Referenced
6+
# Implements the `with_polymorphic_criteria` shared behavior.
7+
#
8+
# @api private
9+
module WithPolymorphicCriteria
10+
# If the receiver represents a polymorphic association, applies
11+
# the polymorphic search criteria to the given `criteria` object.
12+
#
13+
# @param [ Mongoid::Criteria ] criteria the criteria to append to
14+
# if receiver is polymorphic.
15+
# @param [ Mongoid::Document ] base the document to use when resolving
16+
# the polymorphic type keys.
17+
#
18+
# @return [ Mongoid::Criteria] the resulting criteria, which may be
19+
# the same as the input.
20+
def with_polymorphic_criterion(criteria, base)
21+
if polymorphic?
22+
# 1. get the resolver for the inverse association
23+
resolver = klass.reflect_on_association(as).resolver
24+
25+
# 2. look up the list of keys from the resolver, given base
26+
keys = resolver.keys_for(base)
27+
28+
# 3. use equality if there is just one key, `in` if there are multiple
29+
if keys.many?
30+
criteria.where(type => { :$in => keys })
31+
else
32+
criteria.where(type => keys.first)
33+
end
34+
else
35+
criteria
36+
end
37+
end
38+
end
39+
end
40+
end
41+
end

lib/mongoid/attributes/nested.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ def accepts_nested_attributes_for(*args)
6060
re_define_method(meth) do |attrs|
6161
_assigning do
6262
if association.polymorphic? and association.inverse_type
63-
options = options.merge!(:class_name => self.send(association.inverse_type))
63+
klass = association.resolver.model_for(send(association.inverse_type))
64+
options = options.merge!(:class_name => klass)
6465
end
6566
association.nested_builder(attrs, options).build(self)
6667
end

0 commit comments

Comments
 (0)