How to create Rails models with multiple complex associations/joins? - ruby-on-rails

I am trying to figure out how to create ActiveRecord models with associations that can yield the same results as this SQL query:
SELECT login, first_name, last_name, email_address
FROM accounts
INNER JOIN people ON person.id = accounts.person_id
INNER JOIN email_address_people ON person.id = email_address_people.person_id
INNER JOIN email_addresses ON email_address.id = email_address_people.email_address_id
INNER JOIN email_address_types ON email_address_types.id = email_address_people.email_address_type_id
WHERE email_address_types.email_address_type = 'account';
The table structure is as follows, and assumes each table has an id per normal ActiveRecord convention:
accounts
id : int
person_id : int
login : string
people
id : int
first_name : string
last_name : string
email_address_people
id : int
person_id : int
email_address_id : int
email_address_type_id : int
email_addresses
id : int
email_address : string
email_address_types
id : int
email_address_type: string
I need the models to be fully functional, and not limited by things like :find_by_sql.
How do I create the associated models that make this possible?
Thanks!
Chris Benson
chris#chrisbenson.com

Try this:
Your model classes:
class EmailAddress < ActiveRecord::Base
end
class EmailAddressType < ActiveRecord::Base
end
class People < ActiveRecord::Base
has_many :accounts
has_many :email_address_people
has_many :email_addresses, :through => :email_address_people
has_many :account_email_address_people,
:class_name => "EmailAddressPeople",
:conditions => "email_address_type = 'account'"
has_many :account_email_addresses,
:through => :account_email_address_people
end
class EmailAddressPeople < ActiveRecord::Base
belongs_to :person
belongs_to :email_address
belongs_to :email_address_type
end
Your account model:
class Account < ActiveRecord::Base
belongs_to :person
# now to the actual method
def account_emails
person.account_email_addresses.map do |email|
[login, person.first_name, person.last_name, email.email_address]
end
end
# Brute force SQL if you prefer
def account_emails2
sql = "YOUR SQL HERE"
self.connection.select_values(sql)
end
end
Assuming you have the Account object in hand account.account_emails makes two database calls:
Get the person using a id
Get the account emails for the person
Going directly to the database(i.e. account.account_emails2) is the fastest option, but it is not the Rails way.

I think the best thing to do here is to give you the documentation first: http://railsbrain.com/api/rails-2.3.2/doc/index.html
Look up "has_many" (paying attention to :through) and "belongs_to", as well as "has_one", although I don't think you'll use the later.
This blog post will help you with the has_many :through concept -- and I think after that, you'll be set. Let us know if there's anything that's not clear!
class Account < ActiveRecord::Base
belongs_to :person
end
class Person < ActiveRecord::Base
has_many :accounts
has_many :email_addresses :through => :email_address_people
end
class EmailAddress < ActiveRecord::Base
belongs_to :email_address_type
belongs_to :person
has_one :email_address_type
end
class EmailAddressType < ActiveRecord::Base
has_many :email_addresses :through => :email_address_people
end
I would get started with that. It's not tested, but if we see what breaks, then we can fix it.. :)

Related

Association not working

I have three models:
Department
class Department < ActiveRecord::Base
has_many :patients, :dependent => :destroy
has_many :waitingrooms, :dependent => :destroy
end
Waitingroom with fields patient_id:integer and department_id:integer
class Waitingroom < ActiveRecord::Base
belongs_to :patient
end
Patient with department_id:integer
class Patient < ActiveRecord::Base
belongs_to :department
has_many :waitingrooms
end
I save a waitingroom after a patient was in the waitingroom! So now i tried to retrieve the patients who where in the the waitingroom of the department:
def index
#waited = #current_department.waitingrooms.patients
end
Somehow it didnt worked it returned this error:
undefined method `patients' for #<ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_Waitingroom:0x374c658>
But this worked: What did i wrong? Thanks!
def index
#waited = #current_department.waitingrooms
end
You can't invoke an association on a collection. You need to invoke it on a specific record. If you want to get all the patients for a set of waiting rooms, you need to do this:
def index
rooms = #current_department.waitingrooms
#waited = rooms.map { |r| r.patients }
end
If you want a flat array, you could (as a naive first pass) use rooms.map { |r| r.patients }.flatten.uniq. A better attempt would just build a list of patient ids and fetch patients once:
#waited = Patient.where(id: rooms.pluck(:patient_id).uniq)

how to find out active record property through many to many

I'm currently adjusting fedena to have a many:many relationship between students and guardians (as opposed to one:many student:guardians).
So this is what I did:
class Guardian < ActiveRecord::Base
has_many :parentings, :dependent=>:destroy
has_many :students, :through=>:parentings
end
class Student < ActiveRecord::Base
has_many :parentings, :dependent=>:destroy
has_many :guardians, :through=>:parentings
end
class Parenting < ActiveRecord::Base
attr_accessible :student_id, :guardian_id
belongs_to :student
belongs_to :guardian
end
inside guardian.rb there was this class method:
def self.shift_user(student)
# find all the guardians having a ward_id = student.d (comment my own)
self.find_all_by_ward_id(student.id).each do |g|
..
end
I want to change it using the newly defined relationshop ie
self.find_all_by_student_id(student.id).each do |g|
..
It doesn't work! I thought it would work since I've already defined that a Guardian has many students through the Parenting class.. I've tried several permutations of the command above and I keep on getting the error:
undefined method `find_all_by_student_id' for #<Class:0x1091c6b28>
ideas? I'm using ruby 1.8.7 and RoR 2.3.5
Guardian has no propety student_id so there is no method find_all_by_student_id. So I don't understand why you are confused. Why don't you just use student.guardians?
You can do this using a named scope and a more complex query
class Guardian < ActiveRecord::Base
has_many :parentings, :dependent=>:destroy
has_many :students, :through=>:parentings
named_scope :find_all_by_student_id, lambda {|student_id|
{ :all,
:select => "guardians.*",
:joins => "JOIN parentings ON parentings.guardian_id = guardians.id
JOIN students ON students.id = parentings.student_id",
:conditions = ["students.id = ?", student_id] } }
end

Rails 3 Three Models Join

I don't seem to get this right for some reason, though it sounds simple :/
I have three models :
User(id,name,email)
Skill(id,name,description)
UserSkill(user_id,skill_id,level)
How can i get all skills of a certain user, whether he or she has discovered them or not ?
For example, 3 skills (walk, talk, write). 3 users (John, Mary, Jack).
If Mary walks and writes, how can i get it back as a result like :
Mary => {Skill: walk(includes UserSkill), Skill : talk, Skill : write(includes UserSkill) }
You get the idea :)
Try this:
class User
def skill_list
Skill.all(
:select =>"skills.*, A.user_id AS user_id",
:joins => "LEFT OUTER JOIN user_skills A
ON A.skill_id = skills.id
AND A.user_id = #{id}").map do |skill|
skill.name + (skill.user_id.nil? ? "" : "(*)")
end
end
end
Now
user = User.find_by_name("Mary")
user.skill_list
Will print:
[
walk(*),
talk,
write(*)
]
I'm assuming you want to set something up like this:
class User < ActiveRecord::Base
has_many :user_skills
has_many :skills, :through => :user_skills
end
class Skill < ActiveRecord::Base
has_many :user_skills
has_many :users, :through => :user_skills
end
class UserSkill < ActiveRecord::Base
belongs_to :user
belongs_to :skill
end
then you can do:
my_user.skills # returns all Skill records assigned to the user
my_user.user_skills.includes(:skill) # this allows you to access :level in addition to Skill attributes
So the way to get both skills and user_skills is to use the :user_skills association. Basic has_many :through. Am I missing something?
user = User.first
user.user_skills.all.map(&:skills)

Rails: order using a has_many/belongs_to relationship

I was wondering if it was possible to use the find method to order the results based on a class's has_many relationship with another class. e.g.
# has the columns id, name
class Dog < ActiveRecord::Base
has_many :dog_tags
end
# has the columns id, color, dog_id
class DogTags < ActiveRecord::Base
belongs_to :dog
end
and I would like to do something like this:
#result = DogTag.find(:all, :order => dog.name)
thank you.
In Rails 4 it should be done this way:
#result = DogTag.joins(:dog).order('dogs.name')
or with scope:
class DogTags < ActiveRecord::Base
belongs_to :dog
scope :ordered_by_dog_name, -> { joins(:dog).order('dogs.name') }
end
#result = DogTags.ordered_by_dog_name
The second is easier to mock in tests as controller doesn't have to know about model details.
You need to join the related table to the request.
#result = DogTag.find(:all, :joins => :dog, :order => 'dogs.name')
Note that dogs is plural in the :order statement.

Using ActiveRecord belongs_to with two keys

I have two ActiveRecord models with a hasMany / belongsTo association:
class User < ActiveRecord::Base
has_many :letters
end
class Letter < ActiveRecord::Base
belongs_to :user
end
The User model has a revision_number attribute, to which I would like to scope the belongs_to association, so the letter is associated to a User by both user.id and user.revision_number.
I tried using the :conditions key as documented in the API docs:
class Letter < ActiveRecord::Base
belongs_to :user, :conditions => "revision_number = #{client_revision}"
end
but this attempts to call client-revision on the Letter class, not the instance of Letter. Could anyone point me in the right direction for scoping the belongs_to association correctly?
I'm using the acts-as-revisable plugin to version the User model.
I am having a hard time understanding why you would want to scope the belongs_to in this way. Correct me if I am wrong, but it might be better to do something like this. I am assuming you want some sort of version control system:
class User < ActiveRecord::Base
has_many :letters
end
class Letter < ActiveRecord::Base
has_many :revisions, :class_name => "LetterVersion"
belongs_to :current, :class_name => "LetterVersion"
end
class LetterVersion < ActiveRecord::Base
belongs_to :letter
end
Finally figured out what I needed was something like composite keys, which Rails ActiveRecord doesn't support. The solution (for now at least) was to write custom client accessors on the Letter to support the composite keys (id and revision_number):
class Letter < ActiveRecord::Base
def client
Client.find_by_id(self.client_id).try(:find_revision, self.client_revision)
end
def client=(c)
self.client_id = c.id
self.client_revision = c.revision_number
end
end
class Client < ActiveRecord::Base
acts_as_revisable
has_many :letters
end
With this setup, Client#1.letters will retrieve an array of both letters, but Letter#2.client will retrieve Client#1r2, whilst Letter#2.client will retrieve Client#1r4:
Client id: 1 1 1 1 1 1
rev_number: 1 2 3 4 5 6
Letter id: 1 2
client_id: 1 1
client_revision: 2 5
Still not sure if this is the best approach to this problem, but it seems to work for now.

Resources