Associations#
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:
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:
# 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:
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:
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:
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:
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:
{
"_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:
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:
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:
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:
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:
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:
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
idfield is removed when embedding).
Referenced Associations#
CouchbaseOrm supports the has_many, belongs_to and
has_and_belongs_to_many associations familiar to ActiveRecord users.
Has Many#
Use the has_many association to declare that the parent has zero or more
children stored in a separate collection:
class Band < CouchbaseOrm::Base
has_many :members
end
The child model must use belongs_to to declare the
association with the parent:
class Member < CouchbaseOrm::Base
belongs_to :band
end
The child documents contain references to their respective parents:
band = Band.create!(members: [Member.new])
# => #<Band _id: 6001166d4896684910b8d1c5, >
band.members
# => [#<Member _id: 6001166d4896684910b8d1c6, band_id: '6001166d4896684910b8d1c5'>]
Use validations to require that at least one child is present:
class Band < CouchbaseOrm::Base
has_many :members
validates_presence_of :members
end
Belongs To#
Use the belongs_to macro to associate a child with a parent stored in a
separate collection. The _id of the parent (if a parent is associated)
is stored in the child.
class Studio < CouchbaseOrm::Base
belongs_to :band
end
studio = Studio.create!
# => #<Studio _id: 600118184896684987aa884f, band_id: nil>
Although has_many associations require the
corresponding belongs_to association to be defined on the child,
belongs_to may also be used has_many macro.
In this case the child is not accessible from the parent
but the parent is accessible from the child:
class Band < CouchbaseOrm::Base
end
class Studio < CouchbaseOrm::Base
belongs_to :band
end
Has And Belongs To Many#
Use the has_and_belongs_to_many macro to declare a many-to-many
association:
class Band < CouchbaseOrm::Base
has_and_belongs_to_many :tags
end
class Tag < CouchbaseOrm::Base
has_and_belongs_to_many :bands
end
Both model instances store a list of ids of the associated models, if any:
band = Band.create!(tags: [Tag.create!])
# => #<Band _id: 60011d554896684b8b910a2a, tag_ids: ['60011d554896684b8b910a29']>
band.tags
# => [#<Tag _id: 60011d554896684b8b910a29, band_ids: ['60011d554896684b8b910a2a']>]
Custom Association Names#
You can name your associations whatever you like, but if the class cannot be inferred by CouchbaseOrm from the name, and neither can the opposite side you’ll want to provide the macro with some additional options to tell CouchbaseOrm how to hook them up.
class Car < CouchabseOrm::Base
belongs_to :engine, class_name: "Motor"
end
class Motor < CouchabseOrm::Base
has_many :machine, class_name: "Car"
end
Custom Foreign Keys#
The attributes used when looking up associations can be explicitly specified.
The default is to use id on the “parent” association and #{association_name}_id
on the “child” association, for example with a has_many/belongs_to:
class Company < CouchbaseOrm::Base
has_many :emails
end
class Email < CouchbaseOrm::Base
belongs_to :company
end
company = Company.find(id)
# looks up emails where emails.company_id == company.id
company.emails
Specify a different foreign_key to change the attribute name on the “child”
association:
class Company < CouchbaseOrm::Base
attribute :c, type: String
has_many :emails, foreign_key: 'c_ref'
end
class Email < CouchbaseOrm::Base
# This definition of c_ref is automatically generated by CouchbaseOrm:
# attribute :c_ref, type: Object
# But the type can also be specified:
attribute :c_ref, type: String
belongs_to :company, foreign_key: 'c_ref'
end
company = Company.find(id)
# looks up emails where emails.c_ref == company.c
company.emails
Polymorphism#
has_and_belongs_to_many associations support polymorphism, which is
having a single association potentially contain objects of different classes.
For example, we could model an organization in which departments and teams
have managers as follows:
class Department < CouchbaseOrm::Base
has_and_belongs_to_many :unit, class_name "Manager"
end
class Team < CouchbaseOrm::Base
has_and_belongs_to_many :unit, class_name "Manager"
end
class Manager < CouchbaseOrm::Base
belongs_to :unit, polymorphic: true
end
dept = Department.create!
team = Team.create!
alice = Manager.create!(unit: dept)
alice.unit == dept
# => true
dept.manager == alice
# => true
Dependent Behavior#
You can provide dependent options to referenced associations to instruct CouchbaseOrm how to handle situations where one side of the association is deleted, or is attempted to be deleted. The options are as follows:
:destroy: Destroy the child document(s) and run all of the model callbacks.
If no :dependent option is provided, deleting the parent document leaves the child document unmodified
(in other words, the child document continues to reference the now deleted parent document via the foreign key attribute).
The child may become orphaned if it is ordinarily only referenced via the parent.
class Band < CouchbaseOrm::Base
has_many :albums, dependent: :destroy
belongs_to :label
end
class Album < CouchbaseOrm::Base
belongs_to :band
end
class Label < CouchbaseOrm::Base
has_many :bands
end
Band.first.destroy # Will delete all associated albums.