views:

150

answers:

2

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.

+1  A: 

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".

TTT
Thank you. This is the approach that I took. I am posting how I implemented this below.
Sean McCleary
A: 

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
Sean McCleary