Saving related entities in Rails - ruby-on-rails

I can't modify existing relations in RoR.
Db schema is users(id), books(id, title), ratings(user_id, book_id, rating)
Models are
class User < ActiveRecord::Base
has_many :ratings
end
class Rating < ActiveRecord::Base
belongs_to :user
belongs_to :book
end
class Book < ActiveRecord::Base
has_many :ratings
end
Creating new rating with book.ratings.build(:user_id => 1, :rating => 5); book.save works fine, but I can't modify existing rating with
r = book.ratings.where(:user_id => 1).first
r.rating = 5
book.save doesn't save corresponding rating and r.save raises error about column rating.id. Is there a way to make things work without modifying db schema?

The reason why you're getting the error is because your ratings table has data but doesn't have and id column.
Add and id column to your ratings table and save your change calling rating.save.

Related

Eager load in model?

I wonder if we could eager load in model level:
# country.rb
class Country < ActiveRecord::Base
has_many :country_days
def country_highlights
country_days.map { |country_day| country_day.shops }.flatten.uniq.map { |shop| shop.name }.join(", ")
end
end
# country_day.rb
class CountryDay < ActiveRecord::Base
belongs_to :country
has_many :country_day_shops
has_many :shops, :through => :country_day_shops
end
# shop.rb
class Shop < ActiveRecord::Base
end
Most of the times it's difficult to use .includes in controller because of some polymorphic association. Is there anyway for me to eager load the method country_highlights at the model level, so that I don't have to add .includes in the controller?
You can't "eager load" country_days from a model instance, but you can certainly skip loading them all together by using a has_many through:. You can also skip the extra map, too.
# country.rb
class Country < ActiveRecord::Base
has_many :country_days
has_many :country_day_shops, through: :country_days #EDIT: You may have to add this relationship
has_many :shops, through: :country_day_shops #And change this one to use the new relationship above.
def country_highlights
shops.distinct_names.join(", ")
end
end
# country_day.rb
class CountryDay < ActiveRecord::Base
belongs_to :country
has_many :country_day_shops
has_many :shops, :through => :country_day_shops
end
# shop.rb
class Shop < ActiveRecord::Base
def self.distinct_names
pluck("DISTINCT shops.name") #Edit 2: You may need this instead of 'DISTINCT name' if you get an ambiguous column name error.
end
end
The has_many through: will use a JOIN to load the associate shop records, in effect eager loading them, rather than loading all country_day records and then for each country_day record, loading the associated shop.
pluck("DISTINCT name") will return an array of all of the unique names of shops in the DB, using the DB to perform a SELECT DISTINCT, so it will not return duplicate records, and the pluck will avoid loading ActiveRecord instances when all you need is the string name.
Edit: Read the comments first
You could cache the end result (the joined string or text record in your case), so you'll not have to load several levels of records just to build this result.
1) Add a country_highlights text column (result might be beyond string column limits)
2) Cache the country_highlights in your model with a callback, e.g. before every save.
class Country < ActiveRecord::Base
has_many :country_days
before_save :cache_country_highlights
private
def cache_country_highlights
self.country_highlights = country_days.flat_map(&:shops).uniq.map(&:name).join(", ")
end
end
Caching you calculation will invoke a little overhead when saving a record, but having to load only one instead of three model records for displaying should speed up your controller actions so much that it's worth it.

Order by Join Table Attribute in Many-to-Many Relation

I have a many-to-many relation between Resumes and Educations which allows for the same education entry to appear on multiple resumes. When displaying Educations on a resume, I want the Educations to be ordered for that specific resume. To do this I set up a join table, Educations_Resumes, with the order information as a property.
However, when I try some like resume.educations I get the following error:
ActiveRecord::StatementInvalid: SQLite3::SQLException: near "order": syntax error:
SELECT "educations".* FROM "educations" INNER JOIN "educations_resumes" ON
"educations"."id" = "educations_resumes"."education_id" WHERE
"educations_resumes"."resume_id" = 2 ORDER BY educations_resumes.order
The models are setup as:
class Resume < ActiveRecord::Base
has_many :educations_resumes
has_many :educations, :through => :educations_resumes,
:order => 'educations_resumes.order'
end
class EducationsResume < ActiveRecord::Base
belongs_to :resume
belongs_to :education
end
class Education < ActiveRecord::Base
has_many :educations_resumes
has_many :resumes, :through => :educations_resumes
end
Any suggestions on how to correctly order resume.educations would be greatly appreciated
You are using a keyword as a column name:
http://www.sqlite.org/lang_keywords.html
Run a migration and change the column name:
rails g migration FixColumnName
This will give you a empty migration file that you should fill in with something like:
class FixColumnName < ActiveRecord::Migration
def change
rename_column :educations_resumes, :order, :ordering
end
end

joining 3 tables or more with active record

Let say I have 3 tables:
schools{id, name, desc, adress}
reviews{id, content, rating, school_id, user_id} # user_id & schoold_id is foregin keys
users{id, name, city}
How do I write a rails scope or method the joins all 3 tables and grab the column
the schools.name , reviews.content and reviews.rating, the users.name
I tried somehting this, but it only return the review data and not the joining part.
Review.joins(:school, :user).select("content, rating, schools.name, users.name").all
I am using rails 3.2
You just need to define has-many :through relationships for schools and users like this:
class School < ActiveRecord::Base
has_many :users, :through => :reviews
end
class Review < ActiveRecord::Base
belongs_to :users
belongs_to :schools
end
class User < ActiveRecord::Base
has_many schools, :through => reviews
end
In your Review controller you can do
def index
#reviews = Review.all
end
Then in your views each review object will have a school and a user associated with it, so you would just do:
review.content, review.rating, review.user.name, review.school.name
Reading this will help you understand why: http://guides.rubyonrails.org/association_basics.html

How to save extra fields in Rails using has_and_belongs_to_many

I have the following structure in my rails application:
class Movie < ActiveRecord::Base
has_and_belongs_to_many :celebs, :join_table => "movies_celebs"
end
class Celeb < ActiveRecord::Base
has_and_belongs_to_many :movies, :join_table => "movies_celebs"
end
class MovieCeleb < ActiveRecord::Base
belong_to :movie
belong_to :celeb
end
Now MovieCeleb has 2 extra fields CastName(string), CastType('Actor/Director). When I save Movie i create celebs also and fill celebs into celebs relation and it save movies_celebs automatically into database. But how can i pass the CastName and CastType to save also.
Please advise
Thanks in advance.
You should use has_many :through instead of has_and_belongs_to_many if you need validations, callbacks, or extra attributes on the join model.
See :
http://guides.rubyonrails.org/association_basics.html#choosing-between-has_many-through-and-has_and_belongs_to_many
Note the following is for Rails v2
script/generate model Movie id:primary_key name:string
script/generate model Actor id:primary_key movie_id:integer celeb_id:integer cast_name:string cast_type:string
script/generate model Celeb id:primary_key name:string
model/movie.rb
class Movie < ActiveRecord::Base
has_many :actors
has_many :celebs, :through => :actors
end
model/celeb.rb
class Celeb < ActiveRecord::Base
has_many :actors
has_many :movies, :through => :actors
end
model/actor.rb
class Actor < ActiveRecord::Base
belongs_to :movie
belongs_to :celeb
end
Test the associations with the ruby rails console in the application folder
>script/console
>m = Movie.new
>m.name = "Do Androids Dream Of Electric Sheep"
>m.methods.sort #this will list the available methods
#Look for the methods 'actors' and 'celebs' - these
#are the accessor methods built from the provided models
>m.actors #lists the actors - this will be empty atm
>c = Celeb.new
>c.name = "Harrison Ford"
>m.celebs.push(c) #push Harrison Ford into the celebs for Blade Runner
>m.actors #Will be empty atm because the movie hasnt been saved yet
>m.save #should now save the Movie, Actor and Celeb rows to relevant tables
>m.actors #Will now contain the association for
#Movie(id : 1, name : "Do Androids..") - Actor(id : 1, movie_id : 1, celeb_id : 1) -
#Celeb(id : 1, name : "Harrision Ford")
>m = Movie.new #make a new movie
>m.name = "Star Wars"
>m.celebs.push(c) #associated the existing celeb with it
>m.save
>movies = Movie.all #should have the two movies saved now
>actors = Actor.all #should have 2 associations
>this_actor = Actor.first
>this_actor.cast_type = "ACTOR"
>this_actor.save
Then you'll probably want to check out Ryan Bates' Railcasts http://railscasts.com #47 (Two Many-to-Many), and #73, #74, #75 (Complex forms parts 1-3).
There's an updated version of the many-to-many form code on the webpage for #75.

Rails 3, belongs_to, has one? For 3 models, Users, Instances, Books

I have the following models:
Users (id, name, email, instance_id, etc...)
Instances (id, domain name)
Books (id, name, user_id, instance_id)
In Rails 3, When a new book is created, I need the user_id, and instance_id to be populated based on the current_user.
Currently, user_id is being assigned when I create a new book but not instance_id? What needs to happen in rails to make that field get filled out on book creation?
Also, shouldn't rails be error'ing given that I can create books without that instance_id filled out?
thxs
It looks like you have de-normalized User and Book models by adding reference to Instance model. You can avoid the redundant reference unless you have a specific reason.
I would rewrite your models as follows:
class Instance < ActiveRecord::Base
has_many :users
has_many :books, :through => :users, :order => "created_at DESC"
end
class User < ActiveRecord::Base
belongs_to :instance
has_many :books, :order => "created_at DESC"
end
class Book < ActiveRecord::Base
belongs_to :user
has_one :instance, :through => :user
end
Now to create a new book for a user.
current_user.books.build(...)
To get a list of the books belonging to user's instance:
current_user.instance.books
To get a list of the books created by the user:
current_user.books
Make sure you index the instance_id column in users table and user_id column in books table.
Rails will only produce an error in this case if (a) you have a validation that's failing, or (b) you have database foreign keys that aren't being satisfied.
What's an instance? i.e. if instance_id is to be populated based on the current user, what attribute of the user should supply it? (the instance_id? why?)

Resources