rails3 and the proper way to use associations - ruby-on-rails-3

I'm doing my first rails(3) application.
Associations don't make sense. First, even the rails guides don't
really explain what they do, they just explain how to use them.
From what I gather, associations do two things:
a) Allow ActiveRecord to optimize the structure of the database.
b) Allow ActiveRecord to offer an alternate ruby syntax for
joins and the like (SQL queries). I want this.
I'm trying to understand associations, and how to properly use them. Based
on the example below, it seems like associations are 'broken' or at least
the documentation is.
Consider a trivial version of my application. A teacher modifying wordlists
for study.
There are 3 relevant tables for this discussion. For clarity, I've simply
included the annotate(1) tool's definition of the table, and removed
unnecessary fields/columns.
A wordlist management table:
Table name: wordlist_mgmnt_records
id :integer not null, primary key
byline_id :integer(8) not null
A table that maps words to a word list:
Table name: wordlists
wordlist_mgmnt_id :integer not null
word_id :integer not null
We don't actually care about the words themselves. But we do care about
the last table, the bylines:
Table name: bylines
id :integer(8) not null, primary key
teacher_id :integer not null
comment :text not null
Bylines record who, what tool was used, where, when, etc. Bylines are
mainly used to trouble shoot what happened so I can explain to users what
they should have done (and/or repair their mistakes).
A teacher may modify one or more word list management records at a time
(aka single byline). Said another way, a single change may update multiple
word lists.
For wordlist_mgmnt_records the associations would be:
has_many :bylines # the same byline id can exist
# in many wordlist_mgmnt_records
But what's the corresponding entry for bylines?
The Beginning Rails 3 (Carneiro, et al) book says:
"Note: For has_one and has_many associations, adding a belongs_to
on the other side of the association is always recommended. The
rule of thumb is that the belongs_to declaration always goes in
the class with the foreign key."
[ Yes, I've also looked at the online rails guide(s) for this. Didn't
help. ]
For the bylines table/class do I really want to say?
belongs_to :wordlist_mgmnt_records
That really doesn't make sense. the bylines table basically belongs_to
every table in the data base with a bylines_id. So would I really say
belongs_to all of them? Wouldn't that set up foreign keys in all of the
other tables? That in turn would make changes more expensive (too many
CPU cycles) than I really want. Some changes hit lots of tables, some of
them very large. I prize speed in normal use, and am willing to wait to
find bylines without foreign keys when using bylines for cleanup/repair.
Which brings us full circle. What are associations really doing in rails,
and how does one use them intelligently?
Just using associations because you can doesn't seem to be the right
answer, but how do you get the added join syntax otherwise?

I'll try to help your confusion....
A byline can have multiple wordlist_mgmnt_records, so defining the has_many there seems to make sense.
I'm not sure I understand your confusion in the other direction. Since you have defined the attribute wordlist_mgmnt_records.byline_id, any given wordlist_mgmnt_record can only 'have' (belong_to) a single byline. You're simply defining the crows foot via ruby (if you like database diagrams):
wordlist_msgmnt_records (many)>>----------(one) byline
Or read in english: "One byline can have many wordlist_mgmnts, and many individual wordlist_mgmnt's can belong to a single byline"
Adding the belongs_to definition to the wordlist_mgmnt model doesn't affect the performance of the queries, it just let's you do things like:
#record = WordlistMgmntRecord.find(8)
#record_byline = #record.byline
Additionally you're able to do joins on tables like:
#records = WordlistMgmntRecord.joins(:byline).where({:byline => {:teacher_id => current_user.id}})
Which will execute this SQL:
SELECT wordlist_mgmnt_records.*
FROM wordlist_mgmnt_records
INNER JOIN bylines
ON wordlist_mgmnt_records.byline_id = bylines.id
WHERE bylines.teacher_id = 25
(Assuming current_user.id returned 25)
This is based off of your current DB design. If you find that there's a way you can implement the functionality you want without having byline_id as a foreign key in the wordlist_mgmnt_records table then you would modify your models to accomodate it. However this seems to be how a normalized database should look, and I'm not really sure what other way you would do it.

Related

Ruby on Rails where query with relations

I am trying to use where query with relationships.
How can I query using where with relations in this case?
This is model
User
has_many :projects
has_many :reasons, through: :projects
Project
belongs_to :user
has_many :reasons
Reasons
belongs_to :project
This is the codes which doesn't work
# GET /reasons
def index
reasons = current_user.reasons
updated_at = params[:updated_at]
# Filter with updated_at for reloading from mobile app
if updated_at.present?
# This one doesn't work!!!!!!!!!!!!
reasons = reasons.includes(:projects).where("updated_at > ?", Time.at(updated_at.to_i))
# Get all non deleted objects when logging in from mobile app
else
reasons = reasons.where(deleted: false)
end
render json: reasons
end
---Update---
This is correct thanks to #AmitA.
reasons = reasons.joins(:project).where("projects.updated_at > ?", Time.at(updated_at.to_i))
If you want to query all reasons whose projects have some constraints, you need to use joins instead of includes:
reasons = reasons.joins(:project).where("projects.updated_at > ?", Time.at(updated_at.to_i))
Note that when both includes and joins receive a symbol they look for association with that precise name. That's why you can't actually do includes(:projects), but must do includes(:project) or joins(:project).
Also note that the constraints on joined tables specified by where must refer to the table name, not the association name. That's why I used projects.updated_at (in plural) rather than anything else. In other words, when calling the where method you are in "SQL domain".
There is a difference between includes and joins. includes runs a separate query to load the dependents, and then populates them into the fetched active record objects. So:
reasons = Reason.where('id IN (1, 2, 3)').includes(:project)
Will do the following:
Run the query SELECT * FROM reasons WHERE id IN (1,2,3), and construct the ActiveRecord objects Reason for each record.
Look into each reason fetched and extract its project_id. Let's say these are 11,12,13. Then run the query SELECT * FROM projects WHERE id IN (11,12,13) and construct the ActiveRecord objects Project for each record.
Pre-populate the project association of each Reason ActiveRecord object fetched in step 1.
The last step above means you can then safely do:
reasons.first.project
And no query will be initiated to fetch the project of the first reason. This is why includes is used to solve N+1 queries. However, note that no JOIN clauses happen in the SQLs - they are separate SQLs. So you cannot add SQL constraints when you use includes.
That's where joins comes in. It simply joins the tables so that you can add where constraints on the joined tables. However, it does not pre-populate the associations for you. In fact, Reason.joins(:project), will never instantiate Project ActiveRecord objects.
If you want to do both joins and includes, you can use a third method called eager_load. You can read more about the differences here.

Choosing Between has_many :through and has_and_belongs_to_many - "should" or "must"

Or: "What happens if you decide wrong"
Doc says:
You should use has_many :through if you need validations, callbacks,
or extra attributes on the join model.
But should the "you should use" be a "you must use"?
The point is, we have one attribute in a join table, and this one is heavily discussed.
So what happens if I (we) decide to use the simpler HABTM and in one year the friendly attribute pops up? Is it possible to access it (more complex, ok), or have we go back to start and redesign?
All the answers (and there are a lot of) to this - "HABTM or :through", are more or less easy to decide; "take this or the other".
I want to know how to correct the error if we decide wrong.
Is it eg. possible to "push a model between", or how to access this one attribute, if it pops up?
Or is the better strategy to start with :through? just for sure
Yes, it's possible to convert a HABTM into a HMT.
The join table follows a naming convention of table + table in lexical order like "developers_projects".
Later, if you want to make the relationship HMT just create a model called DevelopersProject and use that as the join table. It's the same table. But you can then use migrations to add fields, and use the model to add validations, etc. etc.
The advantage of starting with HMT is that you get to call the join table whatever you want, but that's not hugely important.

Ruby on Rails Migration with Primary and Foreign Key

I am just starting ROR and making different tables for the SQLite database and am running into some trouble in relation to automatically generated id's of tables and foreign keys. To create my tables I used the "rails generate scaffold" command followed by the attributes that I needed. I went to the db/migrate directory and looked at what I had just defined. But I don't understand how I am supposed to explicitly reference foreign keys. I'm used to using Oracle so I'd normally do this process in the very first step during creation and be done with it already.
For example, I have a users table with some general attributes such as a username, password, etc. I also have an orders table with attributes transactionID (auto generated), userID (I want to this to be an FK), PartNo (FK key from the Products table). What I don't understand is how to use the auto generated key from the Users table and include it as a foreign key in the Orders table.
Also, I read somewhere else that if I put a line of code such as
t.integer user_id
in my create_orders.rb file then it would automatically know that this is a foreign key!?
I feel like this is probably really easy and I'm missing something. This is my first time using rails and I thought I got to define the foreign keys straight away via SQL. Any help would be greatly appreciated. If I could figure this out it would save me a lot of time.
After you do this, on your Order model, you'd use a belongs_to :user to generate the relationship and utilize the foreign key.
If you want to be able to go from User to Order, you'll likely be a one-to-many, so you'd have to do has_many :orders on User.
Here is the documentation for associations in Rails. And here is the specific documentation for belongs_to vs has_one.

Is it bad data design to duplicate Rails associations?

Let's say I have a Rails 3 application with the following model associations:
user
belongs_to :group
item
belongs_to :group
belongs_to :user
If code is not carefully written, this can result in data discrepancies where:
item.group
and
item.user.group
no longer return the same group, when they should. An item should always only belong to only 1 group.
My understanding is that this duplicate association may have been created to make querying simpler (reduce the number of tables joined).
So my question is, is this just an outright terrible practice or is this a question of valid trade-offs, that there are cases where the data and association duplication are acceptable because we can make querying simpler with fewer joins.
UPDATE
So far seems like the answer is "trade offs" and not "bad practice/code smell".
There seems to be multiple ways this can be handled, probably with a mix of constraints, advantages, disadvantages, use cases, etc:
1) denormalized, duplicated data as above
2) item has_one :group, :through => :user
3) item delegate :group :to => :user
I'm trying to understand the differences between approach #2 and #3. After experimenting with both approaches in the console, seems like the queries produced by Rails when item.group is called will be different. (2) produces a single query that joins groups and users. (2) produces two queries, first to find the user and then to find the group based on the user.
I think this is a question of valid trade-offs. Strictly speaking, in a fully normalized database your items table wouldn't have a group column, instead it would always go through the users table to find the group. That has the least amount of duplication, and thus the highest data integrity, but at the cost of doing that extra join every time you want to find an item's group. I'm assuming that a user also only belongs to one group. If a user can belong to many groups, then I think you would have to have that items.group_id column to know to which of those groups an item belongs.
If you want the faster query performance on lookup, you can keep the extra association like you have, and add an extra before_* hook to make sure that item.group_id = item.user.group_id, and raise a validation error if they don't match. This would make validating/inserting slightly slower, but would maximize your data integrity and still let you get slightly better performance when reading from the database.

Multiple Foreign Keys for a Single Record in Rails 3?

I am working on an app that will manage students enrolled in a course. The app will have users who can log in and manipulate students. Users can also comment on students. So three of our main classes are Student, User, and Comment. The problem is that I need to associate individual comments with both of the other models: User and Student. So I've started with some basic code like this...
class Student < ActiveRecord::Base
has_many :comments
end
class User < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :student
belongs_to :user
attr_accessible :comment
end
So in the comments table, a single record would have the following fields:
id
comment
student_id
user_id
created_at
updated_at
This presents several problems. First, the nice Rails syntax for creating associated objects breaks down. If I want to make a new comment, I have to choose between the foreign keys. So...
User.comments.create(attributes={})
OR
Student.comments.create(attributes={})
Another option is to arbitrarily pick one of the foreign keys, and manually add it to the attrs hash. So...
User.comments.create(:comment => "Lorem ipsum", :student_id => 1)
The problem with this option is that I have to list student_id under attr_accessible in my Comment model. But my understanding is that this poses a security risk since someone could technically come along and reassociate the comment with a different student using mass assignment.
This leads to a further question about data modeling in general using Rails. The app I'm currently building in Rails is one that I originally wrote in PHP/MySQL a few years ago. When I first studied SQL, great importance was placed on the idea of normalization. So, for example, if you have a contacts table which stores names and addresses, you would use a lot of foreign key relationships to avoid repeating data. If you have a state column, you wouldn't want to list the states directly. Otherwise you could potentially have thousands of rows that all contain string values like "Texas." Much better to have a separate states table and associate it with your contacts table using foreign key relationships. My understanding of good SQL theory was that any values which could be repeating should be separated into their own tables. Of course, in order to fully normalize the database, you would likely end up with quite a few foreign keys in the contacts table. (state_id, gender_id, etc.)
So how does one go about this in "the Rails way"?
For clarification (sorry, I know this is getting long) I have considered two other common approaches: "has_many :through =>" and polymorphic associations. As best I can tell, neither solves the above stated problem. Here's why:
"has_many :through =>" works fine in a case like a blog. So we have Comment, Article, and User models. Users have many Comments through Articles. (Such an example appears in Beginning Rails 3 from Apress. Great book, by the way.) The problem is that for this to work (if I'm not mistaken) each article has to belong to a specific user. In my case (where my Student model is here analogous to Article) no single user owns a student. So I can't say that a User has many comments through Students. There could be multiple users commenting on the same student.
Lastly, we have polymorphic associations. This works great for multiple foreign keys assuming that no one record needs to belong to more than one foreign class. In RailsCasts episode #154, Ryan Bates gives an example where comments could belong to articles OR photos OR events. But what if a single comment needs to belong more than one?
So in summary, I can make my User, Student, Comment scenario work by manually assigning one or both foreign keys, but this does not solve the issue of attr_accessible.
Thanks in advance for any advice!
I had your EXACT question when I started with rails. How to set two associations neatly in the create method while ensuring the association_ids are protected.
Wukerplank is right - you can't set the second association through mass assignment, but you can still assign the association_id directly in a new line.
This type of association assignment is very common and is littered throughout my code, since there are many situations where one object has more than one association.
Also, to be clear: Polymorphic associations and has_many :through will not solve your situation at all. You have two separate associations (the 'owner' of a comment and the 'subject' of a comment) - they can't be rationalised into one.
EDIT: Here's how you should do it:
#student = Student.find_by_id(params[:id])
#comment = #student.comments.build(params[:comment]) #First association is set here
#comment.user = current_user #Second association is set here
if #comment.save
# ...
else
# ...
end
By using the Object.associations.build, Rails automatically creates a new 'association' object and associates it with Object when you save it.
I think polymorphic association is the way to go. I'd recommend using a plugin instead of "rolling your own". I had great results with ActsAsCommentable (on Github).
As for your attr_accessible problem: You are right, it's more secure to do this. But it doesn't inhibit what you are trying to do.
I assume that you have something that holds the current user, in my example current_user
#student = Student.find(params[:id])
#comment = Comment.new(params[:comment]) # <= mass assignment
#comment.student = #student # <= no mass assignment
#comment.user = current_user # <= no mass assignment
if #comment.save
# ...
else
# ...
end
The attr_accessible protects you from somebody sneaking a params[:comment][:student_id] in, but it won't prevent the attribute from being set another way.
You still can get all comments of your users through the has_many :comments association, but you can also display who commented on a student thanks to the belongs_to :user association:
<h1><%= #student.name %></h1>
<h2>Comments</h2>
<%- #student.comments.each do |comment| -%>
<p><%= comment.text %><br />
by <%= comment.user.name %></p>
<%- end -%>
PLUS:
Don't over engineer your app. Having a state:string field is perfectly fine unless you want to do something meaningful with a State object, like storing all districts and counties. But if all you need to know a students state, a text field is perfectly fine. This is also true for gender and such.
Well, for the first part. If I understood your question correctly, I think, that since comments are listed within students controller, you should associate them through Student model (it just seems logical to me). However, to protect it from assigning wrong user id, you could do something like this
#student = Student.find params[:id]
#student.comments.create :user => current_user
current_user might be a helper that does User.find session[:user_id] or something like that.
has_many :through association doesn't make sense here. You could use it to associate User and Student through Comment, but not User and Comment through Student
Hope that helps.