mongoid 9.1.0 → 9.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/config/locales/en.yml +69 -0
- data/lib/mongoid/association/accessors.rb +27 -5
- data/lib/mongoid/association/depending.rb +14 -10
- data/lib/mongoid/association/eager.rb +9 -0
- data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +88 -0
- data/lib/mongoid/association/eager_load/embedded_distributor.rb +151 -0
- data/lib/mongoid/association/eager_load/inclusion.rb +173 -0
- data/lib/mongoid/association/eager_load/inclusion_tree.rb +96 -0
- data/lib/mongoid/association/eager_load/lookup_pipeline.rb +121 -0
- data/lib/mongoid/association/eager_load/polymorphic_preloader.rb +61 -0
- data/lib/mongoid/association/eager_load/polymorphic_targets.rb +151 -0
- data/lib/mongoid/association/eager_loadable.rb +65 -101
- data/lib/mongoid/association/embedded/batchable.rb +17 -1
- data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -1
- data/lib/mongoid/association/embedded/embeds_one.rb +1 -0
- data/lib/mongoid/association/macros.rb +13 -1
- data/lib/mongoid/association/nested/many.rb +34 -5
- data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
- data/lib/mongoid/association/nested/one.rb +6 -1
- data/lib/mongoid/association/options.rb +17 -0
- data/lib/mongoid/association/referenced/belongs_to/eager.rb +6 -1
- data/lib/mongoid/association/referenced/belongs_to.rb +1 -0
- data/lib/mongoid/association/referenced/counter_cache.rb +3 -3
- data/lib/mongoid/association/referenced/has_and_belongs_to_many/buildable.rb +6 -0
- data/lib/mongoid/association/referenced/has_and_belongs_to_many.rb +5 -0
- data/lib/mongoid/association/referenced/has_many/enumerable.rb +3 -1
- data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
- data/lib/mongoid/association/referenced/has_many_through/eager.rb +99 -0
- data/lib/mongoid/association/referenced/has_many_through/proxy.rb +68 -0
- data/lib/mongoid/association/referenced/has_many_through.rb +201 -0
- data/lib/mongoid/association/referenced/has_one.rb +1 -0
- data/lib/mongoid/association/referenced/has_one_through/eager.rb +65 -0
- data/lib/mongoid/association/referenced/has_one_through/proxy.rb +30 -0
- data/lib/mongoid/association/referenced/has_one_through.rb +167 -0
- data/lib/mongoid/association/referenced.rb +2 -0
- data/lib/mongoid/association/relatable.rb +58 -0
- data/lib/mongoid/association.rb +15 -0
- data/lib/mongoid/atomic/modifiers.rb +100 -16
- data/lib/mongoid/clients/factory.rb +9 -13
- data/lib/mongoid/collection_configurable.rb +8 -0
- data/lib/mongoid/config/defaults.rb +0 -1
- data/lib/mongoid/config/encryption.rb +170 -22
- data/lib/mongoid/config.rb +56 -5
- data/lib/mongoid/contextual/aggregable/memory.rb +6 -2
- data/lib/mongoid/contextual/memory.rb +18 -7
- data/lib/mongoid/criteria/includable.rb +35 -11
- data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
- data/lib/mongoid/criteria/queryable/selectable.rb +123 -7
- data/lib/mongoid/deprecable.rb +45 -2
- data/lib/mongoid/document.rb +3 -0
- data/lib/mongoid/encryptable.rb +73 -0
- data/lib/mongoid/errors/config_redactor.rb +43 -0
- data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
- data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
- data/lib/mongoid/errors/mixed_client_configuration.rb +1 -1
- data/lib/mongoid/errors/no_client_database.rb +1 -1
- data/lib/mongoid/errors/no_client_hosts.rb +1 -1
- data/lib/mongoid/errors/no_encryption_schema.rb +30 -0
- data/lib/mongoid/errors/readonly_association.rb +29 -0
- data/lib/mongoid/errors.rb +5 -0
- data/lib/mongoid/extensions/vector.rb +64 -0
- data/lib/mongoid/extensions.rb +1 -0
- data/lib/mongoid/field_readable.rb +69 -0
- data/lib/mongoid/fields.rb +7 -0
- data/lib/mongoid/indexable.rb +38 -26
- data/lib/mongoid/matchable.rb +6 -1
- data/lib/mongoid/matcher/eq_impl_with_regexp.rb +2 -4
- data/lib/mongoid/matcher/regex.rb +11 -12
- data/lib/mongoid/matcher/regexp_budget.rb +372 -0
- data/lib/mongoid/matcher.rb +1 -0
- data/lib/mongoid/persistable/creatable.rb +120 -14
- data/lib/mongoid/persistable/updatable.rb +9 -11
- data/lib/mongoid/persistable.rb +15 -15
- data/lib/mongoid/persistence_context.rb +77 -0
- data/lib/mongoid/search_indexable.rb +69 -24
- data/lib/mongoid/serializable.rb +12 -4
- data/lib/mongoid/tasks/database.rb +33 -16
- data/lib/mongoid/threaded.rb +36 -0
- data/lib/mongoid/timestamps/timeless.rb +84 -10
- data/lib/mongoid/touchable.rb +28 -1
- data/lib/mongoid/traversable.rb +2 -2
- data/lib/mongoid/validatable/associated.rb +18 -2
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +4 -0
- data/spec/integration/app_spec.rb +7 -1
- data/spec/integration/associations/belongs_to_spec.rb +13 -0
- data/spec/integration/associations/embedded_delayed_atomic_sets_spec.rb +216 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +31 -2
- data/spec/integration/atomic/modifiers_spec.rb +283 -0
- data/spec/integration/dots_and_dollars_spec.rb +12 -2
- data/spec/integration/encryption_spec.rb +300 -0
- data/spec/integration/matcher_operator_data/regex.yml +21 -0
- data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
- data/spec/integration/query_operator_guard_spec.rb +89 -0
- data/spec/mongoid/association/accessors_spec.rb +35 -0
- data/spec/mongoid/association/eager_loadable_spec.rb +47 -0
- data/spec/mongoid/association/eager_spec.rb +2 -2
- data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +33 -0
- data/spec/mongoid/association/fallback_spec.rb +425 -0
- data/spec/mongoid/association/macros_spec.rb +26 -0
- data/spec/mongoid/association/referenced/belongs_to/eager_spec.rb +17 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/buildable_spec.rb +11 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb +50 -0
- data/spec/mongoid/association/referenced/has_many/eager_spec.rb +33 -0
- data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
- data/spec/mongoid/association/referenced/has_many_through/eager_spec.rb +58 -0
- data/spec/mongoid/association/referenced/has_many_through/proxy_spec.rb +62 -0
- data/spec/mongoid/association/referenced/has_many_through_spec.rb +399 -0
- data/spec/mongoid/association/referenced/has_one_through/eager_spec.rb +58 -0
- data/spec/mongoid/association/referenced/has_one_through/proxy_spec.rb +49 -0
- data/spec/mongoid/association/referenced/has_one_through_spec.rb +211 -0
- data/spec/mongoid/atomic/modifiers_spec.rb +347 -0
- data/spec/mongoid/attributes/nested_spec.rb +278 -8
- data/spec/mongoid/clients/factory_spec.rb +36 -0
- data/spec/mongoid/config/defaults_spec.rb +33 -0
- data/spec/mongoid/config/encryption_spec.rb +451 -0
- data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
- data/spec/mongoid/contextual/memory_spec.rb +177 -0
- data/spec/mongoid/criteria/includable_spec.rb +1221 -0
- data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +15 -1
- data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +201 -0
- data/spec/mongoid/criteria_spec.rb +3 -0
- data/spec/mongoid/encryptable_spec.rb +115 -0
- data/spec/mongoid/errors/config_redactor_spec.rb +112 -0
- data/spec/mongoid/errors/mixed_client_configuration_spec.rb +31 -0
- data/spec/mongoid/errors/no_client_database_spec.rb +29 -0
- data/spec/mongoid/errors/no_client_hosts_spec.rb +26 -0
- data/spec/mongoid/errors/readonly_association_spec.rb +34 -0
- data/spec/mongoid/extensions/vector_spec.rb +175 -0
- data/spec/mongoid/fields_spec.rb +8 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
- data/spec/mongoid/search_indexable_spec.rb +321 -9
- data/spec/mongoid/tasks/database_spec.rb +31 -0
- data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
- data/spec/mongoid/touchable_spec.rb +242 -0
- data/spec/mongoid/touchable_spec_models.rb +47 -0
- data/spec/mongoid/validatable/associated_spec.rb +19 -0
- data/spec/spec_helper.rb +5 -1
- data/spec/support/crypt/models.rb +253 -0
- data/spec/support/models/modifier_models.rb +65 -0
- metadata +55 -2
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mongoid/association/referenced/has_many_through/proxy'
|
|
4
|
+
require 'mongoid/association/referenced/has_many_through/eager'
|
|
5
|
+
|
|
6
|
+
module Mongoid
|
|
7
|
+
module Association
|
|
8
|
+
module Referenced
|
|
9
|
+
# Metadata class for has_many :through associations.
|
|
10
|
+
class HasManyThrough
|
|
11
|
+
include Relatable
|
|
12
|
+
|
|
13
|
+
# The options available for this type of association, in addition to the
|
|
14
|
+
# common ones.
|
|
15
|
+
#
|
|
16
|
+
# @return [ Array<Symbol> ] The extra valid options.
|
|
17
|
+
ASSOCIATION_OPTIONS = %i[order source through].freeze
|
|
18
|
+
|
|
19
|
+
# The complete list of valid options for this association, including
|
|
20
|
+
# the shared ones.
|
|
21
|
+
#
|
|
22
|
+
# @return [ Array<Symbol> ] The valid options.
|
|
23
|
+
VALID_OPTIONS = (ASSOCIATION_OPTIONS + SHARED_OPTIONS).freeze
|
|
24
|
+
|
|
25
|
+
# The list of association complements.
|
|
26
|
+
#
|
|
27
|
+
# @return [ Array ]
|
|
28
|
+
def relation_complements
|
|
29
|
+
[].freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Setup instance methods on the owner class.
|
|
33
|
+
#
|
|
34
|
+
# @return [ self ]
|
|
35
|
+
def setup!
|
|
36
|
+
setup_instance_methods!
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Is this association embedded?
|
|
41
|
+
#
|
|
42
|
+
# @return [ false ]
|
|
43
|
+
def embedded?
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The proxy class for this association type.
|
|
48
|
+
#
|
|
49
|
+
# @return [ Class ]
|
|
50
|
+
def relation
|
|
51
|
+
Proxy
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Through associations never store a foreign key on the owner document.
|
|
55
|
+
#
|
|
56
|
+
# @return [ false ]
|
|
57
|
+
def stores_foreign_key?
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The intermediate association metadata on the owner class.
|
|
62
|
+
# Resolved lazily to allow forward references.
|
|
63
|
+
#
|
|
64
|
+
# @return [ Mongoid::Association::Relatable ]
|
|
65
|
+
def through_association
|
|
66
|
+
@through_association ||= begin
|
|
67
|
+
assoc = @owner_class.relations[@options[:through].to_s] ||
|
|
68
|
+
raise(
|
|
69
|
+
Errors::InvalidRelationOption.new(
|
|
70
|
+
@owner_class, name, :through, @options[:through]
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
if assoc.embedded?
|
|
74
|
+
raise(
|
|
75
|
+
Errors::InvalidRelationOption.new(
|
|
76
|
+
@owner_class, name, :through,
|
|
77
|
+
'through association must be a referenced association, not embedded'
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
assoc
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The source association metadata on the intermediate class.
|
|
86
|
+
# Resolved lazily to allow forward references.
|
|
87
|
+
#
|
|
88
|
+
# @return [ Mongoid::Association::Relatable ]
|
|
89
|
+
def source_association
|
|
90
|
+
@source_association ||= begin
|
|
91
|
+
source_name = (@options[:source] || name.to_s.singularize).to_s
|
|
92
|
+
assoc = through_association.klass.relations[source_name] ||
|
|
93
|
+
raise(
|
|
94
|
+
Errors::InvalidRelationOption.new(
|
|
95
|
+
@owner_class, name, :source, source_name
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
if assoc.is_a?(Referenced::HasAndBelongsToMany)
|
|
99
|
+
raise(
|
|
100
|
+
Errors::InvalidRelationOption.new(
|
|
101
|
+
@owner_class, name, :source,
|
|
102
|
+
'has_and_belongs_to_many is not supported as a :through source'
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
assoc
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Return a Criteria scoped to the target documents reachable from base
|
|
111
|
+
# via the through association. Performs two queries: one against the
|
|
112
|
+
# intermediate collection, one against the source collection.
|
|
113
|
+
#
|
|
114
|
+
# @param [ Document ] base The owner document.
|
|
115
|
+
#
|
|
116
|
+
# @return [ Mongoid::Criteria ]
|
|
117
|
+
def criteria(base)
|
|
118
|
+
through_crit = through_association.criteria(base)
|
|
119
|
+
|
|
120
|
+
crit = if source_association.stores_foreign_key?
|
|
121
|
+
# FK is on the intermediate (e.g. appointment.patient_id -> belongs_to :patient)
|
|
122
|
+
target_pk = source_association.primary_key # '_id' on Patient
|
|
123
|
+
source_fk = source_association.foreign_key # 'patient_id' on Appointment
|
|
124
|
+
source_association.klass.where(
|
|
125
|
+
target_pk => { '$in' => key_values(through_crit, source_fk) }
|
|
126
|
+
)
|
|
127
|
+
else
|
|
128
|
+
# FK is on the source (e.g. reader.book_id -> has_many :readers on Book)
|
|
129
|
+
source_pk = source_association.primary_key # '_id' on intermediate (Book)
|
|
130
|
+
source_fk = source_association.foreign_key # 'book_id' on Reader
|
|
131
|
+
source_association.klass.where(
|
|
132
|
+
source_fk => { '$in' => key_values(through_crit, source_pk) }
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
order ? crit.order_by(order) : crit
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The default for validating the association object.
|
|
139
|
+
#
|
|
140
|
+
# @return [ false ]
|
|
141
|
+
def validation_default
|
|
142
|
+
false
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
# Pluck the given key from the criteria, dropping intermediates that
|
|
148
|
+
# have no value for it. A nil in an $in array matches every document
|
|
149
|
+
# whose queried field is null or absent, which would return documents
|
|
150
|
+
# outside the association. An empty result correctly matches nothing.
|
|
151
|
+
# Mirrors what the eager loader does.
|
|
152
|
+
#
|
|
153
|
+
# @param [ Mongoid::Criteria ] criteria The intermediate criteria.
|
|
154
|
+
# @param [ String ] key The key to pluck.
|
|
155
|
+
#
|
|
156
|
+
# @return [ Array<Object> ] The usable key values.
|
|
157
|
+
def key_values(criteria, key)
|
|
158
|
+
criteria.pluck(key).compact.uniq
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def setup_instance_methods!
|
|
162
|
+
define_through_getter!
|
|
163
|
+
define_through_ids_getter!
|
|
164
|
+
define_readonly_setter!
|
|
165
|
+
define_existence_check!
|
|
166
|
+
self
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def define_through_getter!
|
|
170
|
+
assoc = self
|
|
171
|
+
assoc_name = name
|
|
172
|
+
@owner_class.re_define_method(assoc_name) do |reload = false|
|
|
173
|
+
if reload || !instance_variable_defined?("@_#{assoc_name}")
|
|
174
|
+
set_relation(assoc_name, HasManyThrough::Proxy.new(self, assoc))
|
|
175
|
+
end
|
|
176
|
+
instance_variable_get("@_#{assoc_name}")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def define_through_ids_getter!
|
|
181
|
+
assoc_name = name
|
|
182
|
+
ids_method = :"#{assoc_name.to_s.singularize}_ids"
|
|
183
|
+
@owner_class.re_define_method(ids_method) do
|
|
184
|
+
send(assoc_name).pluck(:_id)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def define_readonly_setter!
|
|
189
|
+
assoc = self
|
|
190
|
+
@owner_class.re_define_method(:"#{name}=") do |_object|
|
|
191
|
+
raise Mongoid::Errors::ReadonlyAssociation.new(self.class, assoc)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def default_primary_key
|
|
196
|
+
PRIMARY_KEY_DEFAULT
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid
|
|
4
|
+
module Association
|
|
5
|
+
module Referenced
|
|
6
|
+
class HasOneThrough
|
|
7
|
+
# Two-query eager preloader for has_one :through associations.
|
|
8
|
+
class Eager < Association::Eager
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def preload
|
|
12
|
+
@docs.each { |d| set_relation(d, nil) }
|
|
13
|
+
|
|
14
|
+
through_assoc = @association.through_association
|
|
15
|
+
source_assoc = @association.source_association
|
|
16
|
+
|
|
17
|
+
owner_pk = through_assoc.primary_key
|
|
18
|
+
through_fk = through_assoc.foreign_key
|
|
19
|
+
|
|
20
|
+
owner_ids = @docs.filter_map { |d| d.public_send(owner_pk) }.uniq
|
|
21
|
+
return if owner_ids.empty?
|
|
22
|
+
|
|
23
|
+
intermediates = through_assoc.klass.where(through_fk => { '$in' => owner_ids }).to_a
|
|
24
|
+
intermediate_to_target = build_intermediate_to_target(intermediates, through_fk, source_assoc)
|
|
25
|
+
|
|
26
|
+
@docs.each do |doc|
|
|
27
|
+
owner_key_val = doc.public_send(owner_pk)
|
|
28
|
+
target = intermediate_to_target[owner_key_val]
|
|
29
|
+
proxy = target ? HasOneThrough::Proxy.new(doc, target, @association) : nil
|
|
30
|
+
doc.set_relation(@association.name, proxy) unless doc.blank?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build_intermediate_to_target(intermediates, through_fk, source_assoc)
|
|
35
|
+
if source_assoc.stores_foreign_key?
|
|
36
|
+
# FK is on the intermediate (e.g. belongs_to :store => intermediate.store_id)
|
|
37
|
+
source_fk_values = intermediates.filter_map { |i| i.public_send(source_assoc.foreign_key) }.uniq
|
|
38
|
+
targets = source_assoc.klass.where(source_assoc.primary_key => { '$in' => source_fk_values }).to_a
|
|
39
|
+
targets_by_key = targets.index_by { |t| t.public_send(source_assoc.primary_key) }
|
|
40
|
+
intermediates.to_h do |i|
|
|
41
|
+
[ i.public_send(through_fk), targets_by_key[i.public_send(source_assoc.foreign_key)] ]
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
# FK is on the source (e.g. has_one :store => store.franchise_id)
|
|
45
|
+
source_pk = source_assoc.primary_key
|
|
46
|
+
intermediate_pks = intermediates.filter_map { |i| i.public_send(source_pk) }.uniq
|
|
47
|
+
targets = source_assoc.klass.where(source_assoc.foreign_key => { '$in' => intermediate_pks }).to_a
|
|
48
|
+
targets_by_fk = targets.index_by { |t| t.public_send(source_assoc.foreign_key) }
|
|
49
|
+
intermediates.index_by { |i| i.public_send(through_fk) }.transform_values do |i|
|
|
50
|
+
targets_by_fk[i.public_send(source_pk)]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Required by the base class contract. Not called by this preloader
|
|
56
|
+
# because preload manages document traversal directly without using
|
|
57
|
+
# the grouped_docs / keys_from_docs machinery from the base class.
|
|
58
|
+
def group_by_key
|
|
59
|
+
@association.through_association.primary_key
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid
|
|
4
|
+
module Association
|
|
5
|
+
module Referenced
|
|
6
|
+
class HasOneThrough
|
|
7
|
+
# Read-only proxy for has_one :through associations.
|
|
8
|
+
# Instances are returned by the association getter. Write attempts raise
|
|
9
|
+
# ReadonlyAssociation.
|
|
10
|
+
class Proxy < Association::One
|
|
11
|
+
module ClassMethods
|
|
12
|
+
def eager_loader(association, docs)
|
|
13
|
+
Eager.new(association, docs)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns true if the association is an embedded one. In this case
|
|
17
|
+
# always false.
|
|
18
|
+
#
|
|
19
|
+
# @return [ false ] Always false.
|
|
20
|
+
def embedded?
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
extend ClassMethods
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mongoid/association/referenced/has_one_through/proxy'
|
|
4
|
+
require 'mongoid/association/referenced/has_one_through/eager'
|
|
5
|
+
|
|
6
|
+
module Mongoid
|
|
7
|
+
module Association
|
|
8
|
+
module Referenced
|
|
9
|
+
# Metadata class for has_one :through associations.
|
|
10
|
+
class HasOneThrough
|
|
11
|
+
include Relatable
|
|
12
|
+
|
|
13
|
+
# The options available for this type of association, in addition to the
|
|
14
|
+
# common ones.
|
|
15
|
+
#
|
|
16
|
+
# @return [ Array<Symbol> ] The extra valid options.
|
|
17
|
+
ASSOCIATION_OPTIONS = %i[source through].freeze
|
|
18
|
+
|
|
19
|
+
# The complete list of valid options for this association, including
|
|
20
|
+
# the shared ones.
|
|
21
|
+
#
|
|
22
|
+
# @return [ Array<Symbol> ] The valid options.
|
|
23
|
+
VALID_OPTIONS = (ASSOCIATION_OPTIONS + SHARED_OPTIONS).freeze
|
|
24
|
+
|
|
25
|
+
# The list of association complements.
|
|
26
|
+
#
|
|
27
|
+
# @return [ Array ]
|
|
28
|
+
def relation_complements
|
|
29
|
+
[].freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Setup instance methods on the owner class.
|
|
33
|
+
#
|
|
34
|
+
# @return [ self ]
|
|
35
|
+
def setup!
|
|
36
|
+
setup_instance_methods!
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Is this association embedded?
|
|
41
|
+
#
|
|
42
|
+
# @return [ false ]
|
|
43
|
+
def embedded?
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The proxy class for this association type.
|
|
48
|
+
#
|
|
49
|
+
# @return [ Class ]
|
|
50
|
+
def relation
|
|
51
|
+
Proxy
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The intermediate association metadata on the owner class.
|
|
55
|
+
# Resolved lazily to allow forward references.
|
|
56
|
+
#
|
|
57
|
+
# @return [ Mongoid::Association::Relatable ]
|
|
58
|
+
def through_association
|
|
59
|
+
@through_association ||= begin
|
|
60
|
+
assoc = @owner_class.relations[@options[:through].to_s] ||
|
|
61
|
+
raise(
|
|
62
|
+
Errors::InvalidRelationOption.new(
|
|
63
|
+
@owner_class, name, :through, @options[:through]
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
if assoc.embedded?
|
|
67
|
+
raise(
|
|
68
|
+
Errors::InvalidRelationOption.new(
|
|
69
|
+
@owner_class, name, :through,
|
|
70
|
+
'through association must be a referenced association, not embedded'
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
assoc
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The source association metadata on the intermediate class.
|
|
79
|
+
# Resolved lazily to allow forward references.
|
|
80
|
+
#
|
|
81
|
+
# @return [ Mongoid::Association::Relatable ]
|
|
82
|
+
def source_association
|
|
83
|
+
@source_association ||= begin
|
|
84
|
+
source_name = (@options[:source] || name).to_s
|
|
85
|
+
assoc = through_association.klass.relations[source_name] ||
|
|
86
|
+
raise(
|
|
87
|
+
Errors::InvalidRelationOption.new(
|
|
88
|
+
@owner_class, name, :source, source_name
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
if assoc.is_a?(Referenced::HasAndBelongsToMany)
|
|
92
|
+
raise(
|
|
93
|
+
Errors::InvalidRelationOption.new(
|
|
94
|
+
@owner_class, name, :source,
|
|
95
|
+
'has_and_belongs_to_many is not supported as a :through source'
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
assoc
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Through associations never store a foreign key on the owner document.
|
|
104
|
+
#
|
|
105
|
+
# @return [ false ]
|
|
106
|
+
def stores_foreign_key?
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Resolve the target by delegating through the intermediate proxy.
|
|
111
|
+
# Unlike other association types, this returns a document (or nil)
|
|
112
|
+
# directly rather than a Mongoid::Criteria, because the two-hop
|
|
113
|
+
# traversal is performed eagerly via the existing association proxy.
|
|
114
|
+
#
|
|
115
|
+
# @param [ Document ] base The owner document.
|
|
116
|
+
#
|
|
117
|
+
# @return [ Document | nil ]
|
|
118
|
+
def criteria(base)
|
|
119
|
+
through_target = base.public_send(through_association.name)
|
|
120
|
+
return nil if through_target.nil?
|
|
121
|
+
|
|
122
|
+
through_target.public_send(source_association.name)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The default for validating the association object.
|
|
126
|
+
#
|
|
127
|
+
# @return [ false ]
|
|
128
|
+
def validation_default
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def setup_instance_methods!
|
|
135
|
+
define_through_getter!
|
|
136
|
+
define_readonly_setter!
|
|
137
|
+
define_existence_check!
|
|
138
|
+
self
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def define_through_getter!
|
|
142
|
+
assoc = self
|
|
143
|
+
assoc_name = name
|
|
144
|
+
@owner_class.re_define_method(assoc_name) do |reload = false|
|
|
145
|
+
if reload || !instance_variable_defined?("@_#{assoc_name}")
|
|
146
|
+
doc = assoc.criteria(self)
|
|
147
|
+
proxy = doc ? HasOneThrough::Proxy.new(self, doc, assoc) : nil
|
|
148
|
+
set_relation(assoc_name, proxy)
|
|
149
|
+
end
|
|
150
|
+
instance_variable_get("@_#{assoc_name}")
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def define_readonly_setter!
|
|
155
|
+
assoc = self
|
|
156
|
+
@owner_class.re_define_method("#{name}=") do |_object|
|
|
157
|
+
raise Mongoid::Errors::ReadonlyAssociation.new(self.class, assoc)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def default_primary_key
|
|
162
|
+
PRIMARY_KEY_DEFAULT
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -7,3 +7,5 @@ require 'mongoid/association/referenced/belongs_to'
|
|
|
7
7
|
require 'mongoid/association/referenced/has_many'
|
|
8
8
|
require 'mongoid/association/referenced/has_and_belongs_to_many'
|
|
9
9
|
require 'mongoid/association/referenced/has_one'
|
|
10
|
+
require 'mongoid/association/referenced/has_one_through'
|
|
11
|
+
require 'mongoid/association/referenced/has_many_through'
|
|
@@ -177,6 +177,23 @@ module Mongoid
|
|
|
177
177
|
end
|
|
178
178
|
alias klass relation_class
|
|
179
179
|
|
|
180
|
+
# The class of the association target, or nil when the named class is
|
|
181
|
+
# not defined.
|
|
182
|
+
#
|
|
183
|
+
# An association may name a class that never gets defined. The
|
|
184
|
+
# association is then unusable, but its owner still has to be. Callers
|
|
185
|
+
# that walk every association of every model, rather than following the
|
|
186
|
+
# one the application asked for, use this instead of relation_class.
|
|
187
|
+
#
|
|
188
|
+
# @return [ Class | nil ] The association objects' class.
|
|
189
|
+
#
|
|
190
|
+
# @api private
|
|
191
|
+
def try_relation_class
|
|
192
|
+
relation_class
|
|
193
|
+
rescue NameError
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
|
|
180
197
|
# The class name of the object owning this association.
|
|
181
198
|
#
|
|
182
199
|
# @return [ String ] The owning objects' class name.
|
|
@@ -200,6 +217,24 @@ module Mongoid
|
|
|
200
217
|
stores_foreign_key? ? foreign_key : primary_key
|
|
201
218
|
end
|
|
202
219
|
|
|
220
|
+
# The field a $lookup writes this association's preloaded documents into.
|
|
221
|
+
#
|
|
222
|
+
# @return [ String ] The field.
|
|
223
|
+
def preloaded_field
|
|
224
|
+
"#{PRELOADED_PREFIX}#{name}"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# A field where the aggregation collects this association's documents
|
|
228
|
+
# before moving them to where they belong. The qualifier tells it apart
|
|
229
|
+
# from the other temporary fields of the same association.
|
|
230
|
+
#
|
|
231
|
+
# @param [ String ] qualifier What tells this temporary field apart.
|
|
232
|
+
#
|
|
233
|
+
# @return [ String ] The field.
|
|
234
|
+
def temporary_field(qualifier)
|
|
235
|
+
"#{preloaded_field}_#{qualifier}"
|
|
236
|
+
end
|
|
237
|
+
|
|
203
238
|
# The name of the setter on this object for assigning an associated object.
|
|
204
239
|
#
|
|
205
240
|
# @return [ String ] The setter name.
|
|
@@ -342,6 +377,15 @@ module Mongoid
|
|
|
342
377
|
[ Referenced::BelongsTo, Embedded::EmbeddedIn ].any? { |a| is_a?(a) }
|
|
343
378
|
end
|
|
344
379
|
|
|
380
|
+
# Is this association a many-to-many association? Such an association stores
|
|
381
|
+
# its foreign keys as an array on the document itself.
|
|
382
|
+
#
|
|
383
|
+
# @return [ true | false ] true if it is a has_and_belongs_to_many
|
|
384
|
+
# association, false if not.
|
|
385
|
+
def many_to_many?
|
|
386
|
+
is_a?(Referenced::HasAndBelongsToMany)
|
|
387
|
+
end
|
|
388
|
+
|
|
345
389
|
private
|
|
346
390
|
|
|
347
391
|
# Gets the model classes with inverse associations of this model. This is used to determine
|
|
@@ -413,6 +457,20 @@ module Mongoid
|
|
|
413
457
|
end
|
|
414
458
|
end
|
|
415
459
|
|
|
460
|
+
if @options.key?(:fallback)
|
|
461
|
+
unless @options[:fallback].respond_to?(:call)
|
|
462
|
+
raise ArgumentError,
|
|
463
|
+
"The :fallback option for association '#{name}' on " \
|
|
464
|
+
"#{@owner_class} must be a Proc or lambda."
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
if @options.key?(:autobuild)
|
|
468
|
+
raise ArgumentError,
|
|
469
|
+
"The :fallback option for association '#{name}' on " \
|
|
470
|
+
"#{@owner_class} cannot be combined with :autobuild."
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
416
474
|
[ name, :"#{name}?", :"#{name}=" ].each do |n|
|
|
417
475
|
raise Errors::InvalidRelation.new(@owner_class, n) if Mongoid.destructive_fields.include?(n)
|
|
418
476
|
end
|
data/lib/mongoid/association.rb
CHANGED
|
@@ -33,6 +33,14 @@ module Mongoid
|
|
|
33
33
|
include Macros
|
|
34
34
|
include Reflections
|
|
35
35
|
|
|
36
|
+
# Prefixes the field a $lookup writes an association's preloaded documents
|
|
37
|
+
# into. They travel apart from the document's own fields, so the hydration
|
|
38
|
+
# can keep them out of the attributes instead of leaving them on the
|
|
39
|
+
# document, where a later save would persist them.
|
|
40
|
+
#
|
|
41
|
+
# @api private
|
|
42
|
+
PRELOADED_PREFIX = '__mongoid_eager_loaded_'
|
|
43
|
+
|
|
36
44
|
# Map the macros to their corresponding Association classes.
|
|
37
45
|
#
|
|
38
46
|
# @return [ Hash ] The mapping from macros to their Association class.
|
|
@@ -46,6 +54,13 @@ module Mongoid
|
|
|
46
54
|
belongs_to: Association::Referenced::BelongsTo
|
|
47
55
|
}.freeze
|
|
48
56
|
|
|
57
|
+
# Internal mapping used when :through option is present. Not exposed as
|
|
58
|
+
# callable macros.
|
|
59
|
+
THROUGH_MACRO_MAPPING = {
|
|
60
|
+
has_one: Association::Referenced::HasOneThrough,
|
|
61
|
+
has_many: Association::Referenced::HasManyThrough
|
|
62
|
+
}.freeze
|
|
63
|
+
|
|
49
64
|
attr_accessor :_association
|
|
50
65
|
|
|
51
66
|
included do
|