Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
323 changes: 323 additions & 0 deletions docs/reference/associations.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,329 @@ Associations
:depth: 2
:class: singlecol

Embedded Associations
=====================

CouchbaseOrm supports embedding documents within a parent document using
``embeds_one`` and ``embeds_many``. Unlike referenced associations, embedded
documents are stored directly within the parent document and do not have their
own separate document ID in the database. This is useful for modeling
has-one and has-many relationships where the child documents don't need to exist
independently.

Embeds One
----------

Use the ``embeds_one`` association to declare that the parent embeds a single
child document:

.. code-block:: ruby

class Profile < CouchbaseOrm::Base
attribute :bio, :string
attribute :website, :string
end

class User < CouchbaseOrm::Base
attribute :name, :string
embeds_one :profile
end

user = User.create!(name: 'Alice', profile: { bio: 'Software Engineer' })
# => #<User _id: "user::123", name: "Alice", profile: {...}>

user.profile
# => #<Profile bio: "Software Engineer", website: nil>

user.profile.bio = 'Senior Software Engineer'
user.profile = user.profile # Reassign to track changes
user.save!

The embedded document is stored as a hash within the parent:

.. code-block:: ruby

# In the database, the document looks like:
{
"_id": "user::123",
"name": "Alice",
"profile": {
"bio": "Software Engineer",
"website": null
}
}

Embedded documents cannot be saved, destroyed, or reloaded independently:

.. code-block:: ruby

user.profile.save
# => raises "Cannot save an embedded document!"

Embeds Many
-----------

Use the ``embeds_many`` association to declare that the parent embeds multiple
child documents:

.. code-block:: ruby

class Address < CouchbaseOrm::Base
attribute :street, :string
attribute :city, :string
end

class Person < CouchbaseOrm::Base
attribute :name, :string
embeds_many :addresses
end

person = Person.create!(
name: 'Bob',
addresses: [
{ street: '123 Main St', city: 'New York' },
{ street: '456 Elm St', city: 'Boston' }
]
)

person.addresses
# => [#<Address street: "123 Main St", city: "New York">,
# #<Address street: "456 Elm St", city: "Boston">]

person.addresses << Address.new(street: '789 Oak Ave', city: 'Chicago')
person.addresses = person.addresses # Reassign to track changes
person.save!

Polymorphic Embedded Associations
----------------------------------

Both ``embeds_one`` and ``embeds_many`` support polymorphic associations,
allowing you to embed different types of documents in the same association:

.. code-block:: ruby

class Image < CouchbaseOrm::Base
attribute :url, :string
attribute :caption, :string
end

class Video < CouchbaseOrm::Base
attribute :url, :string
attribute :duration, :integer
end

class Post < CouchbaseOrm::Base
embeds_one :media, polymorphic: true
end

# Embed an image using an object
post = Post.create!(media: Image.new(url: 'photo.jpg', caption: 'Sunset'))
post.media
# => #<Image url: "photo.jpg", caption: "Sunset">

# Or use a hash with type key (snake_case class name)
post = Post.create!(media: { type: 'image', url: 'photo.jpg', caption: 'Sunset' })

# Switch to a video
post.media = Video.new(url: 'clip.mp4', duration: 120)
post.save!

For ``embeds_many`` with polymorphism:

.. code-block:: ruby

class Article < CouchbaseOrm::Base
embeds_many :attachments, polymorphic: true
end

# Using objects
article = Article.create!(
attachments: [
Image.new(url: 'diagram.png', caption: 'Architecture'),
Video.new(url: 'demo.mp4', duration: 90)
]
)

# Or using hashes with type key (snake_case class names)
article = Article.create!(
attachments: [
{ type: 'image', url: 'diagram.png', caption: 'Architecture' },
{ type: 'video', url: 'demo.mp4', duration: 90 }
]
)

article.attachments
# => [#<Image ...>, #<Video ...>]

When using polymorphic embedded associations, the type information is stored
inside each embedded document as a ``type`` field. For example:

.. code-block:: json

{
"_id": "post::123",
"media": {
"type": "Image",
"url": "photo.jpg",
"caption": "Sunset"
}
}

**Note**: When using hashes with polymorphic associations, you must include
the ``type`` key (either as a symbol ``:type`` or string ``'type'``) with
the snake_case version of the class name. For example, use ``'image'`` for
the ``Image`` class or ``'video_attachment'`` for the ``VideoAttachment`` class.
Without it, an ``ArgumentError`` will be raised.

Restricting Polymorphic Types
------------------------------

You can restrict which types are allowed in a polymorphic association by
passing an array of allowed class names to the ``polymorphic`` parameter:

.. code-block:: ruby

class Post < CouchbaseOrm::Base
# Only Image and Video are allowed
embeds_one :media, polymorphic: ['Image', 'Video']
end

class Article < CouchbaseOrm::Base
# Only specific attachment types allowed
embeds_many :attachments, polymorphic: ['ImageAttachment', 'VideoAttachment']
end

# This works
post = Post.new(media: Image.new(url: 'photo.jpg'))
post.valid?
# => true

# This raises a validation error
post = Post.new(media: Audio.new(url: 'song.mp3'))
post.valid?
# => false
post.errors[:media]
# => ["Audio is not an allowed type. Allowed types: Image, Video"]

You can use snake_case names in the array, which will be automatically
converted to CamelCase class names:

.. code-block:: ruby

class Post < CouchbaseOrm::Base
embeds_one :media, polymorphic: [:image, 'video'] # Same as ['Image', 'Video']
end

**Polymorphic Type Validator**: When type restrictions are specified, CouchbaseOrm
automatically adds a ``PolymorphicTypeValidator`` to ensure that only allowed types
can be assigned to the association. The validator:

- Validates both single objects (``embeds_one``) and arrays (``embeds_many``)
- Provides clear error messages listing the allowed types
- Skips validation when no type restrictions are set or the value is nil
- Runs during normal model validation (``valid?``, ``save``, ``create``, etc.)

For ``embeds_many`` associations with type restrictions, each item in the array
is validated individually:

.. code-block:: ruby

class Article < CouchbaseOrm::Base
embeds_many :attachments, polymorphic: ['Image', 'Video']
end

article = Article.new(
attachments: [
Image.new(url: 'photo.jpg'),
Audio.new(url: 'song.mp3') # Not allowed!
]
)

article.valid?
# => false
article.errors[:attachments]
# => ["item #1 (Audio) is not an allowed type. Allowed types: Image, Video"]

Custom Storage Keys
-------------------

Use the ``store_as`` option to specify a different attribute name for storage:

.. code-block:: ruby

class User < CouchbaseOrm::Base
embeds_one :profile, store_as: 'p'
embeds_many :addresses, store_as: 'addrs'
end

# In the database:
{
"_id": "user::123",
"p": { "bio": "..." },
"addrs": [{ "street": "..." }]
}

Embedded Association Validation
--------------------------------

By default, embedded associations are validated when the parent is validated.
You can disable this with ``validate: false``:

.. code-block:: ruby

class Profile < CouchbaseOrm::Base
attribute :bio, :string
validates :bio, presence: true
end

class User < CouchbaseOrm::Base
embeds_one :profile
end

user = User.new(profile: { bio: nil })
user.valid?
# => false
user.errors[:profile]
# => ["is invalid"]

# To disable validation:
class User < CouchbaseOrm::Base
embeds_one :profile, validate: false
end

Custom Class Names
------------------

Specify a different class name for embedded associations when the class cannot
be inferred from the association name:

.. code-block:: ruby

class User < CouchbaseOrm::Base
embeds_one :bio, class_name: 'Profile'
embeds_many :locations, class_name: 'Address'
end

Key Differences from Referenced Associations
---------------------------------------------

Embedded associations differ from referenced associations in several ways:

- **Storage**: Embedded documents are stored within the parent document, not as
separate documents in the database.

- **Lifecycle**: Embedded documents cannot be saved, updated, or destroyed
independently. They are always saved as part of the parent document.

- **Performance**: Reading a parent document also loads all embedded documents
in a single operation, which can be more efficient than separate queries.

- **Querying**: Embedded documents cannot be queried independently. You must
query the parent document and then access the embedded documents.

- **Identity**: Embedded documents do not have their own document ID by default
(the ``id`` field is removed when embedding).

Referenced Associations
=======================

Expand Down
1 change: 1 addition & 0 deletions lib/couchbase-orm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module CouchbaseOrm
autoload :EmbedsOne, 'couchbase-orm/utilities/embeds_one'
autoload :EmbedsMany, 'couchbase-orm/utilities/embeds_many'
autoload :EmbeddedAssociatedValidator, 'couchbase-orm/validators/embedded_associated_validator'
autoload :PolymorphicTypeValidator, 'couchbase-orm/validators/polymorphic_type_validator'

# if COUCHBASE_ORM_DEBUG environement variable exist then logger is set to Logger::DEBUG level
# else logger is set to Logger::INFO level
Expand Down
Loading