diff --git a/snooty.toml b/snooty.toml index 1b299d10..df700929 100644 --- a/snooty.toml +++ b/snooty.toml @@ -8,7 +8,8 @@ intersphinx = [ "https://www.mongodb.com/docs/manual/objects.inv", toc_landing_pages = [ "/quick-start-rails", "/quick-start-sinatra", - "/interact-data" + "/interact-data", + "/interact-data/specify-query" ] [constants] diff --git a/source/includes/interact-data/scoping.rb b/source/includes/interact-data/scoping.rb new file mode 100644 index 00000000..2e1889a7 --- /dev/null +++ b/source/includes/interact-data/scoping.rb @@ -0,0 +1,170 @@ +# start-named-scope-1 +class Band + include Mongoid::Document + + field :country, type: String + field :genres, type: Array + + scope :japanese, ->{ where(country: "Japan") } + scope :rock, ->{ where(:genres.in => [ "rock" ]) } +end +# end-named-scope-1 + +# start-query-named-scope +Band.japanese.rock +# end-query-named-scope + +# start-named-scope-2 +class Band + include Mongoid::Document + + field :name, type: String + field :country, type: String + + scope :based_in, ->(country){ where(country: country) } +end +# end-named-scope-2 + +# start-query-named-scope-2 +Band.based_in("Spain") +# end-query-named-scope-2 + +# start-named-scope-3 +class Band + include Mongoid::Document + + def self.on_tour + true + end + + scope :on_tour, ->{ where(on_tour: true) } +end +# end-named-scope-3 + +# start-default-scope-1 +class Band + include Mongoid::Document + + field :name, type: String + field :active, type: Boolean + + default_scope -> { where(active: true) } +end +# end-default-scope-1 + +# start-default-scope-2 +class Band + include Mongoid::Document + + field :name, type: String + field :on_tour, type: Boolean, default: true + + default_scope ->{ where(on_tour: false) } +end + +# Creates a new Band instance in which "on_tour" is "false" +Band.new +# end-default-scope-2 + +# start-unscoped +# Inline example +Band.unscoped.where(name: "Depeche Mode") + +# Block example +Band.unscoped do + Band.where(name: "Depeche Mode") +end +# end-unscoped + +# start-scope-association +class Label + include Mongoid::Document + + field :name, type: String + + embeds_many :bands +end + +class Band + include Mongoid::Document + + field :name, type: String + field :active, default: true + + embedded_in :label + default_scope ->{ where(active: true) } +end +# end-scope-association + +# start-scope-association-steps +label = Label.new(name: "Hello World Records") +band = Band.new(name: "Ghost Mountain") +label.bands.push(band) +label.bands # Displays the Band because "active" is "true" +band.update_attribute(:active, false) # Updates "active" to "false" + +# Displays the "Ghost Mountain" band +label.bands # => {"_id":"...","name":"Ghost Mountain",...} + +# Won't display "Ghost Mountain" band after reloading +label.reload.bands # => nil +# end-scope-association-steps + +# start-scope-query-behavior +class Band + include Mongoid::Document + + field :name + field :touring + field :member_count + + default_scope ->{ where(touring: true) } +end + +# Combines the condition to the default scope with "and" +Band.where(name: 'Infected Mushroom') +# Interpreted query: +# {"touring"=>true, "name"=>"Infected Mushroom"} + +# Combines the first condition to the default scope with "and" +Band.where(name: 'Infected Mushroom').or(member_count: 3) +# Interpreted query: +# {"$or"=>[{"touring"=>true, "name"=>"Infected Mushroom"}, {"member_count"=>3}]} + +# Combines the condition to the default scope with "or" +Band.or(member_count: 3) +# Interpreted query: +# {"$or"=>[{"touring"=>true}, {"member_count"=>3}]} +# end-scope-query-behavior + +# start-override-scope +class Band + include Mongoid::Document + + field :country, type: String + field :genres, type: Array + + scope :mexican, ->{ where(country: "Mexico") } +end +# end-override-scope + +# start-override-scope-block +Band.with_scope(Band.mexican) do + Band.all +end +# end-override-scope-block + +# start-class-methods +class Band + include Mongoid::Document + + field :name, type: String + field :touring, type: Boolean, default: true + + def self.touring + where(touring: true) + end +end + +Band.touring +# end-class-methods diff --git a/source/interact-data/modify-results.txt b/source/interact-data/modify-results.txt index cc87951c..efdaadf9 100644 --- a/source/interact-data/modify-results.txt +++ b/source/interact-data/modify-results.txt @@ -222,13 +222,12 @@ When you chain sort specifications, the first call defines the first sorting order and the newest call defines the last sorting order after the previous sorts have been applied. -.. TODO update link in the following note for scope - .. note:: Sorting in Scopes - If you define a scope on your model that includes a sort specification, - the scope sort takes precedence over the sort specified in a query, - because the default scope is evaluated first. + If you define a :ref:`default scope ` on your + model that includes a sort specification, the scope sort takes precedence + over the sort specified in a query, because the default scope is + evaluated first. .. _mongoid-data-skip-limit: diff --git a/source/interact-data/scoping.txt b/source/interact-data/scoping.txt new file mode 100644 index 00000000..49329818 --- /dev/null +++ b/source/interact-data/scoping.txt @@ -0,0 +1,265 @@ +.. _mongoid-data-scoping: + +======= +Scoping +======= + +.. facet:: + :name: genre + :values: reference + +.. meta:: + :keywords: ruby framework, odm, crud, filter, code example + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +Overview +-------- + +In this guide, you can learn how to implement **scopes** into your +{+odm+} models. Scopes provide a convenient way to reuse common filter +criteria. To learn more about creating filter criteria, see the +:ref:`mongoid-data-specify-query` guide. + +You might implement scopes into your application to reduce repeated code +if you are applying the same criteria to most queries. + +Named Scopes +------------ + +Named scopes are criteria defined at class load that are referenced by a +provided name. Similar to filter criteria, they are lazily loaded and +chainable. + +This example defines a ``Band`` model that includes the following named +scopes: + +- ``japanese``: Matches documents in which the value of the ``country`` + field is ``"Japan"`` + +- ``rock``: Matches documents in which the value of the ``genre`` + field includes ``"rock"`` + +.. literalinclude:: /includes/interact-data/scoping.rb + :start-after: start-named-scope-1 + :end-before: end-named-scope-1 + :language: ruby + :dedent: + :emphasize-lines: 7-8 + +Then, you can query by using the named scopes. The following query uses +the named scopes to match documents in which value of the ``country`` +field is ``"Japan"`` and value of the ``genre`` field includes +``"rock"``: + +.. literalinclude:: /includes/interact-data/scoping.rb + :start-after: start-query-named-scope + :end-before: end-query-named-scope + :language: ruby + :dedent: + +Advanced Scoping +~~~~~~~~~~~~~~~~ + +You can define ``Proc`` objects and blocks in named scopes so that they +accept parameters and extend functionality. + +This example defines a ``Band`` model that includes the ``based_in`` scope, +which matches documents in which the ``country`` field value +is the specified value passed as a parameter: + +.. literalinclude:: /includes/interact-data/scoping.rb + :start-after: start-named-scope-2 + :end-before: end-named-scope-2 + :language: ruby + :emphasize-lines: 7 + :dedent: + +Then, you can query by using the ``based_in`` scope, as shown in the following +code: + +.. literalinclude:: /includes/interact-data/scoping.rb + :start-after: start-query-named-scope-2 + :end-before: end-query-named-scope-2 + :language: ruby + :dedent: + +{+odm+} allows you to define a scope that shadows an existing class +method, as shown in the following example: + +.. literalinclude:: /includes/interact-data/scoping.rb + :start-after: start-named-scope-3 + :end-before: end-named-scope-3 + :language: ruby + :dedent: + +You can direct {+odm+} to raise an error when a scope overwrites an +existing class method by setting the ``scope_overwrite_exception`` +configuration option to ``true``. + +.. TODO add link to config options page + +Default Scopes +-------------- + +Default scopes are useful for cases where you apply the same +criteria to most queries. By defining a default scope, you specify these +criteria as the default for any queries that use the model. Default +scopes return ``Criteria`` objects. + +To create a default scope, you must define the ``default_scope()`` method +on your model class. + +The following code defines the ``default_scope()`` method on the ``Band`` +model to only retrieve documents in which the ``active`` field value is ``true``: + +.. literalinclude:: /includes/interact-data/scoping.rb + :start-after: start-default-scope-1 + :end-before: end-default-scope-1 + :language: ruby + :dedent: + :emphasize-lines: 7 + +Then, any queries on the ``Band`` model pre-filter for documents in which the +``active`` value is ``true``. + +Field Initialization +~~~~~~~~~~~~~~~~~~~~ + +Specifying a default scope initializes the fields of new models to +the values given in the default scope if those values are literals, such +as boolean values or integers. + +.. note:: Field and Scope Conflicts + + If you provide a default value in a field definition and in the + default scope, the value in the default scope takes precedence, as + shown in the following example: + + .. literalinclude:: /includes/interact-data/scoping.rb + :start-after: start-default-scope-2 + :end-before: end-default-scope-2 + :language: ruby + :dedent: + :emphasize-lines: 5, 7 + +We do not recommend using dot notation to reference nested fields in default +scopes. This can direct {+odm+} to initialize unexpected fields in new +models. + +For example, if you define a default scope that references the +``tour.year`` field, a new model is initialized with the field +``tour.year`` instead of a ``tour`` field with a nested object that +contains a ``year`` field. + +When *querying*, {+odm+} interprets the dot notation correctly and matches +documents in which a nested field has the specified value. + +Associations +~~~~~~~~~~~~ + +If you define a default scope on a model that is part of an +association, you must reload the association to have scoping reapplied. +This is necessary for when you change a value of a document in the +association that affects its visibility when the scope is applied. + +This example uses the following models: + +.. literalinclude:: /includes/interact-data/scoping.rb + :start-after: start-scope-association + :end-before: end-scope-association + :language: ruby + :dedent: + +Suppose you create a ``Label`` model that contains an association to a +``Band`` in which the value of ``active`` is ``true``. When you update +the ``active`` field to ``false``, {+odm+} still loads it despite the +default scope. To view the documents in the association with the scope +applied, you must call the ``reload()`` operator. + +The following code demonstrates this sequence: + +.. literalinclude:: /includes/interact-data/scoping.rb + :start-after: start-scope-association-steps + :end-before: end-scope-association-steps + :language: ruby + :dedent: + :emphasize-lines: 5, 7 + +or and nor Query Behavior +~~~~~~~~~~~~~~~~~~~~~~~~~ + +{+odm+} treats the criteria in a default scope the same way as any other +query conditions. This can lead to surprising behavior when using the +``or()`` and ``nor()`` methods. + +The following examples demonstrate how {+odm+} interprets queries on +models with a default scope: + +.. literalinclude:: /includes/interact-data/scoping.rb + :start-after: start-scope-query-behavior + :end-before: end-scope-query-behavior + :language: ruby + :dedent: + +To learn more about logical operations, see +:ref:`mongoid-query-logical-operations` in the Specify a Query guide. + +Disable Scope When Querying +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can direct {+odm+} to not apply the default scope by using the +``unscoped`` operator, as shown in the following examples: + +.. literalinclude:: /includes/interact-data/scoping.rb + :start-after: start-unscoped + :end-before: end-unscoped + :language: ruby + :dedent: + +Override Default Scope at Runtime +--------------------------------- + +You can use the ``with_scope()`` method to change the default scope in a +block at runtime. + +The following model defines the *named* scope ``mexican``: + +.. literalinclude:: /includes/interact-data/scoping.rb + :start-after: start-override-scope + :end-before: end-override-scope + :language: ruby + :dedent: + :emphasize-lines: 7 + +You can use the ``with_scope()`` method to set the ``mexican`` named +scope as the default scope at runtime, as shown in the following code: + +.. literalinclude:: /includes/interact-data/scoping.rb + :start-after: start-override-scope-block + :end-before: end-override-scope-block + :language: ruby + :dedent: + +Class Methods +------------- + +{+odm+} treats class methods that return ``Criteria`` objects +as scopes. You can query by using these class methods, as shown in +the following example: + +.. literalinclude:: /includes/interact-data/scoping.rb + :start-after: start-class-methods + :end-before: end-class-methods + :language: ruby + :dedent: + :emphasize-lines: 7-9, 12 + +Additional Information +---------------------- + +.. TODO add info links \ No newline at end of file diff --git a/source/interact-data/specify-query.txt b/source/interact-data/specify-query.txt index 63ed1b74..986b275a 100644 --- a/source/interact-data/specify-query.txt +++ b/source/interact-data/specify-query.txt @@ -11,6 +11,11 @@ Specify a Query .. meta:: :keywords: ruby framework, odm, crud, filter, code example +.. toctree:: + :caption: Queries + + /interact-data/scoping + .. contents:: On this page :local: :backlinks: none