Active Record Table Joins - ruby-on-rails

I have been racking my brain all day and can't get this to work. I am very new to ruby and rails so my apologies for any silly errors.
My problem is I am joining 3 tables together to get a #students object. This works but if I call for example #student.name then 'name' doesn't exist.
Below is my code:
Controller
note I have tried using .includes and .join and the same problem happens.
class MyprojectController < ApplicationController
def show
#project = Project.find(params[:id])
#dateformat = '%b %e - %H:%M'
#user = Student.includes("INNER JOIN researchers ON students.researcher_id = researchers.id
INNER JOIN users ON researchers.user_id = users.id").where('user_id = ?', current_user.id)
end
User Model
class User < ApplicationRecord
include EpiCas::DeviseHelper
has_many :event_registrations
has_many :events, through: :event_registrations
belongs_to :project
has_many :researchers
#has_many :students, :through => :researchers
#has_many :supervisors, :through => :researchers
# def self.authenticate(username)
# where(username: username).first
# end
end
Researcher Model
class Researcher < ApplicationRecord
#belongs_to :project
belongs_to :user
has_many :supervisor
has_many :students
end
Student Model
class Student < ApplicationRecord
#Must have the following
validates :name, :email, :surname, :email, :supervisor, :registration_number, presence: true
#ensures unique email addresses
validates :email, uniqueness: true
#assosiations
belongs_to :researcher
end
So every student has a researcher_id and every researcher has a user_id. So the joins should go student->researcher->user and then I want to be able to use all the attributes from all tables in an #user object.
I tried using Student.join(:researcher, :user) but that tried to do a join from the Student table to the researchers table and then tried to join the user table by using a user_id from the student table (but of the user_id is in the researcher table). So i have just done the query myself.
All the data seems to be there but as 'raw attributes'.
Any help would be greatly appreciated!
-James

Rather than try and join things into one return (like you would in sql) use includes so that you can access all your data in fewer queries but you still have access to your data in objects. The point of using an ORM like ActiveRecord is to be able to access your data using objects. The downside of using an ORM is that sometimes it's not as efficient at getting you the exact data you want, because the data is pushing into objects. Using includes provides a sort of middle ground where you can access the data you require in objects and you don't necessarily have to run queries for each association.
Try something like (depending on how you're getting your user id -- I'm assuming from project):
#user = User.includes(researcher: :student).find(project.user_id)
And then you can access things through the normal rails associations:
researcher = #user.researcher
student = researcher.student
I hope that helps and best of luck!

Related

Rails 5.1 use nested_attributes_for to update attributes on join table

I have three models in my API-only backend - User, Game and Ownership, which acts as a join table between users and games.
I have a service method that calls a remote API to get data on games the user owns. It then uses the data to add relevant games into the database, and update the user with extra data (game_count) and creates the relationships between user and his games. I'm able to accomplish this like so:
class GameService
def self.getGamesForUser(user)
# response hash gets populated here
games = response[:games].map do |data|
Game.find_or_create_by(app_id: data[:appid]) do |g|
g.name = data[:name]
g.icon = data[:img_icon_url]
g.logo = data[:img_logo_url]
end
end
user.update games: games, game_count: response[:game_count]
end
end
So far, so good. While updating the user, the ownership associations between the user and the games are created automatically. But I would also like to add extra attributes to the join table at the same time (like playtime). I haven't been able to find a nice solution to this. I tried looking at nested_attributes_for, by adding accepts_nested_attributes_for :ownerships to the game model, and calling
g.ownerships_attributes = [{
playtime: data[:playtime_forever]
}]
before updating the user, but that seemed to have no effect whatsoever. I feel like there must be an elegant solution to this, I'm just not seeing it.
Here is the code for my models:
User.rb
class User < ApplicationRecord
has_many :ownerships
has_many :games, through: :ownerships
end
Game.rb
class Game < ApplicationRecord
has_many :ownerships
has_many :owners, through: :ownerships, source: :user
end
Ownership.rb
class Ownership < ApplicationRecord
belongs_to :user
belongs_to :game
end
When iterating through games, save playtimes in a hash (with game.id as key)
games = response[:games].map do |data|
playtimes = Hash.new
game = Game.find_or_create_by(app_id: data[:appid]) do |g|
g.name = data[:name]
g.icon = data[:img_icon_url]
g.logo = data[:img_logo_url]
end
#This is outside the find_or_create_by because it must be done for all games, not just the newly created.
playtimes[game.id] = data[:playtime_forever]
end
Finnaly, after updating games, update playtimes in the join table:
user.ownerships.find_each do |own|
own.update_attributes(:playtime => playtimes[:own.game_id])
end

Create if record does not exist

I have 3 models in my rails app
class Contact < ActiveRecord::Base
belongs_to :survey, counter_cache: :contact_count
belongs_to :voter
has_many :contact_attempts
end
class Survey < ActiveRecord::Base
has_many :questions
has_many :contacts
end
class Voter < ActiveRecord::Base
has_many :contacts
end
the Contact consists of the voter_id and a survey_id. The Logic of my app is that a there can only be one contact for a voter in any given survey.
right now I am using the following code to enforce this logic. I query the contacts table for records matching the given voter_id and survey_id. if does not exist then it is created. otherwise it does nothing.
if !Contact.exists?(:survey_id => survey, :voter_id => voter)
c = Contact.new
c.survey_id = survey
c.voter_id = voter
c.save
end
Obviously this requires a select and a insert query to create 1 potential contact. When I am adding potentially thousands of contacts at once.
Right now I'm using Resque to allow this run in the background and away from the ui thread. What can I do to speed this up, and make it more efficient?
You can do the following:
Contact.where(survey_id: survey,voter_id: voter).first_or_create
You should add first a database index to force this condition at the lowest level as possible:
add_index :contacts, [:voter_id, :survey_id], unique: true
Then you should add an uniqueness validation at an ActiveRecord level:
validates_uniqueness_of :voter_id, scope: [:survey_id]
Then contact.save will return false if a contact exists for a specified voter and survey.
UPDATE: If you create the index, then the uniqueness validation will run pretty fast.
See if those links can help you.
Those links are for rails 4.0.2, but you can change in the api docks
From the apidock: first_or_create, find_or_create_by
From the Rails Guide: find-or-create-by
It would be better if you let MySQL to handle it.
Create a migration and add a composite unique key to survey_id, voter_id
add_index :contact, [:survey_id, :voter_id], :unique=> true
Now
Contact.create(:survey_id=>survey, :voter_id=>voter_id)
Will create new record only if there is no duplicates.

How to make a has_many_through association mandatory for one member?

I have the following models :
class City < ActiveRecord::Base
has_many :cities_regions_relationships
has_many :regions, through: :cities_regions_relationships
end
class Region < ActiveRecord::Base
has_many :cities_regions_relationships
has_many :cities, through: :cities_regions_relationships
end
class CitiesRegionsRelationship < ActiveRecord::Base
belongs_to :city
belongs_to :region
validates :city_id, presence: true
validates :region_id, presence: true
end
What I want is to make it impossible to create a city without it linked to a region. However, before attempting that, I have to be able to create a relationship on a city that's not yet saved.
I tried this in the console (with a region already created)
c = City.new(name: "Test")
c.cities_region_relationship.build(region_id: Region.first.id)
c.save
However, this fails because the relationship doesn't have a city_id (which is normal because I didn't save the city yet, it doesn't have an ID).
I could try other ways, but I will always have the same problem : How do I create a relationship on a new object, not yet saved in database ?
If you have other completely different solutions to my initial problem (Forcing cities to have at least one region), don't hesitate to suggest something entirely different.
You do not need to build a cities_region_relationship. By passing region_ids to a new City instance, this will create the cities_region_relationship for you.
You can try in the console:
c = City.new(name: "Test", region_ids: [an array of existing region ids])
c.save
for the validation, you can define a new validate method that checks if self.regions.blank? like mentioned in the SO post in my comment above.

Retrieve data from join table

I am new in RoR and I am trying to write a query on a join table that retrieve all the data I need
class User < ActiveRecord::Base
has_many :forms, :through => :user_forms
end
class Form < ActiveRecord::Base
has_many :users, :through => :user_forms
end
In my controller I can successfully retrieve all the forms of a user like this :
User.find(params[:u]).forms
Which gives me all the Form objects
But, I would like to add a new column in my join table (user_forms) that tells the status of the form (close, already filled, etc).
Is it possible to modify my query so that it can also retrieve columns from the user_forms table ?
it is possible. Once you've added the status column to user_forms, try the following
>> user = User.first
>> closed_forms = user.forms.where(user_forms: { status: 'closed' })
Take note that you don't need to add a joins because that's taken care of when you called user.forms.
UPDATE: to add an attribute from the user_forms table to the forms, try the following
>> closed_forms = user.forms.select('forms.*, user_forms.status as status')
>> closed_forms.first.status # should return the status of the form that is in the user_forms table
It is possible to do this using find_by_sql and literal sql. I do not know of a way to properly chain together rails query methods to create the same query, however.
But here's a modified example that I put together for a friend previously:
#user = User.find(params[:u])
#forms = #user.forms.find_by_sql("SELECT forms.*, user_forms.status as status FROM forms INNER JOIN user_forms ON forms.id = user_forms.form_id WHERE (user_forms.user_id = #{#user.id});")
And then you'll be able to do
#forms.first.status
and it'll act like status is just an attribute of the Form model.
First, I think you made a mistake.
When you have 2 models having has_many relations, you should set an has_and_belongs_to_many relation.
In most cases, 2 models are joined by
has_many - belongs_to
has_one - belongs_to
has_and_belongs_to_many - has_and_belongs_to_many
has_and_belongs_to_many is one of the solutions. But, if you choose it, you must create a join table named forms_users. Choose an has_and_belongs_to_many implies you can not set a status on the join table.
For it, you have to add a join table, with a form_id, a user_id and a status.
class User < ActiveRecord::Base
has_many :user_forms
has_many :forms, :through => :user_forms
end
class UserForm < ActiveRecord::Base
belongs_to :user
belongs_to :form
end
class Form < ActiveRecord::Base
has_many :user_forms
has_many :users, :through => :user_forms
end
Then, you can get
User.find(params[:u]).user_forms
Or
UserForm.find(:all,
:conditions => ["user_forms.user_id = ? AND user_forms.status = ?",
params[:u],
'close'
)
)
Given that status is really a property of Form, you probably want to add the status to the Forms table rather than the join table.
Then when you retrieve forms using your query, they will already have the status information retrieved with them i.e.
User.find(params[:u]).forms.each{ |form| puts form.status }
Additionally, if you wanted to find all the forms for a given user with a particular status, you can use queries like:
User.find(params[:u]).forms.where(status: 'closed')

In has_many :through, what is the correct way to create object inheritance

I've hit something that I don't understand how to model with Rails associations and neither STI nor polymorphism seem to address it.
I want to be able to access attributes from a join table via the collection that's created by has_many :through.
In the code below, this means that I want to be able to access the name and description of a committee position via the objects in the .members collection but as far as I can see I can't do that. I have to go through the original join table.
e.g. modelling a club and it's committee members
class User < ActiveRecord::Base
attr_accessible :full_name,
:email
has_many: committee_positions
has_many: committees, :through => committee_positions
end
class Committee < ActiveRecord::Base
attr_accessible :name
has_many :committee_positions
has_many :members, :through => :committee_positions
end
class CommitteePosition < ActiveRecord::Base
attr_accessible :user_id,
:committee_id,
:member_description,
:role_title
belongs_to :committee
belongs_to :user
end
Assume that each committee position instance has a unique description
i.e. the description is particular to both the member and the committee and so has to be stored on the join table and not with either the user or the club.
e.g.
Committee member: Matt Wilkins
Role: "Rowing club president"
Description: "Beats the heart of the rowing club to his own particular drum"
Is there a way to access the data in the join table via the committee.members collection?
While active record gives us this great alias for going directly to the members, there doesn't seem to be any way to access the data on the join table that created the collection:
I cannot do the following:
rowing_committee.members.find_by_role_title('president').name
Each item in the .members collection is a user object and doesn't seem to have access to either the role or description that's stored in the CommitteePositions join table.
The only way to do this would be:
rowing_committee.committee_positions.find_by_role_title('president').user.name
This is perfectly do-able but is clunky and unhelpful. I feel like the use-case is sufficiently generic that I may well be missing something.
What I would like to access via objects in the committee.members collection
member
- full_name
- email
- role_title (referenced from join table attributes)
- member_description (referenced from join table attributes)
This is only a small thing but it feels ugly. Is there a clean way to instruct the "member" objects to inherit the information contained within the join table?
-------------- addendum
On working through this I realise that I can get half way to solving the problem by simply defining a new class for committee member and referencing that instead of user in the has_many :through relationship. It works a little bit better but is still pretty clunky
class Committee < ActiveRecord::Base
...
has_many :committee_positions
has_many :members,
:through => :committee_positions,
:class_name => 'CommitteeMember'
...
end
class CommitteeMember < User
def description( committee )
self.committees.find_by_committee_id( committee.id ).description
end
def role( committee )
self.committees.find_by_committee_id( committee.id ).description
end
end
Now this is getting closer but it still feels clunky in that the code to use it would be:
committee = Committee.first
president_description = committee.members.find_by_title('president').description( committee )
Is there any way to initialize these objects with the committee they are referencing?
I think you could use some delegation here. In your Committee_Position class:
class Committee_Position < ActiveRecord::Base
attr_accessible :user_id,
:committee_id,
:member_description,
:role_title
belongs_to :committee
belongs_to :user
delegate :name, :email, :to => :user
end
so you could do what you say you want:
rowing_club.committee_members.find_by_role_title('president').name

Resources