Skip to content

Commit c1fd4c9

Browse files
neilshwekyp-mongo
andauthored
MONGOID-5226 Allow setting "store_in collection" on document subclass (#5449)
* MONGOID-5226 Allow setting "store_in collection" on document subclass * remove comment * MONGOID-5226 add docs and testing * Apply suggestions from code review Co-authored-by: Oleg Pudeyev <[email protected]> * MONGOID-5226 answer comments Co-authored-by: Oleg Pudeyev <[email protected]>
1 parent 58e2a3d commit c1fd4c9

File tree

8 files changed

+205
-37
lines changed

8 files changed

+205
-37
lines changed

docs/reference/inheritance.txt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,56 @@ either normal setting or through the build and create methods on the association
253253

254254
rect = Rectangle.new(width: 100, height: 200)
255255
firefox.shapes
256+
257+
258+
.. _inheritance-persistence-context:
259+
260+
Persistence Contexts
261+
====================
262+
263+
Mongoid allows the persistence context of a subclass to be changed from the
264+
persistence context of its parent. This means that, using the ``store_in``
265+
method, we can store the documents of the subclasses in different collections
266+
(as well as different databases, clients) than their parents:
267+
268+
.. code:: ruby
269+
270+
class Shape
271+
include Mongoid::Document
272+
store_in collection: :shapes
273+
end
274+
275+
class Circle < Shape
276+
store_in collection: :circles
277+
end
278+
279+
class Square < Shape
280+
store_in collection: :squares
281+
end
282+
283+
Shape.create!
284+
Circle.create!
285+
Square.create!
286+
287+
Setting the collection on the children causes the documents for those children
288+
to be stored in the set collection, instead of in the parent's collection:
289+
290+
.. code:: javascript
291+
292+
> db.shapes.find()
293+
{ "_id" : ObjectId("62fe9a493282a43d6b725e10"), "_type" : "Shape" }
294+
> db.circles.find()
295+
{ "_id" : ObjectId("62fe9a493282a43d6b725e11"), "_type" : "Circle" }
296+
> db.squares.find()
297+
{ "_id" : ObjectId("62fe9a493282a43d6b725e12"), "_type" : "Square" }
298+
299+
If the collection is set on some of the subclasses and not others, the subclasses
300+
with set collections will store documents in those collections, and the
301+
subclasses without set collections will be store documents in the parent's
302+
collection.
303+
304+
.. note::
305+
306+
Note that changing the collection that a subclass is stored in will cause
307+
documents of that subclass to no longer be found in the results of querying
308+
its parent class.

docs/release-notes/mongoid-8.1.txt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,29 @@ to or from a certain value:
213213
# => true
214214
person.name_changed?(from: "Trout", to: "Fletcher")
215215
# => false
216+
217+
218+
Allowed ``store_in`` to be called on subclasses
219+
-----------------------------------------------
220+
221+
Starting in Mongoid 8.1, subclasses can now specify which collection its
222+
documents should be stored in using the ``store_in`` macro:
223+
224+
.. code:: ruby
225+
226+
class Shape
227+
include Mongoid::Document
228+
store_in collection: :shapes
229+
end
230+
231+
class Circle < Shape
232+
store_in collection: :circles
233+
end
234+
235+
class Square < Shape
236+
store_in collection: :squares
237+
end
238+
239+
Previously, an error was raised if this was done. See the section on
240+
:ref:`Inheritance Persistence Context <inheritance-persistence-context>`
241+
for more details.

lib/config/locales/en.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -324,12 +324,6 @@ en:
324324
\_\_\_\_include Mongoid::Document\n
325325
\_\_\_\_store_in collection: 'artists', database: 'music'\n
326326
\_\_end\n\n"
327-
invalid_storage_parent:
328-
message: "Invalid store_in call on class %{klass}."
329-
summary: "The :store_in macro can only be called on a base Mongoid Document"
330-
resolution: "Remove the store_in call on class %{klass}, as it will use its
331-
parent store configuration. Or remove the hierarchy extension for this
332-
class."
333327
invalid_time:
334328
message: "'%{value}' is not a valid Time."
335329
summary: "Mongoid tries to serialize the values for Date, DateTime, and

lib/mongoid/clients/storage_options.rb

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ module StorageOptions
66
extend ActiveSupport::Concern
77

88
included do
9-
10-
cattr_accessor :storage_options, instance_writer: false do
11-
storage_options_defaults
12-
end
9+
class_attribute :storage_options, instance_writer: false, default: storage_options_defaults
1310
end
1411

1512
module ClassMethods
@@ -49,7 +46,7 @@ module ClassMethods
4946
# @return [ Class ] The model class.
5047
def store_in(options)
5148
Validators::Storage.validate(self, options)
52-
storage_options.merge!(options)
49+
self.storage_options = self.storage_options.merge(options)
5350
end
5451

5552
# Reset the store_in options

lib/mongoid/clients/validators/storage.rb

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,9 @@ module Storage
2020
# @param [ Hash | String | Symbol ] options The provided options.
2121
def validate(klass, options)
2222
valid_keys?(options) or raise Errors::InvalidStorageOptions.new(klass, options)
23-
valid_parent?(klass) or raise Errors::InvalidStorageParent.new(klass)
2423
end
2524

2625
private
27-
# Determine if the current klass is valid to change store_in
28-
# options
29-
#
30-
# @api private
31-
#
32-
# @param [ Class ] klass
33-
#
34-
# @return [ true | false ] If the class is valid.
35-
def valid_parent?(klass)
36-
!klass.superclass.include?(Mongoid::Document)
37-
end
3826

3927
# Determine if all keys in the options hash are valid.
4028
#

lib/mongoid/errors.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
require "mongoid/errors/invalid_session_use"
3636
require "mongoid/errors/invalid_set_polymorphic_relation"
3737
require "mongoid/errors/invalid_storage_options"
38-
require "mongoid/errors/invalid_storage_parent"
3938
require "mongoid/errors/invalid_time"
4039
require "mongoid/errors/inverse_not_found"
4140
require "mongoid/errors/mixed_relations"

lib/mongoid/errors/invalid_storage_parent.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ module Mongoid
44
module Errors
55

66
# Raised when calling store_in in a sub-class of Mongoid::Document
7+
#
8+
# @deprecated
79
class InvalidStorageParent < MongoidError
810

911
# Create the new error.

spec/mongoid/clients_spec.rb

Lines changed: 122 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -738,19 +738,6 @@
738738
end
739739
end
740740

741-
context "when provided a class that extends another document" do
742-
743-
let(:klass) do
744-
Class.new(Band)
745-
end
746-
747-
it "raises an error" do
748-
expect {
749-
klass.store_in(database: :artists)
750-
}.to raise_error(Mongoid::Errors::InvalidStorageParent)
751-
end
752-
end
753-
754741
context "when provided a hash" do
755742

756743
context "when the hash is not valid" do
@@ -762,6 +749,128 @@
762749
end
763750
end
764751
end
752+
753+
context "when it is called on a subclass" do
754+
755+
let(:client) { StoreParent.collection.client }
756+
let(:parent) { StoreParent.create! }
757+
let(:child1) { StoreChild1.create! }
758+
let(:child2) { StoreChild2.create! }
759+
760+
before do
761+
class StoreParent
762+
include Mongoid::Document
763+
end
764+
765+
class StoreChild1 < StoreParent
766+
end
767+
768+
class StoreChild2 < StoreParent
769+
end
770+
end
771+
772+
after do
773+
Object.send(:remove_const, :StoreParent)
774+
Object.send(:remove_const, :StoreChild1)
775+
Object.send(:remove_const, :StoreChild2)
776+
end
777+
778+
context "when it is not called on the parent" do
779+
780+
context "when it is called on all subclasses" do
781+
782+
before do
783+
StoreChild1.store_in collection: :store_ones
784+
StoreChild2.store_in collection: :store_twos
785+
[ parent, child1, child2 ]
786+
end
787+
788+
let(:db_parent) { client['store_parents'].find.first }
789+
let(:db_child1) { client['store_ones'].find.first }
790+
let(:db_child2) { client['store_twos'].find.first }
791+
792+
it "stores the documents in the correct collections" do
793+
expect(db_parent).to eq({ "_id" => parent.id, "_type" => "StoreParent" })
794+
expect(db_child1).to eq({ "_id" => child1.id, "_type" => "StoreChild1" })
795+
expect(db_child2).to eq({ "_id" => child2.id, "_type" => "StoreChild2" })
796+
end
797+
798+
it "only queries from its own collections" do
799+
expect(StoreParent.count).to eq(1)
800+
expect(StoreChild1.count).to eq(1)
801+
expect(StoreChild2.count).to eq(1)
802+
end
803+
end
804+
805+
context "when it is called on one of the subclasses" do
806+
807+
before do
808+
StoreChild1.store_in collection: :store_ones
809+
[ parent, child1, child2 ]
810+
end
811+
812+
let(:db_parent) { client['store_parents'].find.first }
813+
let(:db_child1) { client['store_ones'].find.first }
814+
let(:db_child2) { client['store_parents'].find.to_a.last }
815+
816+
it "stores the documents in the correct collections" do
817+
expect(db_parent).to eq({ "_id" => parent.id, "_type" => "StoreParent" })
818+
expect(db_child1).to eq({ "_id" => child1.id, "_type" => "StoreChild1" })
819+
expect(db_child2).to eq({ "_id" => child2.id, "_type" => "StoreChild2" })
820+
end
821+
822+
it "queries from its own collections" do
823+
expect(StoreParent.count).to eq(2)
824+
expect(StoreChild1.count).to eq(1)
825+
expect(StoreChild2.count).to eq(1)
826+
end
827+
end
828+
end
829+
830+
context "when it is called on the parent" do
831+
832+
before do
833+
StoreParent.store_in collection: :st_parents
834+
end
835+
836+
context "when it is called on all subclasses" do
837+
838+
before do
839+
StoreChild1.store_in collection: :store_ones
840+
StoreChild2.store_in collection: :store_twos
841+
[ parent, child1, child2 ]
842+
end
843+
844+
let(:db_parent) { client['st_parents'].find.first }
845+
let(:db_child1) { client['store_ones'].find.first }
846+
let(:db_child2) { client['store_twos'].find.first }
847+
848+
it "stores the documents in the correct collections" do
849+
expect(db_parent).to eq({ "_id" => parent.id, "_type" => "StoreParent" })
850+
expect(db_child1).to eq({ "_id" => child1.id, "_type" => "StoreChild1" })
851+
expect(db_child2).to eq({ "_id" => child2.id, "_type" => "StoreChild2" })
852+
end
853+
end
854+
855+
context "when it is called on one of the subclasses" do
856+
857+
before do
858+
StoreChild1.store_in collection: :store_ones
859+
[ parent, child1, child2 ]
860+
end
861+
862+
let(:db_parent) { client['st_parents'].find.first }
863+
let(:db_child1) { client['store_ones'].find.first }
864+
let(:db_child2) { client['st_parents'].find.to_a.last }
865+
866+
it "stores the documents in the correct collections" do
867+
expect(db_parent).to eq({ "_id" => parent.id, "_type" => "StoreParent" })
868+
expect(db_child1).to eq({ "_id" => child1.id, "_type" => "StoreChild1" })
869+
expect(db_child2).to eq({ "_id" => child2.id, "_type" => "StoreChild2" })
870+
end
871+
end
872+
end
873+
end
765874
end
766875

767876
describe ".with" do

0 commit comments

Comments
 (0)