Reputation: 8348
I have two entities, projects and users. These are modeled in Rails using Mongoid with two Document instances, User and Project.
In this system, one user can create one project, but many users can follow many projects. For example, as user_id 1 I've created project_id 1. But user_ids 10, 11, 40, and 60 all follow project_id 1. I need to represent a many-to-many relationship between users and projects, and represent a specific user_id as the creator of the project, to assign him editing rights.
Practically speaking, when a user logs-in, he needs to be able to see all projects that he is following, including any that he created, commingled with other projects created by other users. His special creator status wont influence this list at all. When a user looks at a specific project, he needs to be able to view all users following a project, and if he's a creator, he can add new followers and delete existing ones.
In a RDBMS I would represents this with tables users
, projects
and a users_projects
join table with a flag of is_creator
. This would easily let me select which projects a user can see, and which users are followers, including which users are creators, of projects.
Mongoid supports many-to-many relationships, but unlike in an RDBMS there's no way for me to put a flag on the relationship. Instead, I'm thinking I'll add a creator
field to the projects
document, which will contain a link back to an _id
field on the users document.
The user->projects relationship might look like this
class User
has_and_belongs_to_many :projects
end
class Project
has_and_belongs_to_many: users
end
But I can't figure out how to map the creator->created_projects relationship. I believe I can reference a user creator in Project
like belongs_to :creator, :class_name => 'User'
but I'm not sure how to set up the other side.
How best can I model these relationships in Mongoid?
Upvotes: 1
Views: 295
Reputation: 14625
the second version uses less space but you need an extra query to get the user details like usernames. An object ID has 12bytes, max. document size is 16mb so the array could hold around 1.3M user ids (theoretically!)..
here you go:
user.rb
class User
# projects this user owns
has_many :projects
has_many :followed_projects,
:class_name => 'Project',
:foreign_key => :follower_ids
# Uncomment if the relation does not work
#def followed_projects
# Project.where(:follower_ids => self.id)
#end
# get projects that this user has created and projects he is following
def related_projects
Project.any_of({:user_id => self.id}, {:follower_ids => self.id})
end
end
project.rb
class Project
# creator
belongs_to :user
field :follower_ids, :type => Array
# adds a follower
def add_follower!(user_obj)
# adds a user uniquely to the follower_ids array
self.add_to_set(:follower_ids, user_obj.id)
end
def remove_follower!(user_obj)
# remove the user
self.pull(:follower_ids, user_obj.id)
end
end
How to work with it:
@project = Project.first
@some_user = User.last
@project.add_follower!(@some_user)
@some_user.followed_projects
@some_user.related_projects
# create hash like @ids_to_user[user_id] = user
@ids_to_users = User.find(@project.follower_ids).inject({}) {|hsh, c_user| hsh[c_user.id] = c_user; hsh}
@project.followers.each do |c_follower|
puts "I'm #{@ids_to_users[c_follower].username} and I'm following this project!"
end
@project.remove_follower!(@some_user)
Upvotes: 2
Reputation: 14625
create an embedded document which holds all followers with their user_id and their username so you won't have to query the follower's usernames.
The benefits:
The downside:
If a user changes his name, you'll have to update all his "followships" but how often do you change your name compared to how often you lookup your followed projects ;)
If you have many thousand followers per project you may reach the document limit of 16mb
user.rb
class User
# projects this user owns
has_many :projects
def followed_projects
Project.where('followers.user_id' => self.id)
end
# get projects that this user has created and projects he is following
def related_projects
Project.any_of({:user_id => self.id}, {'followers.user_id' => self.id})
end
end
project.rb
class Project
# creator
belongs_to :user
embeds_many :followers
# add an index because we're going to query on this
index 'followers.user_id'
# adds a follower
# maybe you want to add some validations, preventing duplicate followers
def add_follower!(user_obj)
self.followers.create({
:user => user_obj,
:username => user_obj.username
})
end
def remove_follower!(user_obj)
self.followers.destroy_all(:conditions => {:user_id => user_obj.id})
end
end
follower.rb
class Follower
include Mongoid::Document
include Mongoid::Timestamps
embedded_in :project
# reference to the real user
belongs_to :user
# cache the username
field :username, :type => String
end
How to work with it:
@project = Project.first
@some_user = User.last
@project.add_follower!(@some_user)
@some_user.followed_projects
@some_user.related_projects
@project.followers.each do |c_follower|
puts "I'm #{c_follower.username} and I'm following this project!"
end
@all_follower_user_ids = @project.followers.map{|c| c.user_id}
# find a specific follower by user_id
@some_follower = @project.followers.where(:user_id => 1234)
# find a specific follower by username
@some_follower = @project.followers.where(:username => 'The Dude')
@project.remove_follower!(@some_user)
PS: If you want a simpler solution, you could just embedd an array of ObjectIDs (user_ids) in the project and use the atomic updates $addToSet
and $pullAll
to add/remove a follower. But you'd need an extra query like User.where(:user_id.in => @project.follower_ids)
(assuming the array is called follower_ids
) to grab all users and their names ;)
Upvotes: 1