Rails 3 Three Models Join - ruby-on-rails

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)

Related

Rails & Active Record: how to join tables that are only related through another table?

I am trying to build an active record query using through table associations. Here are my models:
Event.rb:
has_many :event_keywords
User.rb:
has_many :user_keywords
Keyword.rb:
has_many :event_keywords
has_many :user_keywords
EventKeyword.rb:
belongs_to :event
belongs_to :keyword
UserKeyword.rb:
belongs_to :user
belongs_to :keyword
I am trying to build an Event scope that takes a user_id as a param and returns all the Events with shared keywords. This was my attempt but it's not recognizing the user_keywords association:
scope :with_keywords_in_common, ->(user_id) {
joins(:event_keywords).joins(:user_keywords)
.where("user_keywords.user_id = ?", user_id)
.where("event_keywords.keyword_id = user_keywords.keyword_id")
}
How do I resolve this?
Something like this might work. 2-step process. First, get all user's keywords. Then find all events with the same keyword.
scope :with_keywords_in_common, ->(user) {
joins(:event_keywords).
where("event_keywords.keyword_id" => user.user_keywords.pluck(:id))
}
The database seems to be overkill here and firstly I'd simplify by making keywords polymorphic, this would get rid of 2 of your tables here (event_keywords, and user_keywords).
Your associations would then look like this:
# Event.rb:
has_many :keywords, as: keywordable
# User.rb:
has_many :keywords, as: keywordable
# Keyword.rb:
belongs_to :keywordable, polymorphic: true
And finally, your scope:
scope :with_keywords_in_common, -> (user_id) do
joins(:keywords)
.where('keywords.keywordable_type = User AND keywords.word IN (?)', keywords.pluck(:name))
end

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 find all associated records has_many :through

I would like to list all posts that are connected with some specific category and classroom.
I have:
class Post < ActiveRecord::Base
has_many :category_posts
has_many :categories, :through => :category_posts
has_many :classroom_posts
has_many :classrooms, :through => :classroom_posts
end
class Category < ActiveRecord::Base
has_many :category_posts
has_many :posts, :through => :category_posts
end
class CategoryPost < ActiveRecord::Base
belongs_to :category
belongs_to :post
end
class Classroom < ActiveRecord::Base
has_many :classroom_posts
has_many :posts, :through => :classroom_posts
end
class ClassroomPost < ActiveRecord::Base
belongs_to :classroom
belongs_to :post
end
And I wanna do something like this
Post.where(["category.id = ? AND classroom.id = ?", params[:category_id], params[:classroom_id]])
It indeed is very simple task, but I don't know what I should be looking for (keywords).
It's the same problem like this, but in rails.
EDIT:
I added more details to the question.
This works, but only if I have both params specified. Witch is not always the case - I dont know what params would be specified.
Post.joins(:categories, :classrooms).where(["categories.id = ? AND classrooms.id = ?", params[:classroom_id], params[:category_id]])
Category.find(params[:category_id]).posts
Also take a look at the guides:
Guides for Rails 2.35
Guides for Rails 3.0
Here is what I would do in Rails 3:
In post.rb:
def self.in_category(category_id)
if category_id.present?
join(:category_posts).where(category_posts: {category_id: category_id})
else
self
end
end
def self.in_classroom(classroom_id)
if classroom_id.present?
join(:classroom_posts).where(classroom_posts: {classroom_id: category_id})
else
self
end
end
I do not join Classroom or Category since it makes more work for DBMS and this is not required.
Now, you can do:
Post.in_category(params[:category_id]).in_classroom(params[:classroom_id])
I haven't tested it though. So do not hesitated to ask if needed.
I think that should work:
Post.joins(:category_posts, :classroom_posts)
.where(
["category_posts.category_id = ?
AND classroom_posts.classroom_id = ?", params[:category_id], params[:classroom_id]])
This will traslate to a SQL like :
SELECT
p.*
FROM
posts AS p
INNER JOIN
category_posts AS cap ON cap.id = p.category_posts_id
INNER JOIN
classroom_posts AS clp ON clpid = p.classroom_posts_id
WHERE
cap.category_id = '1' AND clp.classroom_id = '1'
;
As to whether to use :include or joins on Post look at this answer on stackoverflow.
Sounds like you need an if statment.
if params[:category_id] && params[:classroom_id]
Post.joins(:categories, :classrooms).where("classrooms.id" => params[:classroom_id], "categories.id" => params[:category_id]])
elsif params[:category_id]
Category.find(params[:category_id]).posts
else
Classroom.find(params[:classroom_id]).posts
end

Rails model relations depending on count of nested relations

I am putting together a messaging system for a rails app I am working on.
I am building it in a similar fashion to facebook's system, so messages are grouped into threads, etc.
My related models are:
MsgThread - main container of a thread
Message - each message/reply in thread
Recipience - ties to user to define which users should subscribe to this thread
Read - determines whether or not a user has read a specific message
My relationships look like
class User < ActiveRecord::Base
#stuff...
has_many :msg_threads, :foreign_key => 'originator_id' #threads the user has started
has_many :recipiences
has_many :subscribed_threads, :through => :recipiences, :source => :msg_thread #threads the user is subscribed to
end
class MsgThread < ActiveRecord::Base
has_many :messages
has_many :recipiences
belongs_to :originator, :class_name => "User", :foreign_key => "originator_id"
end
class Recipience < ActiveRecord::Base
belongs_to :user
belongs_to :msg_thread
end
class Message < ActiveRecord::Base
belongs_to :msg_thread
belongs_to :author, :class_name => "User", :foreign_key => "author_id"
end
class Read < ActiveRecord::Base
belongs_to :user
belongs_to :message
end
I'd like to create a new selector in the user sort of like:
has_many :updated_threads, :through => :recipiencies, :source => :msg_thread, :conditions => {THREAD CONTAINS MESSAGES WHICH ARE UNREAD (have no 'read' models tying a user to a message)}
I was thinking of either writing a long condition with multiple joins, or possibly writing giving the model an updated_threads method to return this, but I'd like to see if there is an easier way first. Am I able to pass some kind of nested hash into the conditions instead of a string?
Any ideas? Also, if there is something fundamentally wrong with my structure for this functionality let me know! Thanks!!
UPDATE:
While I would still appreciate input on better possibilities if they exist, this is what I have gotten working now:
class User < ActiveRecord::Base
# stuff...
def updated_threads
MsgThread.find_by_sql("
SELECT msg_threads.* FROM msg_threads
INNER JOIN messages ON messages.msg_thread_id = msg_threads.id
INNER JOIN recipiences ON recipiences.msg_thread_id = msg_threads.id
WHERE (SELECT COUNT(*) FROM `reads` WHERE reads.message_id = messages.id AND reads.user_id = #{self.id}) = 0
AND (SELECT COUNT(*) FROM recipiences WHERE recipiences.user_id = #{self.id} AND recipiences.msg_thread_id = msg_threads.id) > 0
")
end
end
Seems to be working fine!
Also to check if a specific thread (and message) are read:
class Message < ActiveRecord::Base
# stuff...
def read?(user_id)
Read.exists?(:user_id => user_id, :message_id => self.id)
end
end
class MsgThread < ActiveRecord::Base
# stuff...
def updated?(user_id)
updated = false
self.messages.each { |m| updated = true if !m.read?(user_id) }
updated
end
end
Any suggestions to improve this?
Add a named_scope to the MsgThread model:
class MsgThread < ActiveRecord::Base
named_scope :unread_threads, lambda { |user|
{
:include => [{:messages=>[:reads]}, recipiencies],
:conditions => ["recipiences.user_id = ? AND reads.message_id IS NULL",
user.id],
:group => "msg_threads.id"
}}
end
Note: Rails uses LEFT OUTER JOIN for :include. Hence the IS NULL check works.
Now you can do the following:
MsgThread.unread_threads(current_user)
Second part can be written as:
class Message
has_many :reads
def read?(usr)
reads.exists?(:user_id => usr.id)
end
end
class MsgThread < ActiveRecord::Base
def updated?(usr)
messages.first(:joins => :reads,
:conditions => ["reads.user_id = ? ", usr.id]
) != nil
end
end
You might want to take a look at Arel, which can help with complex SQL queries. I believe (don't quote me) this is already baked into Rails3.

How to create Rails models with multiple complex associations/joins?

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.. :)

Resources