My question is more related to naming conventions than programmation I guess.
Let's assume an application where users can create new articles (so they are the owner of these articles) and where you can add article "editors", who can only update the article content.
class User
include Mongoid::Document
has_many :articles # as owner of the articles
has_and_belongs_to_many :articles # as editor of the articles
end
class Article
include Mongoid::Document
belongs_to :user
has_and_belongs_to_many :editors, :class_name => 'User'
end
What I want to know is how I should call the articles association in my User model. I mean, an article has an author and editors, which seems strong naming conventions to me, but a user has articles he created and articles he is the editor. How would you call/name/declare the last 2 associations?
I would call them as :edited_articles, and :authored_articles or :owned_articles, or something similarly straightforward names. Just dont forget to add the :class_name and :foreign_key or :through qualifiers to them.
Update:
For has_and_belongs_to_many relation you need a connection table, which is by default, is named of the two joined table. E.g. articles_users in your case. In this table you will propably have two ids, user_id and article_id. This way rails connects your models automatically.
has_and_belongs_to_many :editors, :class_name => 'User', :foreign_id => 'user_id'
Of course if you call it editor_id in the join table then use that. And the opposite, on the user side should work too.
Related
I have a comment system with two tables: comments, and users. On the comment I want to record who the author was and also I want to notify any user that is mentioned in the comment with (#username). So I'm thinking I need to have an author_id on the comment, and also a comments_users table with the comment id and all the users ids that were mentioned. Would this be a correct way to accomplish it?:
User:
has_many :comments
Comment:
belongs_to :users, class_name: 'User', foreign_key: 'author_id'
has_many :users
The associations could be set up thus:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :comments
has_and_belongs_to_many :mentions, join_table: "comments_users", association_foriegn_key: "comment_id"
end
Class Comment < ActiveRecord::Base
belongs_to :author, class_name: "User", foreign_key: "author_id"
has_and_belongs_to_many :mentions, join_table: "comments_users", foreign_key: "comment_id"
end
#comments_users
comment_id | user_id
This will allow you to call:
#user.comments #-> shows comments user has authored
#user.mentions.first.comment #-> shows first comment user was mentioned in
#comment.author #-> shows user who authored comment
#comment.mentions.first.user #-> shows first user who was mentioned in comment
Update
HABTM still needs a table (Rails migration for has_and_belongs_to_many join table), but the difference is that it doesn't need a primary key column (just comment_id | user_id)
We've created a "self-referential" habtm relationship, meaning you don't need to "create" any records -- they should all be created already. The HABTM will just reference them. As such, you'll need to use the << ActiveRecord method to add records into your mentions collection:
#app/controllers/comments_controller.rb
Class CommentsController < ActiveRecord::Base
def create
#comment = Comment.new(comments_params)
#comment.save_with_mentions
end
end
#app/models/comment.rb
Class Comment < ActiveRecord::Base
def save_with_mentions
comment = self.save
#do something to find mentioned users
mentioned_users = User.where("user_id = 1") #example
for user in mentioned_users do
comment.mentions << user
end
end
end
There are always many ways to accomplish any given task, but I'm guessing you're looking for something like this for your models & associations.
User:
has_many :comments
The user model association looked right.
Comment:
belongs_to :author, class_name: 'User', foreign_key: 'user_id'
has_many :users
Note, the belongs_to should reference a model in singular-naming style (ie: user vs users). I think you're going to want to do a reference like comment.author to find the author of your comments. It is more typical to provide a foreign_key of user_id when referring to a User model to keep things clear, but then provide a clarifying association name like "author" or "creator" or whatever for reference as I showed above. So your Comments table would have a foreign_key of user_id to reference back to the Users table. This user would be referenced in Rails by the name "author".
The second part of your question that has to do with tracking other user references in your model sounds like a one-to-many from the comment-users table. So, that sounds like one option. Similar to your "author" comment, you may want to provide a clearer name like "tags" which can just be references to users.
Another good option for this feature may be to set up a polymorphic table (essentially a flexible join table) if you plan to use this principle elsewhere in your app (like for referencing/tagging people in other elements like a photo or posting or something). It could provide greater flexibility for adding features and tracking these user references. A polymorphic table could have any name, but usually has an "-able" type name - like "taggable". Here's a useful reference: http://guides.rubyonrails.org/association_basics.html#polymorphic-associations
I have a has_many :through association setup between two tables (Post and Category). The reason I'm using has_many :through instead of HABTM is that I want to do some validation on the join table (PostCategory).
So I have 4 models in use here:
User:
has_many :posts
has_many :categories
Post:
belongs_to :user
has_many :post_categories
has_many :categories, :through => :post_categories
Category:
belongs_to :user
has_many :post_categories
has_many :posts, :through => :post_categories
PostCategory:
belongs_to :post
belongs_to :category
Basically what I want is: Users can create posts, users can also create their own categories. A user can then categorize posts (not just their posts, any posts). A post can be categorized by many different users (in different ways potentially), and a category could contain many different posts (A user could categorize N posts under a specific category of theirs).
Here's where it gets a little bit tricky for me (I'm a Rails noob).
A post can ONLY belong to ONE category for a given user. That is, a post CANNOT belong to more than ONE category for any user.
What I want to be able to do is create a validation for this. I haven't been able to figure out how.
I've tried things like (inside PostCategory)
validates_uniqueness_of :post_id, :scope => :category_id
But I realize this isn't correct. This would just make sure that a post belongs to 1 category, which means that after one user categorizes the post, no other user could.
Really what I'm looking for is how to validate this in my PostCategory model (or anywhere else for that matter). I'm also not against changing my db schema if that would make things easier (I just felt that this schema was pretty straight forward).
Any ideas?
The simpliest way is to add user_id to PostCategory and to validate uniqueness of post_id with user_id scope.
Another way is to create custom validation which checks using sql if category owner has added category to that post.
Option 1 : use a before_save. In it, do a SQL look up to make sure a post with a similar category for your user doesn't exist (take care that on edit, you'll have to make sure you don't look-up for the current Post that is already in the DB)
Option 2 : custom validators :
http://guides.rubyonrails.org/v3.2.13/active_record_validations_callbacks.html#custom-validators
Never used them, but sounds like it can do what you want
I'm starting out with the whole (wonderful) idea of database associations in Rails but I'm having problems because I'm working with an existing database that does not conform to Rails standards and cannot figure out how to name the associations. There are a couple of similar posts, but I can't wrap my head around the naming for my particular situation which is as follows:
table book with book.formatId looks up values in book_format.id
So foreign key book.formatId
My models are named: Book and BookFormat (I read that you use camelCase when your tables are separated by underscore).
Under the Book model I have this:
has_one :bookFormat, :foreign_key => 'book_format.id' # not sure if this format table.field is correct or I have to use something else here. Also not sure about the bookFormat, should it be BookFormat or bookformat?
The BookFormat model has this:
belongs_to :book
But when I try to do
book = Book.first
book.bookFormat.empty?
I get an error of method not found for bookFormat. So obviously something's wrong, but I can't figure out where.
A second part of the question is the use of many to many relationships. Example:
Tables
book, book_subjects, book_subjects2title
book_subjects.id => book_subjects2title.pId
book.id => book_subjects2title.bookId
I'm reading the Beginning Rails 3 book from Apress (which is a great book) but it's not very clear on all this or I'm just not getting it.
Thanks.
Since the book stores the formatId on it, you should use belongs_to, and change the foreign key as such:
belongs_to :book_format, :class_name => 'BookFormat', :foreign_key => 'formatId'
For the table name, i did some quick searching, and found the following method: set_table_name
So you should be able to add it at the top of your model, like so:
class Book < ActiveRecord::Base
set_table_name 'book'
# the rest of book model code
end
class BookFormat < ActiveRecord::Base
set_table_name 'book_format'
# rest of book_format model code
end
Normally rails uses plural table names, so hence why you need to specify it there.
agmcleod put me on the right track, so here's the full answer in the hopes that this helps other people with similar problems:
I created the model with a different name for easier reading. So model Books will have:
class Books < ActiveRecord::Base
set_table_name 'bookpedia'
belongs_to :format, :foreign_key => 'formatId' # we need to specify the foreign key because it is not the rails default naming
has_many :author2title, :foreign_key => 'bookId'
has_many :author, :through => :author2title
end
model Format:
class Format < ActiveRecord::Base
set_table_name 'book_format'
has_many :books
end
Then an instance of the class will have the format method:
#book = Books.first
#book.format # Hardcover
For many to many relationships I'll paste the syntax here as well since that took me a while to figure out:
class Author < ActiveRecord::Base
set_table_name 'book_author'
has_many :author2title, :foreign_key => 'pId' # junction table
has_many :books, :through => :author2title # establishing the relationship through the junction table
end
This is the actual junction table:
class Author2title < ActiveRecord::Base
set_table_name 'book_author2title'
belongs_to :author, :foreign_key => 'pId' # foreign key needs to be here as well
belongs_to :books, :foreign_key => 'bookId'
end
The book model above has the necessary entries for this many to many relationship already in it.
If anyone needs clarification on this I'd be happy to oblige since I struggled for more than a day to figure this out so would be glad to be of help.
Cheers.
I have the following setup:
class Publication < ActiveRecord::Base
has_and_belongs_to_many :authors, :class_name=>'Person', :join_table => 'authors_publications'
has_and_belongs_to_many :editors, :class_name=>'Person', :join_table => 'editors_publications'
end
class Person < ActiveRecord::Base
has_and_belongs_to_many :publications
end
With this setup I can do stuff like Publication.first.authors. But if I want to list all publications in which a person is involved Person.first.publications, an error about a missing join table people_publications it thrown. How could I fix that?
Should I maybe switch to separate models for authors and editors? It would however introduce some redundancy to the database, since a person can be an author of one publication and an editor of another.
The other end of your associations should probably be called something like authored_publications and edited_publications with an extra read-only publications accessor that returns the union of the two.
Otherwise, you'll run in to sticky situations if you try to do stuff like
person.publications << Publication.new
because you'll never know whether the person was an author or an editor. Not that this couldn't be solved differently by slightly changing your object model.
There's also hacks you can do in ActiveRecord to change the SQL queries or change the behavior of the association, but maybe just keep it simple?
I believe you should have another association on person model
class Person < ActiveRecord::Base
# I'm assuming you're using this names for your foreign keys
has_and_belongs_to_many :author_publications, :foreign_key => :author_id
has_and_belongs_to_many :editor_publications, :foreign_key => :editor_id
end
See comments for updates.
I've been struggling to get a clear and straight-forward answer on this one, I'm hoping this time I'll get it! :D
I definitely have a lot to learn still with Rails, however I do understand the problem I'm facing and would really appreciate additional help.
I have a model called "Task".
I have an abstract model called "Target".
I would like to relate multiple instances of subclasses of Target to Task.
I am not using single table inheritance.
I would like to query the polymorphic relationship to return a mixed result set of subclasses of Target.
I would like to query individual instances of subclasses of Target to obtain tasks that they are in a relationship with.
So, I figure a polymorphic many to many relationship between Tasks and subclasses of Targets is in order.
In more detail, I will be able to do things like this in the console (and of course elsewhere):
task = Task.find(1)
task.targets
[...array of all the subclasses of Target here...]
But! Assuming models "Store", "Software", "Office", "Vehicle", which are all subclasses of "Target" exist, it would be nice to also traverse the relationship in the other direction:
store = Store.find(1)
store.tasks
[...array of all the Tasks this Store is related to...]
software = Software.find(18)
software.tasks
[...array of all the Tasks this Software is related to...]
The database tables implied by polymorphic relationships appears to be capable of doing this traversal, but I see some recurring themes in trying to find an answer which to me defeat the spirit of polymorphic relationships:
Using my example still, people appear to want to define Store, Software, Office, Vehicle in Task, which we can tell right away isn't a polymorphic relationship as it only returns one type of model.
Similar to the last point, people still want to define Store, Software, Office and Vehicle in Task in one way shape or form. The important bit here is that the relationship is blind to the subclassing. My polymorphs will initially only be interacted with as Targets, not as their individual subclass types. Defining each subclass in Task again starts to eat away at the purpose of the polymorphic relationship.
I see that a model for the join table might be in order, that seems somewhat correct to me except that it adds some complexity I assumed Rails would be willing to do away with. I plea inexperience on this one.
It seems to be a small hole in either rails functionality or the collective community knowledge. So hopefully stackoverflow can chronicle my search for the answer!
Thanks to everyone who help!
You can combine polymorphism and has_many :through to get a flexible mapping:
class Assignment < ActiveRecord::Base
belongs_to :task
belongs_to :target, :polymorphic => true
end
class Task < ActiveRecord::Base
has_many :targets, :through => :assignment
end
class Store < ActiveRecord::Base
has_many :tasks, :through => :assignment, :as => :target
end
class Vehicle < ActiveRecord::Base
has_many :tasks, :through => :assignment, :as => :target
end
...And so forth.
Although the answer proposed by by SFEley is great, there a some flaws:
The retrieval of tasks from target (Store/Vehicle) works, but the backwards wont. That is basically because you can't traverse a :through association to a polymorphic data type because the SQL can't tell what table it's in.
Every model with a :through association need a direct association with the intermediate table
The :through Assignment association should be in plural
The :as statement wont work together with :through, you need to specify it first with the direct association needed with the intermediate table
With that in mind, my simplest solution would be:
class Assignment < ActiveRecord::Base
belongs_to :task
belongs_to :target, :polymorphic => true
end
class Task < ActiveRecord::Base
has_many :assignments
# acts as the the 'has_many targets' needed
def targets
assignments.map {|x| x.target}
end
end
class Store < ActiveRecord::Base
has_many :assignments, as: :target
has_many :tasks, :through => :assignment
end
class Vehicle < ActiveRecord::Base
has_many :assignments, as: :target
has_many :tasks, :through => :assignment, :as => :target
end
References:
http://blog.hasmanythrough.com/2006/4/3/polymorphic-through
The has_many_polymorphs solution you mention isn't that bad.
class Task < ActiveRecord::Base
has_many_polymorphs :targets, :from => [:store, :software, :office, :vehicle]
end
Seems to do everything you want.
It provides the following methods:
to Task:
t = Task.first
t.targets # Mixed collection of all targets associated with task t
t.stores # Collection of stores associated with task t
t.softwares # same but for software
t.offices # same but for office
t.vehicles # same but for vehicles
to Software, Store, Office, Vehicle:
s = Software.first # works for any of the subtargets.
s.tasks # lists tasks associated with s
If I'm following the comments correctly, the only remaining problem is that you don't want to have to modify app/models/task.rb every time you create a new type of Subtarget. The Rails way seems to require you to modify two files to create a bidirectional association. has_many_polymorphs only requires you to change the Tasks file. Seems like a win to me. Or at least it would if you didn't have to edit the new Model file anyway.
There are a few ways around this, but they seem like way too much work to avoid changing one file every once in a while. But if you're that dead set against modifying Task yourself to add to the polymorphic relationship, here's my suggestion:
Keep a list of subtargets, I'm going to suggest in lib/subtargets formatted one entry per line that is essentially the table_name.underscore. (Capital letters have an underscore prefixed and then everything is made lowercase)
store
software
office
vehicle
Create config/initializers/subtargets.rb and fill it with this:
SubtargetList = File.open("#{RAILS_ROOT}/lib/subtargets").read.split.reject(&:match(/#/)).map(&:to_sym)
Next you're going to want to either create a custom generator or a new rake task. To generate your new subtarget and add the model name to the subtarget list file, defined above. You'll probably end up doing something bare bones that makes the change and passes the arguments to the standard generator.
Sorry, I don't really feel like walking you through that right now, but here are some resources
Finally replace the list in the has_many_polymorphs declaration with SubtargetList
class Task < ActiveRecord::Base
has_many_polymorphs :targets, :from => SubtargetList
end
From this point on you could add a new subtarget with
$ script/generate subtarget_model home
And this will automatically update your polymorphic list once you reload your console or restart the production server.
As I said it's a lot of work to automatically update the subtargets list. However, if you do go this route you can tweak the custom generator ensure all the required parts of the subtarget model are there when you generate it.
Using STI:
class Task < ActiveRecord::Base
end
class StoreTask < Task
belongs_to :store, :foreign_key => "target_id"
end
class VehicleTask < Task
belongs_to :vehicle, :foreign_key => "target_id"
end
class Store < ActiveRecord::Base
has_many :tasks, :class_name => "StoreTask", :foreign_key => "target_id"
end
class Vehicle < ActiveRecord::Base
has_many :tasks, :class_name => "VehicleTask", :foreign_key => "target_id"
end
In your databse you'll need:
Task type:string and Task target_id:integer
The advantage is that now you have a through model for each task type which can be specific.
See also STI and polymorphic model together
Cheers!
This may not be an especially helpful answer, but stated simply, I don't think there is an easy or automagic way to do this. At least, not as easy as with simpler to-one or to-many associations.
I think that creating an ActiveRecord model for the join table is the right way to approach the problem. A normal has_and_belongs_to_many relationship assumes a join between two specified tables, whereas in your case it sounds like you want to join between tasks and any one of stores, softwares, offices, or vehicles (by the way, is there a reason not to use STI here? It seems like it would help reduce complexity by limiting the number of tables you have). So in your case, the join table would also need to know the name of the Target subclass involved. Something like
create_table :targets_tasks do |t|
t.integer :target_id
t.string :target_type
t.integer :task_id
end
Then, in your Task class, your Target subclasses, and the TargetsTask class, you could set up has_many associations using the :through keyword as documented on the ActiveRecord::Associations::ClassMethods rdoc pages.
But still, that only gets you part of the way, because :through won't know to use the target_type field as the Target subclass name. For that, you might be able to write some custom select/finder SQL fragments, also documented in ActiveRecord::Associations::ClassMethods.
Hopefully this gets you moving in the right direction. If you find a complete solution, I'd love to see it!
I agree with the others I would go for a solution that uses a mixture of STI and delegation would be much easier to implement.
At the heart of your problem is where to store a record of all the subclasses of Target. ActiveRecord chooses the database via the STI model.
You could store them in a class variable in the Target and use the inherited callback to add new ones to it. Then you can dynamically generate the code you'll need from the contents of that array and leverage method_missing.
Have you pursued that brute force approach:
class Task
has_many :stores
has_many :softwares
has_many :offices
has_many :vehicles
def targets
stores + softwares + offices + vehicles
end
...
It may not be that elegant, but to be honest it's not that verbose, and there is nothing inherently inefficient about the code.