Reputation: 3820
I am just starting a new Rails 3 project using Mongoid ORM for MongoDB. There is just one thing I can not get my head around, and that is how to effectively have a many-to-many relationship. Now there is a good chance that I may be approaching this problem wrong, but as far as I know, there is at least two containers in my project that need a many-to-many relationship. I would prefer to treat both models as "first class" models and allocate each with its own container.
This is the simplest way I can think to structure my many-to-many relationship:
// Javascript pseudo modeling
// -------------------- Apps
{
app: {
_id: "app1",
name: "A",
event_ids: ["event1","event2"]
}
}
{
app: {
_id: "app2",
name: "B",
event_ids: ["event1"]
}
}
// -------------------- Events
{
event: {
_id: "event1",
name: "Event 1",
}
}
{
event: {
_id: "event2",
name: "Event 2",
}
}
As far as I can tell this is the minimum amount of information need to infer a many-to-many relationship. My assumption is that I might have to have a map reduce procedure to determine what apps belong to an event. I would also have to write post commit/save hooks on Event to update App.event_ids if an app is added to or removed from an event model.
Am I on the right track here? If someone has any Mongoid or Mongomapper code examples of a many-to-many relationship working, could you please share.
Upvotes: 3
Views: 1201
Reputation: 3820
I was able to implement this design using Mongoid. I wrote extensive tests and I was able to get my solution working; however, I am not satisfied with my implementation. I believe that my implementation would be a difficult to maintain.
I'm posting my non-elegant solution here. Hopefully, this will help someone with the start of a better implementation.
class App
include Mongoid::Document
field :name
references_one :account
references_many :events, :stored_as => :array, :inverse_of => :apps
validates_presence_of :name
end
class Event
include Mongoid::Document
field :name, :type => String
references_one :account
validates_presence_of :name, :account
before_destroy :remove_app_associations
def apps
App.where(:event_ids => self.id).to_a
end
def apps= app_array
unless app_array.kind_of?(Array)
app_array = [app_array]
end
# disassociate existing apps that are not included in app_array
disassociate_apps App.where(:event_ids => self.id).excludes(:id => app_array.map(&:id)).to_a
# find existing app relationship ids
existing_relationship_ids = App.where(:event_ids => self.id, :only => [:id]).map(&:id)
# filter out existing relationship ids before making the new relationship to app
push_apps app_array.reject { |app| existing_relationship_ids.include?(app.id) }
end
def push_app app
unless app.event_ids.include?(self.id)
app.event_ids << self.id
app.save!
end
end
def disassociate_app app
if app.event_ids.include?(self.id)
app.event_ids -= [self.id]
app.save!
end
end
def push_apps app_array
app_array.each { |app| push_app(app) }
end
def disassociate_apps app_array
app_array.each { |app| disassociate_app(app) }
end
def remove_app_associations
disassociate_apps apps
end
end
Upvotes: 1
Reputation: 2375
Your structure can work and you don't need a mapreduce function to determine what apps belong to an event. You can query the app collection on an eventid. You can index field collection.event_ids.
If you don't want to search apps on an eventid but on a event name, you will need to add that event name to the app collection (denormalization). That means that you also have to update the app collection when the name of an event changes. I don't know if that happens very often?
You often have to denormalize when you use MongoDB, so you don't store the minimal amount of information but you store some things "twice".
Upvotes: 1