Models:
class User < ActiveRecord::Base
has_many :attendances
has_many :courses, :through => :attendances
end
class Course < ActiveRecord::Base
has_many :attendances
end
class Attendance < ActiveRecord::Base
belongs_to :user
belongs_to :course
end
Migrations:
create_table(:users) do |t|
t.string :name
end
create_table(:courses) do |t|
t.string :name
end
create_table(:attendances) do |t|
t.references :user, :course
t.date :date
end
Question
I would like to query:
a list of all courses
the date of a given user's last attendance (if any) for each course
What is the best way to tie the following together into a single query?
#courses = Course.all
#user = User.first
#attendances = #user.attendances.group('course_id').order('date DESC')
Note that there is a requirement to include courses that a user has not yet attended.
Any advice much appreciated.
Update
The result I am looking for is as follows:
Course Last attended
===============================
Some course 2011-03-09
More training Not Attended
Another course 2010-12-25
In SQL, I would write this query as:
SELECT * FROM courses AS c
LEFT OUTER JOIN attendances AS a ON a.course_id=c.id
WHERE a.user_id=1
GROUP BY a.course_id
ORDER BY a.date DESC;
I could always execute this SQL from within Rails, but I would prefer to avoid this and do things "the Rails way" if possible.
The "Rails-Way" is to define small finders in the model and then to chain them in the controllers.
In class Attendance you could define a new method
def self.last_attendance
maximum("date")
end
In class Corse ...
def self.ordered
order("name DESC")
end
And so on. In the controller then, you use them in different combinations. The big advantage of this approach is
Reusability of finder methods
Decoupling of classes
Better readability of code
Your has_many through relation should be like this:
class User < ActiveRecord::Base
has_many :attendances
has_many :courses, :through => :attendances
end
class Course < ActiveRecord::Base
has_many :attendances
has_many :users, :through => :attendances
end
class Attendance < ActiveRecord::Base
belongs_to :user
belongs_to :course
end
And please explain your problem. Because
#user.courses will give you only related courses
Course.all will give you all courses
Edited:
Course.find(:all, :joins => 'LEFT OUTER JOIN attendances ON attendances.course_id =
courses.id', :conditions => ['attendances.user_id = ?', 1], :group =>
'attendances.course_id', :order => 'attendances.date DESC')
I hope it will work for you.
Related
I'm new to Ruby on Rails and I'm trying to build a relationship between the classes Club, Sponsor and Match.
The relationhip has to be like:
One Club has zero to many Sponsors
One Sponsor has zero to many Matches
One Match has zero to many Sponsors
My models look like this
class Match < ApplicationRecord
belongs_to :team
has_many :matchplayers
has_many :players, through: :matchplayers
has_many :sponsors
end
class Club < ApplicationRecord
has_many :teams
has_many :sponsors
accepts_nested_attributes_for :teams, :reject_if => :all_blank, :allow_destroy => true
end
class Sponsor < ApplicationRecord
belongs_to :club
end
and my migrations file for the Sponsor model looks like this:
class CreateSponsors < ActiveRecord::Migration[5.1]
def change
create_table :sponsors do |t|
t.text :name
t.text :url
t.text :imgUrl
t.references :club, foreign_key: true
t.timestamps
end
add_reference :matches, :sponsor, index: true
add_foreign_key :matches, :sponsor
end
end
I have no problems retrieving sponsors for each club instance but I'm having trouble retrieving the sponsors associated with each match.
In my matches_controller.rb I have this
def show
#match = Match.find(params[:id])
render :json => #match.to_json(:include => [:players, :sponsors])
end
But when I try to run it the script fails. with the error message "no such column: sponsors.match_id" as the script tries to run the following SQL statement
SELECT "sponsors".* FROM "sponsors" WHERE "sponsors"."match_id" = ?
What I'd really like it to do would be to run the following statement
SELECT "sponsors".*
FROM "sponsors"
LEFT JOIN "matches"
ON "matches"."sponsor_id" = "sponsors"."id"
WHERE "matches"."id" = ?
And placing the resulting array into the output JSON's "sponsors" attribute.
I have been looking into the different Active Record association types and I feel like the type of association I need for this task is looser than the ones described in the documentation.
You need many-to-many relationship. In rails where are 2 ways to do this. You can read about this here. In general you will need to add has_and_belongs_to_many to Sponsor and to Match. And create 'join-model' which will contain match_id + sponsor_id. In this way ActiveRecord will be able to create suitable SQL query due to 'join-table'.
So. I have users and movies. Users have watched some movies and not others. I want to express this relationship something like this:
Note:
Not sure if it matters; but movies don't have to be connected to a user; they can exist independently (i.e. Movie 1 has no relationship to User 2). Users can also exist independently; they don't have to have watched or unwatched movies (not pictured here, but you get the idea)
One movie can be watched by one user but unwatched by another (grey vs. black connections)
My initial reaction is that this a has_many :through relationship, something like:
/models/user.rb:
def User
has_many :movies, :through => :unwatched_movies
has_many :movies, :through => :watched_movies
end
/models/movie.rb:
def Movie
has_many :users, :through => :unwatched_movies
has_many :users, :through => :watched_movies
end
But first of all, that code definitely doesn't work...
I want to be able to query for, say, u.unwatched_movies (where u is an instance of User, which doesn't seem to jive with the above.
I have a feeling this has something to do with :source or :as... but I'm feeling a little lost. Am I right in thinking that this is a 3-level hierarchy, where I need models for User, UnwatchedMovieList/WatchedMovieList, and Movie? This question feels very close but I can't seem to make it work in this context.
Any help on how to write these models and migrations would be super helpful. Thank you!
You're trying to create a relationship of omission - "unmatched movies". Which isn't a good idea, you should build up a history of movies watch (which is watched_movies) but then for unwatched you would want to find all movies minus watched movies. Then stick it in a function in User, like so:
def unwatched_movies
Movie.where("id NOT IN ?", self.watched_movies.collect(&:movie_id))
end
Here is my solution
Create these models
class User < ActiveRecord::Base
has_many :user_movies
# Use a block to add extensions
has_many :movies, through: :user_movies, source: 'movie' do
# this is an extension
def watched
where('user_movies.watched = ?', true)
end
def unwatched
where('user_movies.watched = ?', false)
end
end
end
class Movie < ActiveRecord::Base
has_many :user_movies
has_many :watchers, through: :user_movies, source: :user do
# users who is an effective watcher
def watchers
where('user_movies.watched = ?', true)
end
# users how marked it but did not watch it yet
def markers
where('user_movies.watched = ?', false)
end
end
end
class UserMovie < ActiveRecord::Base
belongs_to :user
belongs_to :movie
end
class CreateUserMovies < ActiveRecord::Migration
def change
create_table :user_movies do |t|
t.belongs_to :user, index: true
t.belongs_to :movie, index: true
t.boolean :watched, default: false, null: false
t.timestamps null: false
end
add_foreign_key :user_movies, :users
add_foreign_key :user_movies, :movies
end
end
then for queries
#user = User.first
#user.movies.watched
#user.movies.unwatched
#movie = Movie.first
#movie.watchers.watchers
#movie.watchers.markers
The following set of associations should cover your use case of being able to explicitly mark movies watched and unwatched. It makes use of a join table called user_movies that simply contains the following fields: user_id, movie_id, and watched
class User
has_many :unwatched_user_movies, -> { where(watched: false) }, class_name: 'UserMovie'
has_many :unwatched_movies, through: :unwatched_user_movies, class_name: 'Movie'
has_many :watched_user_movies, -> { where(watched: true) }, class_name: 'UserMovie'
has_many :watched_movies, through: :watched_user_movies, class_name: 'Movie'
end
class UserMovie
belongs_to :movie
belongs_to :user
end
class Movie
has_many :user_movies
end
I have two models that can have tags added to them.
Player
Ticket
and I have a Tag model which belongs to both so I have two join models
tag_ticket
tag_player
I am getting a Could not find the association :tag_tickets in model Ticket error but my association is in there.
class Ticket < ActiveRecord::Base
has_many :tag_tickets
has_many :tags, :through => :tag_tickets
end
I'm just focusing on the Ticket model but the player model should look similar.
this is my migration for TagTicket
class CreateTagTickets < ActiveRecord::Migration
def change
create_table :tag_tickets do |t|
t.integer :ticket_id
t.integer :tag_id
t.timestamps
end
end
end
You need to specify the :tag_tickets join first like this:
class Ticket < ActiveRecord::Base
has_many :tag_tickets
has_many :tags, :through => :tag_tickets
end
You would also need to specify the joins in your TagTicket model:
class TagTicket < ActiveRecored::Base
belongs_to :ticket
belongs_to :tag
end
Alternatively, you can skip all this and use a habtm join (only recommended if the tag_tickets join is truly only used as a join and has no primary key for itself). In this case you would have no TagTicket model (just a tag_tickets table) and the Ticket model would look like this:
class Ticket < ActiveRecord::Base
has_and_belongs_to_many :tags
end
I have two tables:
books (id, name, desc, instance_id)
instances (id, domain)
A user should ONLY be able to see data that is assigned to their instance_id in records...
For the books, model, to accomplish this, I'm thinking about using a default scope.. Something like:
class Books < ActiveRecord::Base
attr_accessible :name, :description
belongs_to :user
default_scope :order => 'books.created_at DESC'
AND books.instance_id == current.user.instance_id
end
Any thoughts on that idea? Also how can I write that 2nd to last line for Rails 3? 'AND books.instance_id == current.user.instance_id'
Thanks
It's not a good idea to access the current user inside the model. I would implement this as follows:
class Instance < ActiveRecord::Base
has_many :users
has_many :books
end
class User < ActiveRecord::Base
belongs_to :instance
has_many :books, :order => "created_at DESC"
has_many :instance_books, :through => :instance, :source => :books,
:order => "created_at DESC"
end
class Book < ActiveRecord::Base
belongs_to :user
belongs_to :instance
end
List of Books associated with the user instance:
current_user.instance_books
List of Books created by the user:
current_user.books
Creating a new book:
current_user.books.create(:instance => current_user.instance, ..)
Note:
Your book creation syntax is wrong. The build method takes hash as parameter. You are passing two arguments instead of one.
user.books.build(params[:book].merge(:instance => current_user.instance}))
OR
user.books.build(params[:book].merge(:instance_id => current_user.instance_id}))
I have the following 3 models and relationship between them:
class Calendar < ActiveRecord::Base
has_many:fiscalcalendars
has_many:voucherdatas ,:through => :fiscalcalendars
end
class Fiscalcalendar < ActiveRecord::Base
belongs_to :calendar
has_many :voucherdatas
end
class Voucherdata < ActiveRecord::Base
has_many :fiscalcalendars
has_many :calendars, :through => :fiscalcalendars
end
fields in calendar : id,monthname
fields in fiscalcalendar :id, calendar_id,fiscalyearweek
fields in voucherdata :id, vhour, fiscalyear week
I want the sum of the vhour for each month
I can get it to group by fiscal week by doing
Voucherdata.sum(:vhour,:group=>:fiscalyearweek)
is there a simpler way to get it by month?
This should do what you're asking for, assuming I understand your database relationship.
Voucherdata.sum(:vhour, :joins => :calendars, :group=> 'calendars.monthname)
However this statement won't work without a little modification. You're not telling Rails how to link Voucherdata and Fiscalcalendar. With two :has_many relationships Rails doesn't know where to find the foreign key to link to the other one.
You need to make a join model and either make it a :has_many, :through relationship or use a :has_and_belongs_to_many relationship. Once you've set that up the above statement will work without modification.
Corrected model relationship and migration required. Using a :has_and_belongs_to_many relationship (cleaner syntax):
class Calendar < ActiveRecord::Base
has_many:fiscalcalendars
has_many:voucherdatas ,:through => :fiscalcalendars
end
class Fiscalcalendar < ActiveRecord::Base
belongs_to :calendar
has_and_belongs_to_many :voucherdatas
end
class Voucherdata < ActiveRecord::Base
has_many :calendars, :through => :fiscalcalendars
has_and_belongs_to_many :fiscalcalendars
end
class CreateFiscalcalendarsVoucherdatasJoinTable ActiveRecord::Migration
def self.up
create_table :fiscalcalendars_voucherdatas :id => false do |t|
t.integer :fiscalcalendar_id
t.integer :voucherdata_id
end
end
def self.down
drop_table :fiscalcalendars_voucherdatas
end
end