Module: Mongoid::Association::Accessors
Relationships & Source Files | |
Extension / Inclusion / Inheritance Descendants | |
Included In:
| |
Super Chains via Extension / Inclusion / Inheritance | |
Class Chain:
self,
ActiveSupport::Concern
|
|
Defined in: | lib/mongoid/association/accessors.rb |
Overview
This module contains all the behavior related to accessing associations through the getters and setters, and how to delegate to builders to create new ones.
Class Method Summary
-
.define_builder!(association) ⇒ Class
private
Defines a builder method for an embeds_one association.
-
.define_creator!(association) ⇒ Class
private
Defines a creator method for an embeds_one association.
-
.define_existence_check!(association) ⇒ Class
private
Adds the existence check for associations.
-
.define_getter!(association) ⇒ Class
private
Defines the getter for the association.
-
.define_ids_getter!(association) ⇒ Class
private
Defines the getter for the ids of documents in the association.
-
.define_ids_setter!(association)
private
Defines the setter method that allows you to set documents in this association by their ids.
-
.define_setter!(association) ⇒ Class
private
Defines the setter for the association.
Instance Attribute Summary
-
#without_autobuild? ⇒ true | false
readonly
private
Is the current code executing without autobuild functionality?
Instance Method Summary
-
#__build__(name, object, association, selected_fields = nil) ⇒ Proxy
Builds the related document and creates the association unless the document is nil, then sets the association on this document.
-
#create_relation(object, association, selected_fields = nil) ⇒ Proxy
Create an association from an object and association metadata.
-
#reset_relation_criteria(name)
Resets the criteria inside the association proxy.
-
#set_relation(name, relation) ⇒ Proxy
::Set
the supplied association to an instance variable on the class with the provided name. -
#_mongoid_filter_selected_fields(assoc_key) ⇒ Hash | nil
private
Internal use only
Internal use only
Returns a subset of __selected_fields attribute applicable to the (embedded) association with the given key, or nil if no projection is to be performed.
-
#get_relation(name, association, object, reload = false) ⇒ Proxy
private
Internal use only
Internal use only
Get the association.
- #needs_no_database_query?(object, association) ⇒ Boolean private
-
#parse_args(*args) ⇒ Array<Hash>
private
Parse out the attributes and the options from the args passed to a build_ or create_ methods.
-
#without_autobuild ⇒ Object
readonly
private
Yield to the block with autobuild functionality turned off.
Class Method Details
.define_builder!(association) ⇒ Class
(private)
Defines a builder method for an embeds_one association. This is defined as #build_name.
# File 'lib/mongoid/association/accessors.rb', line 394
def self.define_builder!(association) name = association.name association.inverse_class.tap do |klass| klass.re_define_method("build_#{name}") do |*args| attributes, = parse_args(*args) document = Factory.build(association.relation_class, attributes) _building do child = send("#{name}=", document) child.run_callbacks(:build) child end end end end
.define_creator!(association) ⇒ Class
(private)
Defines a creator method for an embeds_one association. This is defined as #create_name. After the object is built it will immediately save.
# File 'lib/mongoid/association/accessors.rb', line 419
def self.define_creator!(association) name = association.name association.inverse_class.tap do |klass| klass.re_define_method("create_#{name}") do |*args| attributes, = parse_args(*args) document = Factory.build(association.klass, attributes) doc = _assigning do send("#{name}=", document) end doc.save save if new_record? && association.stores_foreign_key? doc end end end
.define_existence_check!(association) ⇒ Class
(private)
Adds the existence check for associations.
# File 'lib/mongoid/association/accessors.rb', line 280
def self.define_existence_check!(association) name = association.name association.inverse_class.tap do |klass| klass.module_eval <<-END, __FILE__, __LINE__ + 1 def #{name}? without_autobuild { !__send__(:#{name}).blank? } end alias :has_#{name}? :#{name}? END end end
.define_getter!(association) ⇒ Class
(private)
Defines the getter for the association. Nothing too special here: just return the instance variable for the association if it exists or build the thing.
# File 'lib/mongoid/association/accessors.rb', line 302
def self.define_getter!(association) name = association.name association.inverse_class.tap do |klass| klass.re_define_method(name) do |reload = false| value = get_relation(name, association, nil, reload) if value.nil? && association.autobuilding? && !without_autobuild? value = send("build_#{name}") end value end end end
.define_ids_getter!(association) ⇒ Class
(private)
Defines the getter for the ids of documents in the association. Should be specify only for referenced many associations.
# File 'lib/mongoid/association/accessors.rb', line 324
def self.define_ids_getter!(association) ids_method = "#{association.name.to_s.singularize}_ids" association.inverse_class.tap do |klass| klass.re_define_method(ids_method) do send(association.name).pluck(:_id) end end end
.define_ids_setter!(association) (private)
Defines the setter method that allows you to set documents in this association by their ids. The defined setter, finds documents with given ids and invokes regular association setter with found documents. Ids setters should be defined only for referenced many associations.
@param [ Mongoid::Association::Relatable ] association The association for the association.
@return [ Class ] The class being set up.
# File 'lib/mongoid/association/accessors.rb', line 375
def self.define_ids_setter!(association) ids_method = "#{association.name.to_s.singularize}_ids=" association.inverse_class.aliased_associations[ids_method.chop] = association.name.to_s association.inverse_class.tap do |klass| klass.re_define_method(ids_method) do |ids| send(association.setter, association.relation_class.find(ids.reject(&:blank?))) end end end
.define_setter!(association) ⇒ Class
(private)
Defines the setter for the association. This does a few things based on some conditions. If there is an existing association, a target substitution will take place, otherwise a new association will be created with the supplied target.
# File 'lib/mongoid/association/accessors.rb', line 344
def self.define_setter!(association) name = association.name association.inverse_class.tap do |klass| klass.re_define_method("#{name}=") do |object| without_autobuild do if value = get_relation(name, association, object) if !value.respond_to?(:substitute) value = __build__(name, value, association) end set_relation(name, value.substitute(object.substitutable)) else __build__(name, object.substitutable, association) end end end end end
Instance Attribute Details
#without_autobuild? ⇒ true
| false
(readonly, private)
Is the current code executing without autobuild functionality?
# File 'lib/mongoid/association/accessors.rb', line 235
def without_autobuild? Threaded.executing?(:without_autobuild) end
Instance Method Details
#__build__(name, object, association, selected_fields = nil) ⇒ Proxy
Builds the related document and creates the association unless the document is nil, then sets the association on this document.
# File 'lib/mongoid/association/accessors.rb', line 27
def __build__(name, object, association, selected_fields = nil) relation = create_relation(object, association, selected_fields) set_relation(name, relation) end
#_mongoid_filter_selected_fields(assoc_key) ⇒ Hash | nil
(private)
Returns a subset of __selected_fields attribute applicable to the (embedded) association with the given key, or nil if no projection is to be performed.
Also returns nil if exclusionary projection was requested but it does not exclude the field of the association.
For example, if __selected_fields is => 1, ‘b.c’ => 2, ‘b.c.f’ => 3
, and assoc_key is ‘b’, return value would be => 2, ‘c.f’ => 3
.
# File 'lib/mongoid/association/accessors.rb', line 158
def _mongoid_filter_selected_fields(assoc_key) return nil unless __selected_fields # If the list of fields was specified using #without instead of #only # and the provided list does not include the association, any of its # fields should be allowed. if __selected_fields.values.all? { |v| v == 0 } && __selected_fields.keys.none? { |k| k.split('.', 2).first == assoc_key } then return nil end projecting_assoc = false filtered = {} __selected_fields.each do |k, v| bits = k.split('.') # If we are asked to project an association, we need all of that # association's fields. However, we may be asked to project # an association *and* its fields in the same query. In this case # behavior differs according to server version: # # 4.2 and lower take the most recent projection specification, meaning # projecting foo followed by foo.bar effectively projects foo.bar and # projecting foo.bar followed by foo effectively projects foo. # To match this behavior we need to track when we are being asked # to project the association and when we are asked to project a field, # and if we are asked to project the association last we need to # remove any field projections. # # 4.4 (and presumably higher) do not allow projection to be on an # association and its field, so it doesn't matter what we do. Hence # we just need to handle the 4.2 and lower case correctly. if bits.first == assoc_key # Projecting the entire association OR some of its fields if bits.length > 1 # Projecting a field bits.shift filtered[bits.join('.')] = v projecting_assoc = false else # Projecting the entire association projecting_assoc = true end end end if projecting_assoc # The last projection was of the entire association; we may have # also been projecting fields, but discard the field projections # and return nil indicating we want the entire association. return nil end # Positional projection is specified as "foo.$". In this case the # document that the $ is referring to should be retrieved with all # fields. See https://www.mongodb.com/docs/manual/reference/operator/projection/positional/ # and https://jira.mongodb.org/browse/MONGOID-4769. if filtered.keys == %w($) filtered = nil end filtered end
#create_relation(object, association, selected_fields = nil) ⇒ Proxy
Create an association from an object and association metadata.
# File 'lib/mongoid/association/accessors.rb', line 44
def create_relation(object, association, selected_fields = nil) 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 nil end # Only need to do this on embedded associations. The pending callbacks # are only added when materializing the documents, which only happens # on embedded associations. There is no call to the database in the # construction of a referenced association. if association. Array(target).each do |doc| doc.try(:run_pending_callbacks) end end target end
#get_relation(name, association, object, reload = false) ⇒ Proxy (private)
Get the association. Extracted out from the getter method to avoid infinite recursion when overriding the getter.
# File 'lib/mongoid/association/accessors.rb', line 109
def get_relation(name, association, object, reload = false) field_name = database_field_name(name) # As per the comments under MONGOID-5034, I've decided to only raise on # embedded associations for a missing attribute. Rails does not raise # for a missing attribute on referenced associations. # We also don't want to raise if we're retrieving an association within # the codebase. This is often done when retrieving the inverse association # during binding or when cascading callbacks. Whenever we retrieve # associations within the codebase, we use without_autobuild. if !without_autobuild? && association. && attribute_missing?(field_name) # We always allow accessing the parent document of an embedded one. try_get_parent = association.is_a?( Mongoid::Association::Embedded::EmbeddedIn ) && field_name == association.key raise Mongoid::Errors::AttributeNotLoaded.new(self.class, field_name) unless try_get_parent end if !reload && (value = ivar(name)) != false value else _building do _loading do if object && needs_no_database_query?(object, association) __build__(name, object, association) else selected_fields = _mongoid_filter_selected_fields(association.key) __build__(name, attributes[association.key], association, selected_fields) end end end end end
#needs_no_database_query?(object, association) ⇒ Boolean (private)
# File 'lib/mongoid/association/accessors.rb', line 224
def needs_no_database_query?(object, association) object.is_a?(Document) && !object. && object._id == attributes[association.key] end
#parse_args(*args) ⇒ Array<Hash> (private)
Parse out the attributes and the options from the args passed to a build_ or create_ methods.
# File 'lib/mongoid/association/accessors.rb', line 263
def parse_args(*args) [args.first || {}, args.size > 1 ? args[1] : {}] end
#reset_relation_criteria(name)
Resets the criteria inside the association proxy. Used by many-to-many associations to keep the underlying ids array in sync.
# File 'lib/mongoid/association/accessors.rb', line 73
def reset_relation_criteria(name) if instance_variable_defined?("@_#{name}") send(name).reset_unloaded end end
#set_relation(name, relation) ⇒ Proxy
::Set
the supplied association to an instance variable on the class with the provided name. Used as a helper just for code cleanliness.
# File 'lib/mongoid/association/accessors.rb', line 89
def set_relation(name, relation) instance_variable_set("@_#{name}", relation) end
#without_autobuild ⇒ Object
(readonly, private)
Yield to the block with autobuild functionality turned off.
# File 'lib/mongoid/association/accessors.rb', line 247
def without_autobuild Threaded.begin_execution("without_autobuild") yield ensure Threaded.exit_execution("without_autobuild") end