diff --git a/source/data-modeling/associations.txt b/source/data-modeling/associations.txt index f441f598..88d65315 100644 --- a/source/data-modeling/associations.txt +++ b/source/data-modeling/associations.txt @@ -25,7 +25,7 @@ this guide, you can learn about the different types of associations that {+odm+} supports and how to use them in your application. Referenced Associations ------------------------- +----------------------- Referenced associations allow you to create a relationship between two models where one model references the other. {+odm+} supports the following referenced @@ -515,3 +515,415 @@ method to remove all embedded ``Album`` documents from the ``Band`` class: :language: ruby :start-after: # start-embedded-destroy-all :end-before: # end-embedded-destroy-all + +Customize Association Behavior +------------------------------ + +You can use {+odm+} to customize how associations behave in your application. +The following sections describe ways to customize association behaviors. + +Extensions +~~~~~~~~~~ + +Extensions allow you to add custom functionality to an association. You can +define an extension on an association by specifying a block in the association +definition, as shown in the following example: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-extensions + :end-before: # end-extensions + :emphasize-lines: 4-6 + +Custom Association Names +~~~~~~~~~~~~~~~~~~~~~~~~ + +You can use the ``class_name`` macro to specify a custom class name for an +association. This is useful when you want to name the association something other +than the name of the class. The following example uses the +``class_name`` macro to specify that an embedded association called ``records`` +represents the ``Album`` class: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-custom-name + :end-before: # end-custom-name + +Custom Keys +~~~~~~~~~~~ + +By default, {+odm+} uses the ``_id`` field of the parent class when looking up +associations. You can specify different fields to use by using the +``primary_key`` and ``foreign_key`` macros. The following example specifies a new +primary and foreign key for the ``albums`` association on a ``Band`` class: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-custom-keys + :end-before: # end-custom-keys + +If you are specifying a ``has_and_belongs_to_many`` association, you can also +use the ``inverse_primary_key`` and ``inverse_foreign_key`` macros. The +``inverse_primary_key`` macro specifies the field on the local model that the +remote model uses to look up the documents. +The ``inverse_foreign_key`` macro specifies the field on the remote model +that stores the values found in ``inverse_primary_key``. + +The following example specifies a new primary and foreign key for the +``Band`` and ``Members`` classes in a ``has_and_belongs_to_many`` association: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-custom-inverse-keys + :end-before: # end-custom-inverse-keys + :emphasize-lines: 9, 20 + +Custom Scopes +~~~~~~~~~~~~~ + +You can specify the scope of an association by using the ``scope`` parameter. +The ``scope`` parameter determines the documents that {+odm+} considers part +of an association. A scoped association returns only documents that match the +scope conditions when queried. You can set the ``scope`` to either a ``Proc`` with an arity +of zero or a ``Symbol`` that references a named scope on the associated model. +The following example sets custom scopes on associations in a ``Band`` class: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-custom-scope + :end-before: # end-custom-scope + +.. note:: + + You can add documents that do not match the scope conditions to an + association. {+odm+} saves the documents to the database and they will appear + in associated memory. However, you won't see the documents when querying the + association. + +Validations +~~~~~~~~~~~ + +When {+odm+} loads an association into memory, by default, it uses the +``validates_associated`` macro to validate that any children are also present. +{+odm+} validates children for the following association types: + +- ``embeds_many`` +- ``embeds_one`` +- ``has_many`` +- ``has_one`` +- ``has_and_belongs_to_many`` + +You can turn off this validation behavior by setting the ``validate`` macro to +``false`` when defining the association, as shown in the following example: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-validation-false + :end-before: # end-validation-false + +Polymorphism +~~~~~~~~~~~~ + +{+odm+} supports polymorphism on the child classes of one-to-one and one-to-many associations. +Polymorphic associations allows a single association to contain objects of different class +types. You can define a polymorphic association by setting the ``polymorphic`` +option to ``true`` in a child association and adding the ``as`` option to the +parent association. The following example creates a polymorphic association in a +``Band`` class: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-polymorphic + :end-before: # end-polymorphic + +In the preceding example, the ``:featured`` association in the ``Band`` class can contain either a +``Label`` or ``Album`` document. + +.. important:: + + {+odm+} supports polymorphism only from child to parent. You cannot specify + a parent ``has_one`` or ``has_many`` association as polymorphic. + +``has_and_belongs_to_many`` associations do not support polymorphism. + +Custom Polymorphic Types +```````````````````````` + +Starting in version 9.0.2, {+odm+} supports custom polymorphic types through +a global registry. You can specify alternative keys to represent different +classes, decoupling your code from the data. The following example specifies +the string ``"artist"`` as an alternate key for the ``Band`` class: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-custom-polymorphic + :end-before: # end-custom-polymorphic + +In the preceding example, the ``identify_as`` directive instructs {+odm+} +to store the ``Band`` class in the database as the string ``"artist"``. + +You can also specify multiple aliases, as shown in the following example: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-multiple-alias + :end-before: # end-multiple-alias + +In the preceding example, ``artist`` is the default name and the others are used +only for looking up records. This allows you to refactor your +code without breaking the associations in your data. + +Polymorphic type aliases are global. The keys you specify must be unique across your +entire code base. However, you can register alternative resolvers that +can be used for different subsets of your models. In this case, the keys must +be unique only for each resolver. The following example shows how to register +alternate resolvers: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-polymorphic-resolvers + :end-before: # end-polymorphic-resolvers + +Both ``Music::Band`` and ``Tools::Band`` are aliased as +``"bnd"``, but each model uses its own resolver to avoid conflicts. + +Dependent Behavior +~~~~~~~~~~~~~~~~~~ + +You can provide ``dependent`` options to referenced associations to specify how +{+odm+} handles associated documents when a document is deleted. You can specify +the following options: + +- ``delete_all``: Deletes all child documents without running any model + callbacks. +- ``destroy``: Deletes the child documents and runs all model callbacks. +- ``nullify``: Sets the foreign key of the child documents to ``nil``. The child + document might become orphaned if it is referenced by only the parent. +- ``restrict_with_exception``: Raises an exception if the child document is not + empty. +- ``restrict_with_error``: Cancels the operation and returns ``false`` if the + child document is not empty. + +If you don't specify any ``dependent`` options, {+odm+} leaves the child +document unchanged when the parent document is deleted. The child document +continues to reference the deleted parent document, and if it is +referenced through only the parent, the child document becomes orphaned. + +The following example specifies ``dependent`` options on the ``Band`` class: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-dependent + :end-before: # end-dependent + +Autosave Referenced Associations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, {+odm+} does not automatically save associated documents from +non-embedded associations when saving the parent document. This can +result in dangling references to documents that don't exist. + +You can use the ``autosave`` option on a referenced association to +automatically save associated documents when saving the parent document. The +following example creates a ``Band`` class with an associated ``Album`` class +and specifies the ``autosave`` option: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-autosave + :end-before: # end-autosave + :emphasize-lines: 10 + +.. note:: + + {+odm+} automatically adds autosave functionality to an association that uses + the ``accepts_nested_attributes_for`` option. + +You do not need to specify the ``autosave`` option for embedded associations +because {+odm+} saves embedded documents in the parent document. + +Autobuild +~~~~~~~~~ + +You can add the ``autobuild`` option to one-to-one associations, such as +``has_one`` and ``embeds_one``, to automatically instantiate a new document when +accessing a ``nil`` association. The following example adds the ``autobuild`` +option to an association on the ``Band`` class: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-autobuild + :end-before: # end-autobuild + +Touch +~~~~~ + +When {+odm+} *touches* a document, it updates the document's +``updated_at`` field to the current date and time. You can add the ``touch`` +option to any ``belongs_to`` association to ensure that {+odm+} touches the +parent document whenever the child document is updated. The following example +adds the ``touch`` option to an association on the ``Band`` class: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-touch + :end-before: # end-touch + +You can also use the ``touch`` option to specify another field on the parent +association, as a string or a symbol. When {+odm+} touches the parent +association, it sets both the ``updated_at`` field and the specified field +to the current date and time. + +The following example instructs {+odm+} to touch the ``bands_updated_at`` field: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-touch-specific + :end-before: # end-touch-specific + +.. note:: + + In embedded associations, when an embedded document is touched, {+odm+} + touches its parents recursively. Because of this, adding a ``touch`` + attribute to an ``embedded_in`` association is unnecessary. + + {+odm+} does not support specifying additional fields to touch in + ``embedded_in`` associations. + +Counter Cache +~~~~~~~~~~~~~ + +You can use the ``counter_cache`` option to store the number of objects +that belong to an associated field. When you specify this option, {+odm+} stores +an extra attribute on the associated models to store the count. Because of this, +you must specify the ``Mongoid::Attributes::Dynamic`` module in the associated +classes. + +The following example adds the ``counter_cache`` option to a ``Band`` +class and specifies the ``Mongoid::Attributes::Dynamic`` in a ``Label`` class: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-counter-cache + :end-before: # end-counter-cache + :emphasize-lines: 4, 9 + +Association Metadata +-------------------- + +When you define an association, {+odm+} stores metadata about that association. +You can access the metadata by calling the ``reflect_on_association`` method on +a model class or document, or by directly accessing the metadata on a specific +document. The following example shows how to access metadata by using the +``reflect_on_association`` method and by direct access: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-access-metadata + :end-before: # end-access-metadata + +.. note:: + + Replace ```` in the preceding example with the name of your + association. + +Attributes +~~~~~~~~~~ + +All associations contain attributes that store information about the associated +document. Associations contain the following attributes: + +- ``_target``: The proxied document or documents +- ``_base``: The document on which the association is defined +- ``_association``: Information about the association + +The following example accesses each of the preceding attributes: + +.. literalinclude:: /includes/data-modeling/association-behaviors.rb + :language: ruby + :start-after: # start-attributes + :end-before: # end-attributes + +The following table shows the information stored in the ``_association`` +attribute: + +.. list-table:: + :header-rows: 1 + :widths: 30 60 + + * - Method + - Description + * - ``Association#as`` + - The name of the parent to a polymorphic child. + * - ``Association#as?`` + - Returns whether an ``as`` option exists. + * - ``Association#autobuilding?`` + - Returns whether the association is autobuilding. + * - ``Association#autosaving?`` + - Returns whether the association is autosaving. + * - ``Association#cascading_callbacks?`` + - Returns whether the association has callbacks cascaded down from the parent. + * - ``Association#class_name`` + - The class name of the proxied document. + * - ``Association#cyclic?`` + - Returns whether the association is a cyclic association. + * - ``Association#dependent`` + - The association's dependent option. + * - ``Association#destructive?`` + - Returns ``true`` if the association has a dependent delete or destroy method. + * - ``Association#embedded?`` + - Returns whether the association is embedded in another document. + * - ``Association#forced_nil_inverse?`` + - Returns whether the association has a ``nil`` inverse defined. + * - ``Association#foreign_key`` + - The name of the foreign-key field. + * - ``Association#foreign_key_check`` + - The name of the foreign-key field's dirty-check method. + * - ``Association#foreign_key_setter`` + - The name of the foreign-key field's setter. + * - ``Association#indexed?`` + - Returns whether the foreign key is auto indexed. + * - ``Association#inverses`` + - The names of all inverse associations. + * - ``Association#inverse`` + - The name of a single inverse association. + * - ``Association#inverse_class_name`` + - The class name of the association on the inverse side. + * - ``Association#inverse_foreign_key`` + - The name of the foreign-key field on the inverse side. + * - ``Association#inverse_klass`` + - The class of the association on the inverse side. + * - ``Association#inverse_association`` + - The metadata of the association on the inverse side. + * - ``Association#inverse_of`` + - The explicitly defined name of the inverse association. + * - ``Association#inverse_setter`` + - The name of the method used to set the inverse. + * - ``Association#inverse_type`` + - The name of the polymorphic-type field of the inverse. + * - ``Association#inverse_type_setter`` + - The name of the polymorphic-type field's setter of the inverse. + * - ``Association#key`` + - The name of the field in the attribute's hash that is used to get the association. + * - ``Association#klass`` + - The class of the proxied documents in the association. + * - ``Association#name`` + - The association name. + * - ``Association#options`` + - Returns ``self``, for API compatibility with ActiveRecord. + * - ``Association#order`` + - The custom sorting options on the association. + * - ``Association#polymorphic?`` + - Returns whether the association is polymorphic. + * - ``Association#setter`` + - The name of the field to set the association. + * - ``Association#store_as`` + - The name of the attribute in which to store an embedded association. + * - ``Association#touchable?`` + - Returns whether the association has a touch option. + * - ``Association#type`` + - The name of the field to get the polymorphic type. + * - ``Association#type_setter`` + - The name of the field to set the polymorphic type. + * - ``Association#validate?`` + - Returns whether the association has an associated validation. diff --git a/source/includes/data-modeling/association-behaviors.rb b/source/includes/data-modeling/association-behaviors.rb new file mode 100644 index 00000000..a74d0739 --- /dev/null +++ b/source/includes/data-modeling/association-behaviors.rb @@ -0,0 +1,228 @@ +# start-extensions +class Band + include Mongoid::Document + + embeds_many :albums do + def find_by_name(name) + where(name: name).first + end + end +end + +band.albums.find_by_name("Omega") # returns album "Omega" +# end-extensions + +# start-custom-name +class Band + include Mongoid::Document + + embeds_many :records, class_name: "Album" +end +# end-custom-name + +# start-custom-keys +class Band + include Mongoid::Document + + field :band_id, type: String + has_many :albums, primary_key: 'band_id', foreign_key: 'band_id_ref' +end + +class Album + include Mongoid::Document + + field :band_id_ref, type: String + belongs_to :band, primary_key: 'band_id', foreign_key: 'band_id_ref' +end +# end-custom-keys + +# start-custom-inverse-keys +class Band + include Mongoid::Document + + field :band_id, type: String + field :member_ids, type: Array + + has_many :members, + primary_key: 'member_id', foreign_key: 'member_ids', + inverse_primary_key: 'band_id', inverse_foreign_key: 'band_ids' +end + +class Member + include Mongoid::Document + + field :member_id, type: String + field :band_ids, type: Array + + has_many :bands, + primary_key: 'band_id', foreign_key: 'band_ids', + inverse_primary_key: 'member_id', inverse_foreign_key: 'member_ids' +end +# end-custom-inverse-keys + +# start-custom-scope +class Band + include Mongoid::Document + + has_many :albums, scope: -> { where(published: true) } + + # Uses a scope called "upcoming" on the Tour model + has_many :tours, scope: :upcoming +end +# end-custom-scope + +# start-validation-false +class Band + include Mongoid::Document + + embeds_many :albums, validate: false +end +# end-validation-false + +# start-polymorphic +class Tour + include Mongoid::Document + + has_one :band, as: :featured +end + +class Label + include Mongoid::Document + + has_one :band, as: :featured +end + +class Band + include Mongoid::Document + + belongs_to :featured, polymorphic: true +end +# end-polymorphic + +# start-custom-polymorphic +class Band + include Mongoid::Document + + identify_as 'artist' + has_many :albums, as: :record +end +# end-custom-polymorphic + +# start-multiple-alias +class Band + include Mongoid::Document + + identify_as 'artist', 'group', 'troupe' + has_many :albums, as: :record +end +# end-multiple-alias + +# start-polymorphic-resolvers +Mongoid::ModelResolver.register_resolver Mongoid::ModelResolver.new, :mus +Mongoid::ModelResolver.register_resolver Mongoid::ModelResolver.new, :tool + +module Music + class Band + include Mongoid::Document + + identify_as 'bnd', resolver: :mus + end +end + +module Tools + class Band + include Mongoid::Document + + identify_as 'bnd', resolver: :tool + end +end +# end-polymorphic-resolvers + +# start-dependent +class Band + include Mongoid::Document + + has_many :albums, dependent: :delete_all + belongs_to :label, dependent: :nullify +end +# end-dependent + +# start-autosave +class Band + include Mongoid::Document + + has_many :albums +end + +class Album + include Mongoid::Document + + belongs_to :band, autosave: true +end + +band = Band.new +album = Album.create!(band: band) +# The band is persisted at this point. +# end-autosave + +# start-autobuild +class Band + include Mongoid::Document + + embeds_one :label, autobuild: true + has_one :producer, autobuild: true +end +# end-autobuild + +# start-touch +class Band + include Mongoid::Document + + field :name + belongs_to :label, touch: true +end +# end-touch + +# start-touch-specific +class Band + include Mongoid::Document + + belongs_to :label, touch: :bands_updated_at +end +# end-touch-specific + +# start-counter-cache +class Band + include Mongoid::Document + + belongs_to :label, counter_cache: true +end + +class Label + include Mongoid::Document + include Mongoid::Attributes::Dynamic + + has_many :bands +end +# end-counter-cache + +# start-access-metadata +# Get the metadata for a named association from the class or document +Model.reflect_on_association(:) + +# Directly access metadata on a document +model.associations[:] +# end-access-metadata + +# start-attributes +class Band + include Mongoid::Document + + embeds_many :songs +end + +Band.songs = [ song ] +Band.songs._target # returns [ song ] +Band.songs._base # returns band +Band.songs._association # returns the association metadata +# end-attributes \ No newline at end of file