In Rails 3 I'm trying to model a user content system, where the user can post different types of content, for example, note, photo, url etc.
From a Java/C# OO perspective, I would use a polymorphic relationship between the User and an interface representing a Content item, e.g. something called IUserContent.
I'm struggling to find an example that works how I expect it to, here's what I tried first, in short I'm getting confused about the implementation of polymorphic associations in ActiveRecord.
# user.rb model - includes...
has_many :notes, :as => :postable, :dependent => :destroy, :inverse_of => :postable
has_many :urls, :as => :postable, :dependent => :destroy, :inverse_of => :postable
has_many :photos, :as => :postable, :dependent => :destroy, :inverse_of => :postable
# url.rb ...
belongs_to :postable, :polymorphic => true, :inverse_of => :urls
# photo.rb
belongs_to :postable, :polymorphic => true, :inverse_of => :photos
# note.rb
belongs_to :postable, :polymorphic => true, :inverse_of => :notes
I'm still just following examples I've found, and frankly this feels like User is the polymorphic target, not the content.
I think I want something like this...
# within user.rb
has_many :postable, :as => :postable, dependent => :destroy, :inverse_of => :users
# photo.rb
# url.rb
# note.rb
# all have the following...
belongs_to :user, :polymorphic => true, :inverse_of => :postable
... looking for a few pointers in the right direction.
Thank you.
The only way you can do that is if all of these classes inherit from the same base class, as in:
class User < ActiveRecord::Base
has_many :postable, :as => :postable, :dependent => :destroy, :class_name => 'Postable'
end
class Postable < ActiveRecord::Base
belongs_to :user, :polymorphic => true
end
class Photo < Postable
end
class Url < Postable
end
class Note < Postable
end
So, you have to use the ActiveRecord single-table inheritance to model such a relationship.
Related
So I'm trying to do a relatively advanced query on a polymorphic model. I have the following models:
class Project < ActiveRecord::Base
has_many :project_stakeholders, :dependent => :destroy
has_many :features, :dependent => :destroy
has_many :iterations, :dependent => :destroy
has_many :comments, :as => :commentable, :dependent => :destroy
end
class ProjectStakeholder < ActiveRecord::Base
belongs_to :project
has_many :comments, :as => :commentable, :dependent => :destroy
end
class Feature < ActiveRecord::Base
belongs_to :project
has_many :comments, :as => :commentable, :dependent => :destroy
end
class Iteration < ActiveRecord::Base
belongs_to :project
has_many :comments, :as => :commentable, :dependent => :destroy
end
class User < ActiveRecord::Base
has_many :comments, :dependent => :destroy
end
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
belongs_to :created_by, :class_name => 'User'
end
I'm trying to find the current_user's comments for a particular project (project.comments, project.features.comments, project.iterations.comments, project.project_stakeholder.comments) and sort them in created_at descending order.
The best I have come up with is:
Class Project < ActiveRecord::Base
def all_comments_for_user(user)
Comment.where(:created_by_id => user.id).select { |c| c.commentable.attributes.has_key?('project_id') }.select { |c| c.commentable.project == self } | comments.where(:created_by_id => user)
end
end
But this doesn't address the descending create_at sequence.
Class Project < ActiveRecord::Base
def all_comments_for_user(user)
Comment.where(:created_by_id => user.id).order('created_at DESC').select { |c| c.commentable.attributes.has_key?('project_id') }.select { |c| c.commentable.project == self } | comments.where(:created_by_id => user)
end
end
Does this work ? I just added order('created_at DESC')
When you have a relation such as embeds_many :album_items which relates to the AlbumItem model. How can I have it stored in just items. I tried embeds_many :album_items, :as => :items and embeds_many :items, :class_name => AlbumItem. Neither worked.
How can I go about renaming the relation?
Thanks
Does this work(assuming your parent model name is Album)?
In Album:
embeds_many :items, :class_name => "AlbumItem", :inverse_of => :album
and in AlbumItem:
embedded_in :album, :class_name => "Album", :inverse_of => :items
The setup
I have a data model with 3 major tables (users, links, topics) with 2 join tables (link_saves and link_topics). My models:
User
has_many :link_saves, :class_name => 'LinkSave', :foreign_key => 'user_id'
has_many :links, :through => :link_saves
LinkSave
belongs_to :user
belongs_to :link
Link
has_many :link_saves, :class_name => 'LinkSave', :foreign_key => 'link_id'
has_many :users, :through => :link_saves
has_many :link_topics, :inverse_of => :link
has_many :topics, :through => :link_topics
LinkTopic
belongs_to :link
belongs_to :topic
Topic
has_many :link_topics
has_many :links, :through => :link_topics
The Question
I want to be able to find a list of all the topics a user has saved links for. I would like to be able to do #user.topics and have it hop across all 5 tables from user all the way to topics. More importantly, I want this to return an ActiveRecord relation so that I can scope/sort/page the list of user topics further so this would NOT work:
## app/models/user.rb
def topics
links.collect(&:topics)
end
Am I going down the wrong path? Is there a way to do this through active record without having to write all the custom SQL? Help please!
Possible Answers (Update)
Using multiple has_many :throughs to make all the hops. This works, but can't be best practice, right?
## app/models/user.rb
has_many :link_saves, :class_name => 'LinkSave', :foreign_key => 'user_id'
has_many :links, :through => :link_saves
has_many :link_topics, :through => :links, :uniq => true
has_many :topics, :through => :link_topics, :uniq => true
I think this is called a 'nested' has_many through, basically going from A to B to C.
In Rails 3.1 this functionality is now supported
http://www.warmroom.com/yesterdays/2011/08/30/rails-3-1-nested-associations/
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html (search for 'Nested')
The example they have is a bit simpler than what you have, but I think it should be enough for you to get some ideas.
class Author < ActiveRecord::Base
has_many :posts
has_many :comments, :through => :posts
has_many :commenters, :through => :comments
end
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :commenter
end
#author = Author.first
#author.commenters # => People who commented on posts written by the author
Prior to Rails 3.1 there was a plugin
'https://github.com/releod/nested_has_many_through'
Ok so here goes. I don't know if I'm over complicating things or if I'm just still so new to Rails that I don't understand the basics. What I want in sudo code is this:
User
has_many projects as owner through relationship
has_many projects as contributor through relationship
has_many projects as follower through relationship
Project
has_one user as owner through relationship
has_many users as followers through relationship
has_many users as contributors through relationship
Relationship
belongs_to user
belongs_to project
Then I'm wanting to have the following magical things:
owner = Project.owner
followers = Project.followers
contributors = Project.contributors
projects = User.projects
myprojects = User.projects... (now I'm really not sure)
followedProjects = ...
contributingProjects = ...
So in writing that down I can see that there is another gap in my understanding of this model. The users can have the role of owner, follower or contributor or any combination of all three.
In terms of real code I have added here what I think is the relevant parts:
class User < ActiveRecord::Base
has_many :user_project_relationships, :as => :relateable, :class_name => "UserProjectRelationship"
has_many :projects, :as => :owner, :through => :relateable, :class_name => "Project", :source_type => :owner
has_many :projects, :as => :follower, :through => :relateable, :class_name => "Project", :source_type => :follower
has_many :projects, :as => :contributor, :through => :relateable, :class_name => "Project", :source_type => :contributor
end
class Project < ActiveRecord::Base
has_many :user_project_relationships, :as => :relateable, :class_name => "UserProjectRelationship"
has_one :user, :as => :owner, :through => :relateable, :class_name => "User"
has_many :users, :as => :followers, :through => :relateable, :source_type => :follower, :class_name => "User"
has_many :users, :as => :contributors, :through => :relateable, :source_type => :contributor, :class_name => "User"
end
class UserProjectRelationship < ActiveRecord::Base
belongs_to :user
belongs_to :project, :polymorphic => true
end
The migration for the relationships model is:
class CreateUserProjectRelationships < ActiveRecord::Migration
def self.up
create_table :user_project_relationships do |t|
t.integer :relateable_id
t.string :relateable_type
t.integer :project_id
t.timestamps
end
add_index :user_project_relationships, [:relateable_id, :relateable_type], :name => :relateable
add_index :user_project_relationships, :project_id
end
def self.down
drop_table :user_project_relationships
end
end
Currently I get errors for things like project.users ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association :relateable in model Project
I feel like I'm too in the wilderness here to really get what I want, and maybe relying on magical rails to do more than it does. Any guidance on the best path would be greatly appreciated.
Thanks in advance
Steve
Rails can do alot, but I think instead you're trying to make the Relationship model do too much. Each of those are a different kind of relationship, so I think try to keep them so.
Split that up into separate join models:
class User < ActiveRecord::Base
has_many :owned_projects, :class_name => "Project", :foreign_key => :owner_id
has_many :projects_followers
has_many :followed_projects, :class_name => "Project", :through => :projects_followers
has_many :projects_contributors
has_many :contributed_projects, :class_name => "Project", :through => :projects_contributors
end
class Project < ActiveRecord::Base
belongs_to :owner
has_many :projects_followers
has_many :followers, :class_name => "User", :through => :projects_followers
has_many :projects_contributors, :foreign_key => :contributor_id
has_many :contributors, :class_name => "User", :through => :projects_contributors
end
class ProjectsFollowers < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
class ProjectsContributors < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
Should be a lot closer to what you want. You can then indeed do
project.owner
project.followers
project.contributors
and
user.owned_projects
user.followed_projects
user.contributed_projects
That should either work, or get you pretty close.
I think your mixup came from trying to make a polymorphic relationship, which I don't think is desireable here. AFAI grok, the use case for polymorphic relationships is when you want 1 Model to be related to Any number of other models in the same way. That's not the case here, as you have 2 models with 3 different types of relationships between them.
I have these VERY similar classes:
class DeliveryDocument < CommercialDocument
# Relations
belongs_to :biller, :class_name => 'Company'
belongs_to :customer, :class_name => 'Company'
belongs_to :customer_center, :class_name => 'Center'
has_many :delivery_document_lines, :dependent => :destroy
alias_attribute :lines, :delivery_document_lines
# Some configuration
accepts_nested_attributes_for :delivery_document_lines
acts_as_freezable :only_dependencies => true,
:has_many => [:delivery_document_lines],
:belongs_to => [:biller, :customer, :customer_center]
acts_as_clonable :has_many => [:delivery_document_lines]
validates_each :lines do |record, attr, value|
# ...
end
end
And:
class InvoiceDocument < CommercialDocument
self.
# Relations
belongs_to :biller, :class_name => 'Company'
belongs_to :customer, :class_name => 'Company'
belongs_to :customer_center, :class_name => 'Center'
has_many :invoice_document_lines, :dependent => :destroy
alias_attribute :lines, :invoice_document_lines
# Some configuration
accepts_nested_attributes_for :invoice_document_lines
acts_as_freezable :only_dependencies => true,
:has_many => [:invoice_document_lines],
:belongs_to => [:biller, :customer, :customer_center]
acts_as_clonable :has_many => [:invoice_document_lines]
# Validations
validates_each :lines do |record, attr, value|
# ...
end
end
I also have some methods I didn't paste that could be extracted to the parent. I only need to know the class name in the parent. When I do this:
class CommercialDocument < Document # document inherits from AR::Base
# ...
has_many :"#{self.to_s.underscore}_lines", :dependent => :destroy
# ...
accepts_nested_attributes_for :"#{self.to_s.underscore}_lines"
# ...
end
it doesn't work, because self.to_s is CommercialDocument.
How would you refactor this behavior in parent class?
I could put things in a module and import it, but then the whole hierarchy of documents becomes almost useless.
I already have the documents' hierarchy, so if I can, I want to use it if there is a way.
You could try using Class.inherited