Skip to content

Commit 0efdc2f

Browse files
p-mongop
andauthored
MONGOID-4592 Add examples of using read_attribute and write_attribute to implement custom field behavior (#5082)
* MONGOID-4592 custom fields -> custom field types * MONGOID-4592 read_attribute and write_attribute examples Co-authored-by: Oleg Pudeyev <code@olegp.name>
1 parent 94ce33e commit 0efdc2f

File tree

1 file changed

+162
-90
lines changed

1 file changed

+162
-90
lines changed

source/reference/fields.txt

Lines changed: 162 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -545,103 +545,14 @@ This is useful for storing different values in ``id`` and ``_id`` fields:
545545
# => #<Band _id: 5fc1c3f42c97a6590684046c, id: "42">
546546

547547

548-
Custom Fields
549-
-------------
550-
551-
You can define custom types in Mongoid and determine how they are serialized and deserialized.
552-
You simply need to provide three methods on it for Mongoid to call to convert your object to
553-
and from MongoDB friendly values.
554-
555-
.. code-block:: ruby
556-
557-
class Profile
558-
include Mongoid::Document
559-
field :location, type: Point
560-
end
561-
562-
class Point
563-
564-
attr_reader :x, :y
565-
566-
def initialize(x, y)
567-
@x, @y = x, y
568-
end
569-
570-
# Converts an object of this instance into a database friendly value.
571-
def mongoize
572-
[ x, y ]
573-
end
574-
575-
class << self
576-
577-
# Get the object as it was stored in the database, and instantiate
578-
# this custom class from it.
579-
def demongoize(object)
580-
Point.new(object[0], object[1])
581-
end
582-
583-
# Takes any possible object and converts it to how it would be
584-
# stored in the database.
585-
def mongoize(object)
586-
case object
587-
when Point then object.mongoize
588-
when Hash then Point.new(object[:x], object[:y]).mongoize
589-
else object
590-
end
591-
end
592-
593-
# Converts the object that was supplied to a criteria and converts it
594-
# into a database friendly form.
595-
def evolve(object)
596-
case object
597-
when Point then object.mongoize
598-
else object
599-
end
600-
end
601-
end
602-
end
603-
604-
The instance method ``mongoize`` takes an instance of your object, and converts it
605-
into how it will be stored in the database. In our example above, we want to store our
606-
point object as an array in the form ``[ x, y ]``.
607-
608-
The class method ``demongoize`` takes an object as how it was stored in the database,
609-
and is responsible for instantiating an object of your custom type. In this case, we
610-
take an array and instantiate a ``Point`` from it.
611-
612-
The class method ``mongoize`` takes an object that you would use to set on your model
613-
from your application code, and create the object as it would be stored in the database.
614-
This is for cases where you are not passing your model instances of your custom type in the setter:
615-
616-
.. code-block:: ruby
617-
618-
point = Point.new(12, 24)
619-
venue = Venue.new(location: point) # This uses the mongoize instance method.
620-
venue = Venue.new(location: [ 12, 24 ]) # This uses the mongoize class method.
621-
622-
The class method ``evolve`` takes an object, and determines how it is to be transformed
623-
for use in criteria. For example we may want to write a query like so:
624-
625-
.. code-block:: ruby
626-
627-
point = Point.new(12, 24)
628-
Venue.where(location: point)
629-
630-
Note that when accessing custom fields from the document, you will get a new instance
631-
of that object with each call to the getter. This is because Mongoid is generating a new
632-
object from the raw attributes on each access.
633-
634-
We need the point object in the criteria to be transformed to a Mongo friendly value when
635-
it is not as well, ``evolve`` is the method that takes care of this. We check if the passed
636-
in object is a ``Point`` first, in case we also want to be able to pass in ordinary arrays instead.
637-
638548
Reserved Names
639549
--------------
640550

641551
Attempting to define a field on a document that conflicts with a reserved
642552
method name in Mongoid will raise an error. The list of reserved names can
643553
be obtained by invoking the ``Mongoid.destructive_fields`` method.
644554

555+
645556
Field Redefinition
646557
------------------
647558

@@ -729,6 +640,167 @@ alias can :ref:`be removed <unalias-id>` if desired (such as to integrate
729640
with systems that use the ``id`` field to store value different from ``_id``.
730641

731642

643+
Customizing Field Behavior
644+
==========================
645+
646+
Mongoid offers several options for customizing the behavior of fields.
647+
648+
649+
Custom Getters And Setters
650+
--------------------------
651+
652+
You can define custom getters and setters for fields to modify the values
653+
when they are being accessed or written. The getters and setters use the
654+
same name as the field. Use ``read_attribute`` and ``write_attribute``
655+
methods inside the getters and setters to operate on the raw attribute
656+
values.
657+
658+
For example, Mongoid provides the ``:default`` field option to write a
659+
default value into the field. If you wish to have a field default value
660+
in your application but do not wish to persist it, you can override the
661+
getter as follows:
662+
663+
.. code-block:: ruby
664+
665+
class DistanceMeasurement
666+
include Mongoid::Document
667+
668+
field :value, type: Float
669+
field :unit, type: String
670+
671+
def unit
672+
read_attribute(:unit) || "m"
673+
end
674+
675+
def to_s
676+
"#{value} #{unit}"
677+
end
678+
end
679+
680+
measurement = DistanceMeasurement.new(value: 2)
681+
measurement.to_s
682+
# => "2.0 m"
683+
measurement.attributes
684+
# => {"_id"=>BSON::ObjectId('613fa0b0a15d5d61502f3447'), "value"=>2.0}
685+
686+
To give another example, a field which converts empty strings to nil values
687+
may be implemented as follows:
688+
689+
.. code-block:: ruby
690+
691+
class DistanceMeasurement
692+
include Mongoid::Document
693+
694+
field :value, type: Float
695+
field :unit, type: String
696+
697+
def unit=(value)
698+
if value.blank?
699+
value = nil
700+
end
701+
write_attribute(:unit, value)
702+
end
703+
end
704+
705+
measurement = DistanceMeasurement.new(value: 2, unit: "")
706+
measurement.attributes
707+
# => {"_id"=>BSON::ObjectId('613fa15aa15d5d617216104c'), "value"=>2.0, "unit"=>nil}
708+
709+
710+
Custom Field Types
711+
------------------
712+
713+
You can define custom types in Mongoid and determine how they are serialized
714+
and deserialized. You simply need to provide three methods on it for Mongoid
715+
to call to convert your object to and from MongoDB friendly values.
716+
717+
.. code-block:: ruby
718+
719+
class Profile
720+
include Mongoid::Document
721+
field :location, type: Point
722+
end
723+
724+
class Point
725+
726+
attr_reader :x, :y
727+
728+
def initialize(x, y)
729+
@x, @y = x, y
730+
end
731+
732+
# Converts an object of this instance into a database friendly value.
733+
def mongoize
734+
[ x, y ]
735+
end
736+
737+
class << self
738+
739+
# Get the object as it was stored in the database, and instantiate
740+
# this custom class from it.
741+
def demongoize(object)
742+
Point.new(object[0], object[1])
743+
end
744+
745+
# Takes any possible object and converts it to how it would be
746+
# stored in the database.
747+
def mongoize(object)
748+
case object
749+
when Point then object.mongoize
750+
when Hash then Point.new(object[:x], object[:y]).mongoize
751+
else object
752+
end
753+
end
754+
755+
# Converts the object that was supplied to a criteria and converts it
756+
# into a database friendly form.
757+
def evolve(object)
758+
case object
759+
when Point then object.mongoize
760+
else object
761+
end
762+
end
763+
end
764+
end
765+
766+
The instance method ``mongoize`` takes an instance of your object, and
767+
converts it into how it will be stored in the database. In our example above,
768+
we want to store our point object as an array in the form ``[ x, y ]``.
769+
770+
The class method ``demongoize`` takes an object as how it was stored in the
771+
database, and is responsible for instantiating an object of your custom type.
772+
In this case, we take an array and instantiate a ``Point`` from it.
773+
774+
The class method ``mongoize`` takes an object that you would use to set on
775+
your model from your application code, and create the object as it would be
776+
stored in the database. This is for cases where you are not passing your
777+
model instances of your custom type in the setter:
778+
779+
.. code-block:: ruby
780+
781+
point = Point.new(12, 24)
782+
venue = Venue.new(location: point) # This uses the mongoize instance method.
783+
venue = Venue.new(location: [ 12, 24 ]) # This uses the mongoize class method.
784+
785+
The class method ``evolve`` takes an object, and determines how it is to be
786+
transformed for use in criteria. For example we may want to write a query
787+
like so:
788+
789+
.. code-block:: ruby
790+
791+
point = Point.new(12, 24)
792+
Venue.where(location: point)
793+
794+
Note that when accessing custom fields from the document, you will get a
795+
new instance of that object with each call to the getter. This is because
796+
Mongoid is generating a new object from the raw attributes on each access.
797+
798+
We need the point object in the criteria to be transformed to a
799+
MongoDB-friendly value when it is not as well, ``evolve`` is the method
800+
that takes care of this. We check if the passed in object is a ``Point``
801+
first, in case we also want to be able to pass in ordinary arrays instead.
802+
803+
732804
.. _dynamic-fields:
733805

734806
Dynamic Fields

0 commit comments

Comments
 (0)