Alexey Zakharov
Alexey Zakharov

Reputation: 25102

Mongoid many to many problem

I want to store blog and its tags as separate documents.

Blog post should have tag_ids field and tag shouldn't have blog_posts_ids field.

Mongoid provides many to many relations out of the box, but it requires both documents of many to many relation to have _ids field.

class BlogPost
  include Mongoid::Document
  field :title
  references_many :tags, :stored_as => :array, :inverse_of => :blog_posts
end

class Tag
  include Mongoid::Document
  field :name
  # I DON'T WANT TO STORE BLOG_POSTS_IDS IN TAG DOCUMENT
  references_many :blog_posts, :stored_as => :array, :inverse_of => :tags
end

Upvotes: 2

Views: 1037

Answers (2)

bowsersenior
bowsersenior

Reputation: 12574

You can get around it with a method on Tag to fake the other side of the association

class BlogPost
  include Mongoid::Document
  field :title
  references_many :tags, :stored_as => :array, :inverse_of => :blog_posts
end

class Tag
  include Mongoid::Document
  field :name

  def blog_posts
    # will match all BlogPost records where the tag_ids array contains self.id
    BlogPost.where(:tag_ids => self.id)
  end
end

Obviously, this isn't as full-featured as :references_many , but you can similarly fake other aspects of the many-to-many relation. For example if you want the ability to assign a new blog_post to a tag you can add a simple create_blog_post method to Tag.

For many real-world situations, this kind of approach is practical as long as you keep the methods simple and don't get carried away.

Upvotes: 4

Aris Bartee
Aris Bartee

Reputation: 728

Map Reduce may be Your answer. Look in to MongoDB map reduce and use the permanent collection output for the tags

class BlogPost
  include Mongoid::Document
  field :title
  field :tags, :type => Array
  references_many :tags, :stored_as => :array, :inverse_of => :blog_posts
end

map =<<JS
  function(){
    if(!this.tags){
      return;
    }
    for(index in this.tags){
      emit(this.tags[index],{count:1,posts:[this.post.id]})
    }
  }
JS

reduce =<<JS
  function(key,values){
    var tagging = {count:0,posts:new Array()}
    values.forEach ( function(val) { 
      tagging.count++;
      tagging.posts.push(val.posts[0])
    });
    return tagging;
  }
JS

BlogPost.collection.map_reduce(map,reduce,{:out => 'tags',:keeptemp => true})

The result collection is always {id,values} where {:id => TAGNAME, :value => {:count => NUMBER_OF_TIMES_TAG_IS_USED,:posts => [ARRAY_OF_BLOG_ARTICLES]}} You can create a formatl Tag class or use:

Mongoid.master.collection("tags")

http://api.mongodb.org/ruby/1.1.2/Mongo/Collection.html#map_reduce-instance_methodt

Upvotes: 2

Related Questions