Skip to content

Commit

Permalink
MONGOID-4812 Allow id to be a regular field in models (not an alias o…
Browse files Browse the repository at this point in the history
…f _id) (#4929)

* unalias id/_id

* remove other traces of aliasing

* keep aliased fields

* create a method to unalias an attribute

* remove whitespace

* make #only more straightforward & efficient

* extract Criteria#only tests in their own file

* verify only works correctly when id is unaliased

* note that aliases are not included

* rename the test because I will add without to it

* move #without tests to projection

* fix #without when id is unaliased

* test cases for using #without when id is unaliased

* Document #without

* Note that id cannot be omitted via without

* basic integration test

* clean up

* add docstring

* remove extra whitespace

* add tutorial documentation

* use _id instead of id everywhere in the codebase

* add an upgrading note

* fix cloning

* fix nested_one association

Co-authored-by: Emily Giurleo <[email protected]>
Co-authored-by: Oleg Pudeyev <[email protected]>
  • Loading branch information
3 people authored Nov 30, 2020
1 parent a080c06 commit 802277e
Show file tree
Hide file tree
Showing 30 changed files with 729 additions and 342 deletions.
91 changes: 75 additions & 16 deletions docs/tutorials/mongoid-documents.txt
Original file line number Diff line number Diff line change
Expand Up @@ -591,30 +591,89 @@ the other attributes are set, use the ``pre_processed: true`` field option:
pre_processed: true


.. _storage-field-names:

Specifying Storage Field Names
------------------------------

One of the drawbacks of having a schemaless database is that MongoDB must
store all field information along with every document, meaning that it
takes up a lot of storage space in RAM and on disk. A common pattern to limit
this is to alias fields to a small number of characters, while keeping the
domain in the application expressive. Mongoid allows you to do this and
reference the fields in the domain via their long names in getters, setters,
and criteria while performing the conversion for you.

.. code-block:: ruby

class Band
include Mongoid::Document
field :n, as: :name, type: String
end

band = Band.new(name: "Placebo")
band.attributes # { "n" => "Placebo" }

criteria = Band.where(name: "Placebo")
criteria.selector # { "n" => "Placebo" }


.. _field-aliases:

Aliasing Fields
---------------
Field Aliases
-------------

One of the drawbacks of having a schemaless database is that MongoDB must store all field
information along with every document, meaning that it takes up a lot of storage space in RAM
and on disk. A common pattern to limit this is to alias fields to a small number of characters,
while keeping the domain in the application expressive. Mongoid allows you to do this and
reference the fields in the domain via their long names in getters, setters, and criteria while
performing the conversion for you.
It is possible to define field aliases. The value will be stored in the
destination field but can be accessed from either the destination field or
from the aliased field:

.. code-block:: ruby

class Band
include Mongoid::Document
field :n, as: :name, type: String
end
class Band
include Mongoid::Document

field :name, type: String
alias_attribute :n, :name
end

band = Band.new(n: 'Astral Projection')
# => #<Band _id: 5fc1c1ee2c97a64accbeb5e1, name: "Astral Projection">

band.attributes
# => {"_id"=>BSON::ObjectId('5fc1c1ee2c97a64accbeb5e1'), "name"=>"Astral Projection"}

band.n
# => "Astral Projection"

Aliases can be removed from model classes using the ``unalias_attribute``
method.

.. code-block:: ruby

class Band
unalias_attribute :n
end

.. _unalias-id:

Unaliasing ``id``
`````````````````

band = Band.new(name: "Placebo")
band.attributes # { "n" => "Placebo" }
``unalias_attribute`` can be used to remove the predefined ``id`` alias.
This is useful for storing different values in ``id`` and ``_id`` fields:

.. code-block:: ruby

class Band
include Mongoid::Document

unalias_attribute :id
field :id, type: String
end

Band.new(id: '42')
# => #<Band _id: 5fc1c3f42c97a6590684046c, id: "42">

criteria = Band.where(name: "Placebo")
criteria.selector # { "n" => "Placebo" }

Custom Fields
-------------
Expand Down
42 changes: 41 additions & 1 deletion docs/tutorials/mongoid-queries.txt
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ the query:
Aliases
```````

Queries take into account :ref:`field aliases <field-aliases>`:
Queries take into account :ref:`storage field names <storage-field-names>`
and :ref:`field aliases <field-aliases>`:

.. code-block:: ruby

Expand Down Expand Up @@ -730,6 +731,9 @@ as the following example shows:
Projection
----------

``only``
````````

The ``only`` method retrieves only the specified fields from the database. This
operation is sometimes called "projection".

Expand Down Expand Up @@ -831,6 +835,42 @@ loaded with ``only`` for those associations to be loaded. For example:
Band.where(name: 'Astral Projection').only(:name, :manager_ids).first.managers
# => [#<Manager _id: 5c5dc2f0026d7c1730969843, band_ids: [BSON::ObjectId('5c5dc2f0026d7c1730969842')]>]

``without``
```````````

The opposite of ``only``, ``without`` causes the specified fields to be omitted:

.. code-block:: ruby

Band.without(:name)
# =>
# #<Mongoid::Criteria
# selector: {}
# options: {:fields=>{"name"=>0}}
# class: Band
# embedded: false>

Because Mongoid requires the ``_id`` field for various operations, it (as well
as its ``id`` alias) cannot be omitted via ``without``:

.. code-block:: ruby

Band.without(:name, :id)
# =>
# #<Mongoid::Criteria
# selector: {}
# options: {:fields=>{"name"=>0}}
# class: Band
# embedded: false>

Band.without(:name, :_id)
# =>
# #<Mongoid::Criteria
# selector: {}
# options: {:fields=>{"name"=>0}}
# class: Band
# embedded: false>


Ordering
--------
Expand Down
6 changes: 6 additions & 0 deletions docs/tutorials/mongoid-upgrade.txt
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ New Embedded Matching Operators
Mongoid 7.3 adds support for bitwise operators, ``$comment``, ``$mod`` and
``$type`` operators when :ref:`embedded matching <embedded-matching>`.

Unaliasing ``id`` Field
-----------------------

It is now possible to :ref:`remove the id alias in models <unalias-id>`,
to make ``id`` a regular field.


Upgrading to Mongoid 7.2
========================
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/association/accessors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ 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).only(:id).map(&:id)
send(association.name).only(:_id).map(&:_id)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/association/constrainable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def convert_to_foreign_key(object)

def convert_polymorphic(object)
if object.is_a?(Mongoid::Document)
object.id
object._id
else
BSON::ObjectId.mongoize(object)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/association/embedded/embeds_many/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ def unscoped
private

def object_already_related?(document)
_target.any? { |existing| existing.id && existing === document }
_target.any? { |existing| existing._id && existing === document }
end

# Appends the document to the target array, updating the index on the
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/association/nested/many.rb
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def update_nested_relation(parent, id, attrs)
first = existing.first
converted = first ? convert_id(first.class, id) : id

if existing.where(id: converted).exists?
if existing.where(_id: converted).exists?
# document exists in association
doc = existing.find(converted)
if destroyable?(attrs)
Expand Down
6 changes: 4 additions & 2 deletions lib/mongoid/association/nested/one.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ def initialize(association, attributes, options)
#
# @since 2.0.0
def acceptable_id?
id = convert_id(existing.class, attributes[:id])
id = association.klass.extract_id_field(attributes)
id = convert_id(existing.class, id)
existing._id == id || id.nil? || (existing._id != id && update_only?)
end

Expand All @@ -84,7 +85,8 @@ def acceptable_id?
#
# @since 2.0.0
def delete?
destroyable? && !attributes[:id].nil?
id = association.klass.extract_id_field(attributes)
destroyable? && !id.nil?
end

# Can the existing association potentially be destroyed?
Expand Down
4 changes: 2 additions & 2 deletions lib/mongoid/association/referenced/has_many/enumerable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ def any?(*args)
# @since 2.1.0
def first(opts = {})
_loaded.try(:values).try(:first) ||
_added[(ul = _unloaded.try(:first, opts)).try(:id)] ||
_added[(ul = _unloaded.try(:first, opts)).try(:_id)] ||
ul ||
_added.values.try(:first)
end
Expand Down Expand Up @@ -368,7 +368,7 @@ def in_memory
def last(opts = {})
_added.values.try(:last) ||
_loaded.try(:values).try(:last) ||
_added[(ul = _unloaded.try(:last, opts)).try(:id)] ||
_added[(ul = _unloaded.try(:last, opts)).try(:_id)] ||
ul
end

Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/association/referenced/has_many/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ def already_related?(document)
document.persisted? &&
document._association &&
document.respond_to?(document._association.foreign_key) &&
document.__send__(document._association.foreign_key) == _base.id
document.__send__(document._association.foreign_key) == _base._id
end

# Instantiate the binding associated with this association.
Expand Down
4 changes: 2 additions & 2 deletions lib/mongoid/association/referenced/has_one/nested_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def initialize(association, attributes, options)
#
# @since 2.0.0
def acceptable_id?
id = convert_id(existing.class, attributes[:id])
id = convert_id(existing.class, attributes[:_id])
existing._id == id || id.nil? || (existing._id != id && update_only?)
end

Expand All @@ -82,7 +82,7 @@ def acceptable_id?
#
# @since 2.0.0
def delete?
destroyable? && !attributes[:id].nil?
destroyable? && !attributes[:_id].nil?
end

# Can the existing association potentially be destroyed?
Expand Down
20 changes: 20 additions & 0 deletions lib/mongoid/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,26 @@ def alias_attribute(name, original)
alias_method "#{name}_will_change!", "#{original}_will_change!"
alias_method "#{name}_before_type_cast", "#{original}_before_type_cast"
end

# Removes a field alias.
#
# @param [ Symbol ] name The aliased field name to remove.
def unalias_attribute(name)
unless aliased_fields.delete(name.to_s)
raise AttributeError, "Field #{name} is not an aliased field"
end

remove_method name
remove_method "#{name}="
remove_method "#{name}?"
remove_method "#{name}_change"
remove_method "#{name}_changed?"
remove_method "reset_#{name}!"
remove_method "reset_#{name}_to_default!"
remove_method "#{name}_was"
remove_method "#{name}_will_change!"
remove_method "#{name}_before_type_cast"
end
end

private
Expand Down
4 changes: 2 additions & 2 deletions lib/mongoid/cacheable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ module Cacheable
# @since 2.4.0
def cache_key
return "#{model_key}/new" if new_record?
return "#{model_key}/#{id}-#{updated_at.utc.to_s(cache_timestamp_format)}" if do_or_do_not(:updated_at)
"#{model_key}/#{id}"
return "#{model_key}/#{_id}-#{updated_at.utc.to_s(cache_timestamp_format)}" if do_or_do_not(:updated_at)
"#{model_key}/#{_id}"
end
end
end
2 changes: 1 addition & 1 deletion lib/mongoid/copyable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def clone
# @note This next line is here to address #2704, even though having an
# _id and id field in the document would cause problems with Mongoid
# elsewhere.
attrs = clone_document.except("_id", "id")
attrs = clone_document.except(*self.class.id_fields)
dynamic_attrs = {}
_attribute_names = self.attribute_names
attrs.reject! do |attr_name, value|
Expand Down
9 changes: 4 additions & 5 deletions lib/mongoid/criteria.rb
Original file line number Diff line number Diff line change
Expand Up @@ -336,16 +336,15 @@ def empty_and_chainable?
#
# @since 1.0.0
def only(*args)
return clone if args.flatten.empty?
args = args.flatten
return clone if args.empty?
if (args & Fields::IDS).empty?
args.unshift(:_id)
end
if klass.hereditary?
super(*args.push(klass.discriminator_key.to_sym))
else
super(*args)
args.push(klass.discriminator_key.to_sym)
end
super(*args)
end

# Set the read preference for the criteria.
Expand Down Expand Up @@ -375,7 +374,7 @@ def read(value = nil)
#
# @since 4.0.3
def without(*args)
args -= Fields::IDS
args -= id_fields
super(*args)
end

Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/criteria/findable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def from_database(ids)
# @since 3.0.0
def mongoize_ids(ids)
ids.map do |id|
id = id[:id] if id.respond_to?(:keys) && id[:id]
id = id[:_id] if id.respond_to?(:keys) && id[:_id]
klass.fields["_id"].mongoize(id)
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def model_name
#
# @since 2.4.0
def to_key
(persisted? || destroyed?) ? [ id.to_s ] : nil
(persisted? || destroyed?) ? [ _id.to_s ] : nil
end

# Return an array with this +Document+ only in it.
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/evolvable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module Evolvable
#
# @since 3.0.0
def __evolve_object_id__
id
_id
end
end
end
Loading

0 comments on commit 802277e

Please sign in to comment.