Rails 3 HABTM Join Table Re-use With Extra Field - ruby-on-rails-3

So I have a HABTM relationship between my Students and Classrooms using a join table called ClassroomStudents to handle many students belonging to many classrooms.
A recent business requirement came up that students should be able to request memberships to a classroom, so I thought that I would just create another invitation table containing classroom_id and student_id but quickly realized my classroomstudents table already handled this so I thought about simply re-using this table. I added a boolean accepted field to my classroomstudents table, true representing a membership that has been confirmed and false being the default.
There are multiple ways in which students can be added to classrooms, one of which I want them to be accepted automatically when the relationship is built. So with the following code:
#classroom << #students
How can I also specify a true value for this boolean accepted field instead of the default of false?

You could make the default value in the database be true. For example, write a migration that looks like this
class MakeAcceptedDefaultToTrue < ActiveRecord::Migration
def change
change_column :classroom_students, :accepted, :boolean, :default => true, :null => false
end
end

Related

Opinions on Best Practice for a Relation that is linked to many other types of Relations

I have a Rails database for reviewing things at a school. My tables are School, Major, Course, Instructor, Review. The way my team currently has the Review table set up, there are non-nullable foreign keys to each one of the other tables.
My issue with this is that to submit a new review the user would want to fill in only 1 of those foreign keys. Is there a way to do this with Rails? Even if there is, it seems like this would be a better use case for Instructor_Review, Course_Review, etc tables. That also has the (very nice) benefit of being able to customize table attributes for each review.
However, if we were to break up Review into multiple tables, is there a mechanism in Rails for having common columns? The overall_rating attribute would need to be included for every type of review, should the attributes just have the same name or is there a way for Rails to have table Subclasses (I know there is in SQL...)
I wouldn't create different models for the different review types if they are similar (apart from the thing the review is about). This is probably a source of duplication which should be avoided.
Instead, you could use a polymorphic association. Your review model has one thing the review belongs to (be it a school, a teacher, or an instructor). So lets model it like that. Give it a reference to that thing and the type of that thing (so that Rails knows which class it belongs to).
With polymorphic associations, your classes can be linked like this:
class Review < ActiveRecord::Base
belongs_to :reviewable, polymorphic: true
belongs_to :user
# ...
end
class School < ActiveRecord::Base
has_many :reviews, as: :reviewable
end
class Teacher < ActiveRecord::Base
has_many :reviews, as: :reviewable
end
On the database-level this means that your review table just needs one foreign key to point at the reviewable thing (plus one type column). The review-migration would look like this:
class CreateReviews < ActiveRecord::Migration
def change
create_table :reviews do |t|
t.references :user, index: true
t.references :reviewable, polymorphic: true, index: true, null: false
t.timestamps null: false
end
end
end
polymorphic: true lets Rails create the id- and type-column.
For details, please refer to the ActiveRecord documentation.
PS: There is also a RailsCast covering this issue. But beware: It's from 2009 - pretty old, but (from a quick glimpse at it) it should still work.
You are looking for polymophic relationships. Basically add a type column on the review and then you have to wire it up correctly. Here is a great article for that.
http://www.gotealeaf.com/blog/understanding-polymorphic-associations-in-rails
The review table will then have your common reviewable columns that will be shared. The next part is a little more complicated. You would then setup a review to have an extension.
To explain the extension, this is normally done when you have an entity that can either be a group or user (similar to how your InstructoreReview and CourseReview will by types of reviews). Those groups/users will have a profile that is similar to the extension of the review.
The ReviewExtension will have it's own table per review that you are extending and most of the time those extensions in their own review_extensions folder under models.
Once that is done you will delegate the common getter and setter methods you are wanting to the extension.
Hopefully this will at least give you enough to get going.

Using a different foreign key with nested resources in Rails

When working with nested resources in Rails is it possible to use a value/field other than the primary key of the parent resource as the foreign key for the child resource objects?
e.g. if I have "books" that belong to "authors", I pass the "author"'s primary key to the book when it's created with t.references :author, index: true in app/db/migrate/[timestamp]_create_books.rb (right?).
Is it possible to pass the author's name, instead? (Assuming that the "authors" table has a "name" field...)
I ask because I have a preexisting table of books with various fields (author, title, subject, year, etc.) and it seems simpler to create an authors table with the unique authors from the books table and then join them where authors.name=books.author instead of having to figure out a way of getting the unique primary keys from authors to associate with the correct author in books. (But I am probably totally wrong about this.) (In any case, I am curious if it can be done and/or what the proper way of bringing in the preexisting database that lacks the author-book associations would be.)
(I apologize if my terminology is off.)
So, your models:
class Author < ActiveRecord::Base
has_many :books
end
class Book < ActiveRecord::Base
belongs_to :author, primary_key: "name", foreign_key: "author_name"
end
It should work. But this way breaks one of the Rails cornerstones: "Convention over configuration" and that's why you have chances to end up with total mess in your DB someday.
And what else attributes except 'name' Author model has? If there are few of them (or even only one - 'name') it will be better and easier to have only one model 'Book' with the attribute 'author'.

Can I make ActiveRecord behave without an automatic ID column?

I need to have a tree table in a Rails 3.2 application that is a copy of a table that an API I am using accesses from another site. This data already exists there, and it already has it's own id and parent_id columns. I'm going to use the ancestry gem to access it.
I will need to refresh this table with that data from time to time. As such, I would like the pre-defined id column to be the id for ActiveRecord purposes. Other than the refresh of the data, there will never be any inserts to this table, only updates.
My question is, can I simply redefine the id field in the model to this predefined id, and let the MySQL "id" field just sit there?
You can disable id field like this
create table :blah, id: false
Or You can disable id as a primary key.
create_table :blah, id: false do |t|
t.integer :id, options: 'PRIMARY KEY'
end

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.

Should a one-to-one relation, column, or something else be used?

Though my problem is specifically in Ruby on Rails, I'm also asking generally: I have a users table and want to designate some users to be students and others to be teachers. What is the best way to design the schema in this case?
Edit:
Though I originally accepted vonconrad's answer--it fits the original criteria--I have to reopen and adjust the question based on feedback from nhnb.
What should be done if students and/or teachers require additional (unique) attributes?
This greatly depends on how many the different attributes between the teacher and student there'll be as well as how often new unique attributes will be added or removed. If the case is that they will differ a lot, you have two choses:
Make two models, one for Student and one for Teacher and use ruby include to share any logic between the two models. Any associations that associate with a user, you would use a polymorphic association (has_many :user, :polymorphic => true)
Put the attributes in another table. For example: you'll have the users table and a user_attributes table. users would have id, user_type, username, etc. and user_attributes would have id, user_id, attribute_name, value.
If, however, your different attributes between the two users are few and pretty rock solid, you should just consider using Single Table Inheritance.
Good luck!
In this case, I'd simply go with a single boolean column called teacher. If true, the user is a teacher. If false, it's a student. No need to create another model.
If you want more than two roles (say, student, teacher, administrator, janitor(?)), you can add another model called UserGroup. You'll also have to create a column in the users table called user_group_id. In the new model, you have a single row for each of the roles. Then, you specify the following relationships:
class User < ActiveRecord::Base
belongs_to :user_group
end
class UserGroup < ActiveRecord::Base
has_many :users
end
This will allow you to assign each user to a specific group.