Let's assume I need two resources nested in the routes.rb as follows:
resources :post do
resources :comment
end
By convention comments.id will be the comments primary key and comments.post_id will be a foreign key.
I need the primary key to be the composite key [comments.post_id, comments.id].
So that I could have every first comment of each distinct post with id == 1, every second comment of each distinct post with id == 2 and so on...
Of course I also need to disable every route that refers to a comment (the child resource) without referring also to its post (the parent resource).
This is just an example, my actual project isn't about a blog (I'd handled this problem differently), I'd like to know if there is a way to achieve such a behavior for a nested resource in order to achieve compatibility with a legacy database.
Thank you.
One way to do this is by creating another column (leave the posts.id as it was primary key), add an uniqueness validation on that column with scope to the post id, then write some before or after _create hood to generate that column value.
An example (not real code)
class Comment < ActiveRecord::Base
...
validates_uniqueness_of :sub_id, :scope => :post_id
before_create do
this.sub_id = post.comments.size + 1
end
end
So the sub_id column acts as a primary key. When you query a comment for a certain post, you do this:
post.comments.where(:sub_id => val).first
or
post.comments.find_by_sub_id(val)
Please be noted the real logic here should be adjusted to satisfy your requirement. For instance, if comments can be deleted, it might be a good idea to keep an counter on post which will used to determine the next sub_id (or write a sub id generator class).
Actually I am not quite sure what you are trying to accomplish and why? Maybe you cold make that a bit clearer. Anyway two links which might help:
Composite Primary Keys in Rails,
Rails Associations
So, if you can I would implement that using a third model as explained in the second link above. If thats is not possible, you might want to try them gem mentioned in the first link.
Just a side note: Probably logic a la
before_create do
this.sub_id = post.comments.size + 1
end
should be backed up by suitable handling of deleting comments. Otherwise we would soon run into duplicate sub_ids.
Related
How do I associate two entries in a database that are connected through a many-to-many relationship in Rails?
I'm trying to associate Users and Issues for an issue tracker. I'm using has_and_belongs_to_many, not :through. I have a :user_id and :issue_id available to me, but there doesn't seem to be User.issues.find(id) or Issue.users.find(id) available to me. I have a route post "/", to: "home#create". I'm trying to make a create method in home_controller.rb.
From the look of it you're calling the method on the User class and not an instance.
If you want to get the issues connected to a user you need to fetch the user first:
User.find(id).issues
If you want to add a record to the association you can use the shovel method or any of the methods generated by the association macro:
User.find(id).issues << Issue.find(3)
User.find(id).issues.push(Issue.find(3))
User.find(id).issue_ids = [1, 2, 3]
Besides that you have a smattering of naming issues in your schema. Use snake_case everywhere in your database schema unless you have a good reason why you want to break the conventions and feel like explicitly configuring table and foreign key names.
I would also really question if you really want to use has_and_belongs_to_many. It should only really be used if you can't foresee that you ever will need to add additional attributes to the join table or never need to query the table directly - it seems pretty unrealistic that that would be true in an issue tracker. You want has_many through: - pretty much always.
I have a route post "/", to: "home#create". I'm trying to make a
create method in home_controller.rb.
Don't throw everything into a junk drawer controller. Think about your app in terms of resources that can be CRUD:ed and create controllers that handle just that resource. You should think about what the relation between a user and an issue is in your domain and how you can model it as an actual entity in the domain logic instead of just plumbing.
Maybe all I need to do is direct you to Rails Guides: Active Record Associations.
There is neither of these
User.issues.find(id)
Issue.users.find(id)
because when you are finding an issue or user by id, you don't use the association. Instead use these:
Issues.find(id)
Users.find(id)
Since the :id is unique this will work and should be what you want.
The only time you want to query issues or users using the association will be when you have the data for the other end of the relationship.
user = User.find(user_id)
issue = user.issues.where(id: issue_id)
Since the :id field is unique, this is the same as Issues.find(id). However if you want to get a collection of a user's issues with some other data, you can put the condition for that data in the where.
You can create an issue for a user this way:
user = User.find(user_id)
issue = User.issues.create( ... )
I am a beginner to rails framework. I have a fundamental question.
I am trying to define some models and their association referring the popular rails guides. My association looks like below.
class Person < ActiveRecord::Base
has_one :head
end
class Head < ActiveRecord::Base
belongs_to :person
end
Here, I need to add the foreign_key (Persons's primary key) in the table 'head'.
Now, if I need to get the 'head' of a 'person', rails need to scan through the head table and match the person_id.
The straight forward way I would think is to add the foreign key in 'person' table. Then I can directly refer the 'head' from 'person' with it's ID.
It appears that rails convention is not performance friendly. Am I missing something here?
When you create the migration to add the column containing the foreign key, it is highly recommended to add an index on this column. This way, the database will efficiently find the Head from the person_id (as efficiently then a search by its id).
add_index :heads, :person_id
If it's a one-to-one association, you can even add the unique option (unless your application accepts conjoined twins :-) ):
add_index :heads, :person_id, :unique => true
I suggest you to have a look to this 2 articles on where to use indexes:
http://tomafro.net/2009/08/using-indexes-in-rails-index-your-associations
http://tomafro.net/2009/08/using-indexes-in-rails-choosing-additional-indexes
Re: It appears that rails convention is not performance friendly. Am I missing something here?
Yes. Rails is designed for production sites. So it includes many performance features. As #Baldrick says, the answer to your specific concern is to always add an index for foreign key fields.
Adding an index for each foreign key field is needed, for performance reasons, for any SQL dbms application, no matter the language. Note that the index is added to the database, not to the MVC layers (Rails).
Rails itself includes additional performance features including sql results caching, optional fragment and page caching, and more.
Rails 3.2 includes the slow query features. These enable Rails to automatically show you the queries which are slow. You can then focus on fixing them as appropriate.
Suppose I have posts, which have many categories through categorizations. Suppose that I add a boolean column primary to categorizations in order to determine the primary category of a post. But now I'm stuck dealing with the join model, when what I'd really like to do is something like this:
post = Post.first
primary_cat = post.categories.where(:primary => true)
post.categories.first.primary = true
post.save # would actually update the categorization, setting primary = true
There are all sorts of examples I could give you to show why this would be useful, but essentially I want to be able to interact with a model as though it is somehow merged with its join model. Being able to say "What's the primary category?" or "OK this category will be the primary one" without ever touching the join model is the intuitive for me the think of this.
Is this even possible with Rails? Has anyone seen a effort to do this sort of thing before?
I like quest's solution except that setting it should just be post.primary_category= and should take a category object. Just setup a has_one :primary_category on post and you're golden.
Short answer is to create a set_primary(post) method on category that takes as its argument the post and boolean.
def set_primary(post)
categorization = post.categorization.where('your opts hash here')
categorization.primary = true
categorization.save!
end
post.categories.first.set_primary(post)
Charlie Bowman does have the first piece of the puzzle with the set_primary logic on the Category model, however his setter doesn't unset the previous active category... In addition, the expensive part of Steve's problem, is actually just getting the primary category on each request. To get around that, I would also save the primary category id on the Post itself. That way you don't need to access the join model to figure out the primary category.
def set_primary_category(post)
post.categorizations.each do |cat|
if cat.post == post
cat.primary = true
cat.save!
post.update_attribute(:primary_category_id, cat.category_id)
else
cat.update_attribute(:primary, false) if cat.primary
end
end
end
TO SET:
post.categories.first.set_primary(post)
TO ACCESS:
post.primary_category
Unfortunately I don't think anyone has made a gem that makes this easier for you to accomplish, but the logic is pretty straightforward. It also has the benefit of having access to the primary state in both the categorization and the post, so that you always have a fast way to access the data.
Also, I think its good to remember. You have a many to many relationship for posts/categories. But only a has_one relationship for post/primary_category. Whenever you have an access pattern like that, I like to try and stay away from the join model for the has_one, since it just isn't needed.
this question is probably a duplicate of Ruby on Rails: attr_accessor for submodels, but basically delegate should be what you're looking for, with an additional callback on save.
I'm porting some functionality to Rails, and I'm working with an existing table which is for comments.
Basically, there are two types of comments - profile comments (photo_id column is null) and photo comments (photo_id column is set to photo's ID)
I got single table inheritance working just fine by adding a type field to the table, but I'm wondering if there's a way to get my single table inheritance working without the type field. According to the Rails API documentation, "If you don‘t have a type column defined in your table, single-table inheritance won‘t be triggered. In that case, it‘ll work just like normal subclasses with no special magic for differentiating between them or reloading the right type with find."
I'm wondering if there's a way that I can customize my models to determine type based on photo_id being nil or having an integer value, rather than using the database column (which I'd rather not add if I don't have to.) Any ideas?
If comments models doesn't differ much, I wouldn't bother with single table inheritance at all. Just add:
# to Comment model
belongs_to :photo
belongs_to :profile
# to Profile model
has_many :comments
# to Photo model
has_many :comments
Then:
#photo.comments # will return comments associated with photos
#profile.comments # will return comments associated with profiles
There may be problem if you had both photo_id and profile_id set (I suppose it may happen when you comment a photo that is associated with profile), so you can change in Profile model:
has_many :comments, :conditions => "photo_id is not null"
Another approach (I think better) it to you polymorphic associations but you will need to modify you sql tables.
I suspect you cannot do this trivially. However, one possibility is to trick active record into using a view rather than a table, and write some database functions to set this magic attribute based on which id is set.
However, in the end, I suspect it would be far, far easier to just add the column.
I think there are a lot of places where my design may be screwing this up. I have very limited experience with Rails though. This is happening in Rails 2.3.2 with Postgres 8.3.
We've got two tables in our DB. One called "survey" and one called "survey_timepoint". A survey can have multiple time points so in the survey_timepoint table there is a column called "survey_id" with an fk constraint on it.
I also think I should mention that the tables were not created with a rails migration although they do follow the rails naming conventions. I suspect AR isn't anticipating a constraint on that column and it doesn't know how to handle the situation.
In my rails models I have:
has_many :survey_timepoint
and
belongs_to :survey
If I do something like:
s = Survey.new
s.survey_timepoint.push SurveyTimepoint.new
s.save!
I get:
ActiveRecord::StatementInvalid: PGError: ERROR: insert or update on table "survey_timepoints" violates foreign key constraint "survey_timepoints_fk"
DETAIL: Key (survey_id)=(59) is not present in table "surveys"
I'm assuming that if I delete that fk constraint on survey_timepoint.survey_id it'll work ok. It seems like I shouldn't have too though. Am I going to be stuck creating and saving each of the objects separately and wrapping the whole process in a transaction? It seems rather un-railsy. Apologies for any necessary information that I may have omitted.
You might want to check the SQL commands being sent. It looks like it is adding the survey_timepoint record before the survey record. Note that you are already dealing with two database changes — the survey and the survey_timepoint — so you should be using a transaction.
You can fix the immediate problem by doing s.save! before adding the timepoint (and then calling it again). My knowledge of Rails functionality is not deep enough to know if there is a more "railsy" way of doing this then wrapping it in a transaction.
I just experimented and found that this works with MySQL:
s = Survey.new()
s.survey_timepoints << SurveyTimepoint.new # Note "survey_timepoints" (plural)
s.save!
I think it would work equally well with PostgreSQL.
It does two inserts, first the Survey, then the timepoint, and wraps them in a transaction.
You can also do it all on one line:
Survey.create!({:name=>'New Survey', :survey_timepoints => [SurveyTimepoint.new]})
Incidentally, for ActiveRecord to work right you have to make sure of your singulars and plurals. (If you want to break the expected forms, you'll need to tell AR you're doing that -- a whole other topic.)
Your tables should be:
surveys
-------
# ...
survey_timepoints
-----------------
survey_id
# ...
And in your models you'd have:
class Survey < ActiveRecord::Base
has_many :survey_timepoints
# etc...
end
class SurveyTimepoint < ActiveRecord::Base
belongs_to :survey
end