Skip to content

Commit c4e173d

Browse files
johnnyshieldsp
authored andcommitted
MONGOID-5128 Support association :scope option
1 parent eaa6d26 commit c4e173d

28 files changed

+718
-20
lines changed

docs/reference/associations.txt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,44 @@ the model. However, since Mongoid doesn't know what type of data should be
969969
allowed in the field, the field is created with a type of Object. It is a
970970
good idea to explicitly define the field with the appropriate type.
971971

972+
Custom Scopes
973+
-------------
974+
975+
You may set a specific scope on an association using the ``:scope`` parameter.
976+
The scope may be either:
977+
- a ``Proc`` with arity zero
978+
- a ``Symbol`` which references a :ref:`named scope <named-scopes>` on the associated model
979+
980+
.. code-block:: ruby
981+
class Trainer
982+
has_many :pets, scope: -> { where(species: 'dog' }
983+
has_many :toys, scope: :rubber
984+
end
985+
986+
class Pet
987+
belongs_to :trainer
988+
end
989+
990+
class Toy
991+
scope :rubber, where(material: 'rubber')
992+
belongs_to :trainer
993+
end
994+
995+
A scoped association will return only documents which satisfy the scope condition.
996+
997+
As a caveat, it is still possible to make associations between documents which violate
998+
the scope condition. In this case, such documents will appear associated in-memory,
999+
but will not be present when the association is re-queried. Refer to the following example:
1000+
1001+
.. code-block:: ruby
1002+
trainer = Trainer.create!
1003+
dog = Pet.create!(trainer: trainer, species: 'dog')
1004+
cat = Pet.create!(trainer: trainer, species: 'cat')
1005+
1006+
trainer.pets #=> [dog, cat]
1007+
1008+
trainer.reload.pets #=> [dog]
1009+
9721010
Validations
9731011
-----------
9741012

lib/mongoid/association/referenced/belongs_to.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ class BelongsTo
2929
:primary_key,
3030
:touch,
3131
:optional,
32-
:required
32+
:required,
33+
:scope
3334
].freeze
3435

3536
# The complete list of valid options for this association, including
@@ -130,6 +131,15 @@ def path(document)
130131
Mongoid::Atomic::Paths::Root.new(document)
131132
end
132133

134+
# Get the scope to be applied when querying the association.
135+
#
136+
# @return [ Proc, Symbol ] The association scope.
137+
#
138+
# @since 7.4
139+
def scope
140+
@options[:scope]
141+
end
142+
133143
private
134144

135145
def setup_instance_methods!

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ def execute_query(object, type)
3232
end
3333

3434
def query_criteria(object, type)
35-
model = type ? type.constantize : relation_class
36-
model.where(primary_key => object)
35+
cls = type ? type.constantize : relation_class
36+
crit = cls.criteria
37+
crit = crit.apply_scope(scope)
38+
crit.where(primary_key => object)
3739
end
3840

3941
def query?(object)

lib/mongoid/association/referenced/eager.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ def each_loaded_document(&block)
7070
# Upstream code is responsible for eliminating nils from keys.
7171
return cls.none if keys.empty?
7272

73-
criteria = cls.any_in(key => keys)
73+
criteria = cls.criteria
74+
criteria = criteria.apply_scope(@association.scope)
75+
criteria = criteria.any_in(key => keys)
7476
criteria.inclusions = criteria.inclusions - [@association]
7577
criteria.each do |doc|
7678
yield doc

lib/mongoid/association/referenced/has_and_belongs_to_many.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class HasAndBelongsToMany
3232
:primary_key,
3333
:inverse_primary_key,
3434
:inverse_foreign_key,
35+
:scope
3536
].freeze
3637

3738
# The complete list of valid options for this association, including
@@ -163,6 +164,15 @@ def path(document)
163164
Mongoid::Atomic::Paths::Root.new(document)
164165
end
165166

167+
# Get the scope to be applied when querying the association.
168+
#
169+
# @return [ Proc, Symbol ] The association scope.
170+
#
171+
# @since 7.4
172+
def scope
173+
@options[:scope]
174+
end
175+
166176
private
167177

168178
def setup_instance_methods!
@@ -251,7 +261,9 @@ def with_ordering(criteria)
251261
end
252262

253263
def query_criteria(id_list)
254-
crit = relation_class.all_of(primary_key => {"$in" => id_list || []})
264+
crit = relation_class.criteria
265+
crit = crit.apply_scope(scope)
266+
crit = crit.all_of(primary_key => {"$in" => id_list || []})
255267
with_ordering(crit)
256268
end
257269
end

lib/mongoid/association/referenced/has_many.rb

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ class HasMany
2929
:dependent,
3030
:foreign_key,
3131
:order,
32-
:primary_key
32+
:primary_key,
33+
:scope
3334
].freeze
3435

3536
# The complete list of valid options for this association, including
@@ -176,6 +177,15 @@ def path(document)
176177
Mongoid::Atomic::Paths::Root.new(document)
177178
end
178179

180+
# Get the scope to be applied when querying the association.
181+
#
182+
# @return [ Proc, Symbol ] The association scope.
183+
#
184+
# @since 7.4
185+
def scope
186+
@options[:scope]
187+
end
188+
179189
private
180190

181191
def default_foreign_key_field
@@ -204,7 +214,9 @@ def default_primary_key
204214
end
205215

206216
def query_criteria(object, base)
207-
crit = klass.where(foreign_key => object)
217+
crit = klass.criteria
218+
crit = crit.apply_scope(scope)
219+
crit = crit.where(foreign_key => object)
208220
crit = with_polymorphic_criterion(crit, base)
209221
crit.association = self
210222
crit.parent_document = base

lib/mongoid/association/referenced/has_one.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ class HasOne
2424
:autosave,
2525
:dependent,
2626
:foreign_key,
27-
:primary_key
27+
:primary_key,
28+
:scope
2829
].freeze
2930

3031
# The complete list of valid options for this association, including
@@ -129,6 +130,15 @@ def path(document)
129130
Mongoid::Atomic::Paths::Root.new(document)
130131
end
131132

133+
# Get the scope to be applied when querying the association.
134+
#
135+
# @return [ Proc, Symbol ] The association scope.
136+
#
137+
# @since 7.4
138+
def scope
139+
@options[:scope]
140+
end
141+
132142
private
133143

134144
# Setup the instance methods on the class having this association type.

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ def clear_associated(object)
4646
end
4747

4848
def query_criteria(object, base)
49-
crit = klass.where(foreign_key => object)
49+
crit = klass.criteria
50+
crit = crit.apply_scope(scope)
51+
crit = crit.where(foreign_key => object)
5052
with_polymorphic_criterion(crit, base)
5153
end
5254

lib/mongoid/criteria/scopable.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@ def apply_default_scope
1717
self.scoping_options = true, false
1818
end
1919

20+
# Applies a scope to the criteria
21+
#
22+
# @param [ Proc, Symbol ] scope The scope to apply.
23+
#
24+
# @return [ Criteria ] The criteria with scoping removed.
25+
#
26+
# @since 7.4.0
27+
def apply_scope(scope)
28+
case scope
29+
when Proc then instance_exec(&scope)
30+
when Symbol then send(scope)
31+
when Criteria then merge(scope)
32+
else self
33+
end
34+
end
35+
2036
# Given another criteria, remove the other criteria's scoping from this
2137
# criteria.
2238
#
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# frozen_string_literal: true
2+
# encoding: utf-8
3+
4+
require 'spec_helper'
5+
require_relative '../../mongoid/association/referenced/has_and_belongs_to_many_models'
6+
require_relative '../../mongoid/association/referenced/has_many_models'
7+
require_relative '../../mongoid/association/referenced/has_one_models'
8+
9+
describe 'association :scope option' do
10+
11+
context 'has_many and belongs_to' do
12+
let!(:trainer1) { HmmTrainer.create!(name: 'Dave') }
13+
let!(:trainer2) { HmmTrainer.create!(name: 'Ash') }
14+
let!(:animal1) { HmmAnimal.create!(taxonomy: 'reptile', trainer: trainer1) }
15+
let!(:animal2) { HmmAnimal.create!(taxonomy: 'bird', trainer: trainer1) }
16+
let!(:animal3) { HmmAnimal.create!(taxonomy: 'mammal', trainer: trainer2) }
17+
18+
it 'initially associates the documents in-memory' do
19+
expect(trainer1.animals).to eq [animal1, animal2]
20+
expect(trainer2.animals).to eq [animal3]
21+
expect(animal1.trainer).to eq trainer1
22+
expect(animal2.trainer).to eq trainer1
23+
expect(animal3.trainer).to eq trainer2
24+
end
25+
26+
it 'loads correct documents when queried' do
27+
expect(trainer1.reload.animals).to eq [animal1]
28+
expect(trainer2.reload.animals).to eq []
29+
expect(animal1.reload.trainer).to eq trainer1
30+
expect(animal2.reload.trainer).to eq trainer1
31+
expect(animal3.reload.trainer).to be_nil
32+
end
33+
34+
it 'eager loads correct documents' do
35+
expect(HmmTrainer.includes(:animals).find(trainer1._id).animals).to eq [animal1]
36+
expect(HmmTrainer.includes(:animals).find(trainer2._id).animals).to eq []
37+
expect(HmmAnimal.includes(:trainer).find(animal1._id).trainer).to eq trainer1
38+
expect(HmmAnimal.includes(:trainer).find(animal2._id).trainer).to eq trainer1
39+
expect(HmmAnimal.includes(:trainer).find(animal3._id).trainer).to be_nil
40+
end
41+
end
42+
43+
context 'has_one and belongs_to' do
44+
let!(:trainer1) { HomTrainer.create!(name: 'Dave') }
45+
let!(:trainer2) { HomTrainer.create!(name: 'Ash') }
46+
let!(:animal1) { HomAnimal.create!(taxonomy: 'reptile', trainer: trainer1) }
47+
let!(:animal2) { HomAnimal.create!(taxonomy: 'bird', trainer: trainer1) }
48+
let!(:animal3) { HomAnimal.create!(taxonomy: 'mammal', trainer: trainer2) }
49+
50+
it 'initially associates the documents in-memory' do
51+
expect(trainer1.animal).to eq animal2
52+
expect(trainer2.animal).to eq animal3
53+
expect(animal1.trainer).to eq trainer1
54+
expect(animal2.trainer).to eq trainer1
55+
expect(animal3.trainer).to eq trainer2
56+
end
57+
58+
it 'loads correct documents when queried' do
59+
expect(trainer1.reload.animal).to eq animal1
60+
expect(trainer2.reload.animal).to be_nil
61+
expect(animal1.reload.trainer).to eq trainer1
62+
expect(animal2.reload.trainer).to eq trainer1
63+
expect(animal3.reload.trainer).to be_nil
64+
end
65+
66+
it 'eager loads correct documents' do
67+
expect(HomTrainer.includes(:animal).find(trainer1._id).animal).to eq animal1
68+
expect(HomTrainer.includes(:animal).find(trainer2._id).animal).to be_nil
69+
expect(HomAnimal.includes(:trainer).find(animal1._id).trainer).to eq trainer1
70+
expect(HomAnimal.includes(:trainer).find(animal2._id).trainer).to eq trainer1
71+
expect(HomAnimal.includes(:trainer).find(animal3._id).trainer).to be_nil
72+
end
73+
end
74+
75+
context 'has_and_belongs_to_many' do
76+
let!(:trainer1) { HabtmmTrainer.create!(name: 'Dave') }
77+
let!(:trainer2) { HabtmmTrainer.create!(name: 'Ash') }
78+
let!(:animal1) { HabtmmAnimal.create!(taxonomy: 'reptile', trainers: [trainer1, trainer2]) }
79+
let!(:animal2) { HabtmmAnimal.create!(taxonomy: 'bird', trainers: [trainer1, trainer2]) }
80+
81+
it 'initially associates the documents in-memory' do
82+
expect(trainer1.animals).to eq [animal1]
83+
expect(trainer2.animals).to eq [animal1]
84+
expect(animal1.trainers).to eq [trainer1, trainer2]
85+
expect(animal2.trainers).to eq [trainer1, trainer2]
86+
end
87+
88+
it 'loads correct documents when queried' do
89+
expect(trainer1.reload.animals).to eq [animal1]
90+
expect(trainer2.reload.animals).to eq [animal1]
91+
expect(animal1.reload.trainers).to eq [trainer1]
92+
expect(animal2.reload.trainers).to eq [trainer1]
93+
end
94+
95+
it 'eager loads correct documents' do
96+
expect(HabtmmTrainer.includes(:animals).find(trainer1._id).animals).to eq [animal1]
97+
expect(HabtmmTrainer.includes(:animals).find(trainer2._id).animals).to eq [animal1]
98+
expect(HabtmmAnimal.includes(:trainers).find(animal1._id).trainers).to eq [trainer1]
99+
expect(HabtmmAnimal.includes(:trainers).find(animal2._id).trainers).to eq [trainer1]
100+
end
101+
end
102+
end

0 commit comments

Comments
 (0)