Globally unique values on embedded Mongoid documents

Mongoid is an excellent ORM for using MongoDB. Its very easy to use as a replacement for ActiveRecord in Rails as it uses ActiveModel inside and offers a lot of the same functionality as ActiveRecord.

MongoDB encourages you to embed documents for contains type of relations rather than creating relations between different collections. If you want to know more about embedding versus linking I encourage you to read the MongoDB documentation on schema design.

When you use Mongoid and add uniqueness validations on an embedded document you’ll soon discover that these validations only apply to the scope of the parent document (as described in the validations documentation). In many cases that’s exactly what you want, however there may be scenarios where you want a field in an embedded document to be unique across the entire collection. In this article I’ll show you how that can be done.

Lets get started with an example:

class Order
  include Mongoid::Document
  embeds_many :order_lines
end

class OrderLine
  include Mongoid::Document
  embedded_in :order
  field :article, type: String
  field :reference, type: String
end

The example is an order that has many order lines, where each order line has an article and a reference. Let’s say for the purpose of example that we want the reference on the order line to be unique across the entire order collection. The first thing to do to ensure the integrity of your collection is to add a unique index on the collection, you can define one on the Order model like so:

class Order
  include Mongoid::Document
  embeds_many :order_lines
  index "order_lines.reference", unique: true
end

Note that you need to create the indexes in order to enforce them (see the Mongoid documentation on indexes).

While the index ensures the integrity of your database you you may get some unexpected behavior:

Loading development environment (Rails 3.2.2)
[1] pry(main)> order = Order.create!(order_lines: [{article: "Tea", reference: "ref1"}])
=> #<Order _id: 4f5c7ae7d9fb2405e5000001, _type: nil>
[2] pry(main)> Order.count
=> 1
[3] pry(main)> order = Order.create!(order_lines: [{article: "Coffee", reference: "ref1"}])
=> #<Order _id: 4f5c7af8d9fb2405e5000003, _type: nil>
[4] pry(main)> Order.count
=> 1
[5] pry(main)>

The index protects the integrity of your collection, but it doesn’t tell you that it fails to save. You can force Mongoid to immediately save to the collection like so:

[5] pry(main)> order = Order.safely.create(order_lines: [{article: "Coffee", reference: "ref1"}])
Mongo::OperationFailure: 11000: E11000 duplicate key error index: orders_development.orders.$order_lines.reference_1  dup key: { : "ref1" }
from /home/mkremer/local/ruby-1.9.3-p125/lib/ruby/gems/1.9.1/gems/mongo-1.6.0/lib/mongo/networking.rb:95:in `send_message_with_safe_check'

While that works it isn’t performance or user friendly, ideally you’d want to use validations so you can define a friendlier error message and can use the standard save mechanism instead of forcing a direct write. This is actually pretty easy:

class OrderLine
  include Mongoid::Document
  embedded_in :order
  field :article, type: String
  field :reference, type: String

  validate do |order_line|
    order_line.errors.add :reference, 'must be unique' if Order.where(:id.ne => order_line.order.id, "order_lines.reference" => order_line.reference).count > 0
  end
end

And that behaves pretty much how you’d expect:

Loading development environment (Rails 3.2.2)
[1] pry(main)> order = Order.new(order_lines: [{article: "Coffee", reference: "ref1"}])
=> #<Order _id: 4f5c7f2cd9fb24061f000001, _type: nil>
[2] pry(main)> order.valid?
=> false
[3] pry(main)> order.errors.messages
=> {:order_lines=>["is invalid"]}
[4] pry(main)> order.order_lines[0].valid?
=> false
[5] pry(main)> order.order_lines[0].errors.messages
=> {:reference=>["must be unique"]}
[6] pry(main)> order.order_lines[0].reference = "ref2"
=> "ref2"
[7] pry(main)> order.valid?
=> true
[8] pry(main)> order.order_lines[0].valid?
=> true
[9] pry(main)> order.save!
=> true
[10] pry(main)> Order.count
=> 2
[11] pry(main)>

That’s really all there is to it 🙂

Advertisements
Advertisements
%d bloggers like this: