Could I add an association based on another association? - ruby-on-rails

My User model looks like:
User
habtm :Roles
Role
habtm :Users
RoleExtension
belongs_to :Role
mysql tables:
users
id
..
roles
id
..
roles_users
user_id
role_id
role_extensions
id
role_id
feature_id
..
..
Now everything seems to be working fine so far.
Now I want the User model to have a collection of RoleExtensions, based on the habtm Roles collection.
example:
user = User.find(1)
user.Roles (returns roles with id's of 1,2,3)
So I want:
user.RoleExtensions
to return all Role extensions that have role_id in (1,2,3)

Normally you'd use a has_many, :through association, but that doesn't apply to has_and_belongs_to_many relations.
So, instead, in your User model:
def role_extensions
return roles.inject([]) do |array, role|
role.role_extensions do |re|
array.include?(re) ? array << re : array
end
end
end
Then my_user.role_extensions should return an array of all role extensions belonging to all the user's roles.
Note: I haven't tested this, but it should work
UPDATE: I like this better
def role_extensions
return roles.inject([]) { |array, role| array << role.role_extensions }.flatten!.uniq
end

user = User.find(1)
RoleExtension.find(:all, :conditions => ["role_id IN (?)", user.role_ids])
Otherwise you can use nested joins.

Try this -
# Fetch user object
user = User.first
# If you want roles of that user try this
roles = user.roles
# You can map all the role extensions of that user by
role_extensions = user.roles.map(&:role_extensions).uniq
Be aware that this will be extremely slow for large number of roles. In that case better write your own query method. Something like
role_extensions = RoleExtension.where("role_id in (?)", user.role_ids).all

#user.role_extensions.where(:joins => :roles)

Related

Rails: Any way to get multiple first/last records of one-many models?

I have a User model, and User model has_many :roles.
Say I have 1000 Users' record in the database, is there a way to obtain the first/last record of the roles for each User's record? i.e what I would like to achieve is to retrieve user.roles.first or user.roles.last for each User's record.
I could do something like
User.all.each do |u|
puts u.roles.first # or u.roles.last
end
but in this case, the code would loop 1000 times. Is there any simpler (and elegant) way to achieve the same purpose?
You can improve your query like this way. Still have to loop over users.
User.includes(:roles).find_each do |user|
puts user.roles[0] #First Role
puts user.roles[-1] #Last Role
end
If you dont want to do a loop, you can create foreign keys for first and last Role in your "users" table and update them every time a new role is created. The query should be like
User.includes(:first_role, :last_role)
i assume that user-roles is a one-many relationship, then you can get first/last role each user with one query (postgres/mysql):
class Role < ApplicationRecord
belongs_to :user
scope :first_each_user, ->(sort) {
from(
<<~SQL
(
SELECT roles.*,
row_number() OVER (
PARTITION BY roles.user_id
ORDER BY roles.created_at #{sort.to_s}
) AS rn
FROM roles
) roles
SQL
).where("roles.rn = 1")
}
end
# first
Role.first_each_user(:asc)
# last
Role.first_each_user(:desc)

Add attribute to Rails 5 model, which not exists in the database

I have tables - users and articles. It is one to many relation or user can have many articles. What I want is to select all users from users table with LIMIT 40, but include count all articles for each user - add property for each user count_articles. I am trying in this way:
def self.get_users(limit, offset)
users = []
order('created_at').limit(limit).offset(offset).each do |user|
user.attributes[:count_articles] = user.articles.count
users << user
byebug
end
users
end
, but I am getting users without this attribute. If I use byebug and type in the
console - user.count_articles, I can see the result of count_articles for current user, but if I type only user, I see all the attributes without count_articles.
You could try with:
User.joins(:articles)
.select('users.id, users.name, COUNT(articles.id) AS count_articles')
.group('users.id')
.limit(40)
I'm doing s.th. similar with a sub-select. So no join is required.
Try putting this into your User Model:
def self.get_users(limit=40, offset=0)
order(:created_at)
.limit(limit)
.offset(offset)
.select('users.*, (SELECT COUNT(id) FROM articles WHERE articles.id = users.id) articles_count')
end
The resulting User instances in the returned Array will then have the attribute "articles_count".

Filter table on attribute of first result from joined table

I have two tables users and task_lists, users has_many task_lists.
task_lists belongs to users and has an attribute tasks_counter.
users
|id|
task_lists
|id|user_id|tasks_counter|
I would like to find all the users whose first (MIN(id)) tasks_list has a tasks_counter < 5.
How would I achieve this in PostGreSQL? I'm using Rails, if somebody knows a solution using ActiveRecords.
This will set users_ids variable with an Array containing all User id's whose first TaskList has a tasks_counter < 5:
user_ids = TaskList.select("MIN(id) AS id, user_id, tasks_counter")
.group(:user_id) # Get first TaskList for each user
.select { |t| t.tasks_counter < 5 } # Keep users tha meet criteria
.pluck(:user_id) # Return users' id in array
If you would like to get an ActiveRecord_Relation object with User objects you can use the result from the previous query and filter User.
users = User.where(id: user_ids)
Or, everything in one line:
users = User.where(id: TaskList.select("MIN(id) AS id, user_id, tasks_counter")
.group(:user_id)
.select { |t| t.tasks_counter < 5 }
.pluck(:user_id))

Query optimization in associated models

I have a User model
class User < ActiveRecord::Base
has_many :skills
has_one :profile
end
Profile table has two columns named, age & experience
Now, I've a search form where the parameters are passed are:
params[:skill_ids] = [273,122,233]
params[:age] = "23"
params[:experience] = "2"
I've to search through all the users where user's skills meet any of the params[:skill_ids] and also from the user's profile, their age and experience.
Do I have to go through a loop like:
users = []
User.all.each do |user|
if (user.skills.collect{|s| s.id} & params[:skill_ids] ) > 0
// skip other parts
users << user
end
end
or, any of you have any better solution?
Because your skills belong to exactly one user, you could first fetch all users belonging to the skill ids provided and filter them by the other criteria:
matching_users = User.includes(:skills, :profile)
.where(["skills.id in (?) AND profile.age = ? AND profile.experience = ?",
params[:skill_ids],
params[:age].to_i,
params[:experience].to_i]
)
Try this:
#users = User.includes(:skills).where(["skills.id in (?)", params[:skill_ids]]).all

Self-referential find in controller count relations

I'm having real trouble pulling out a set of records that are self-referentially related to a user in order to show these on a user's 'show' page.
Here's the idea:
Users (current_user) rate the compatibility between two other users (user_a and user_b). They can rate compatibility either positively or negatively: rating two users "compatible" creates a positive_connection between user_a and user_b, and rating them "incompatible" creates a negative_connection. So there are models for positive_connection, negative_connection and user.
Now I need to display only users that are overall_positively_connected_to(#user) (i.e. where positive_connections_to(#user).count > negative_connections_to(#user).count).
This is where I've got to, but I can't get any further:
User model:
def overall_positive_connected_to(user)
positive_connections_to(user).count > negative_connections_to(user).count
end
def positive_connections_to(user)
positive_connections.where("user_b_id = ?", user)
end
def negative_connections_to(user)
negative_connections.where("user_b_id = ?", user)
end
Controller
#user.user_bs.each do |user_b|
if user_b.overall_pos_connected_to(#user)
#compatibles = user_b
end
end
The code in the controller is clearly wrong, but how should I go about doing this? I'm completely new to rails (and sql), so may have done something naive.
Any help would be great.
So am I right in saying you have 3 models
User (id, name)
PositiveConnection (user_a_id, user_b_id)
NegativeConnection (user_a_id, user_b_id)
Or something of that sort.
I think you just want 2 models
and for convenience I'm going to rename the relations as "from_user" and "to_user"
User (id, name)
Connection (value:integer, from_user_id, to_user_id)
Where value is -1 for a negative
and +1 for a positive.
Now we can have do something like
(note: you need to sort out the exact syntax, like :foreign_key, and :source, and stuff)
class User
has_many :connections, :foreign_key => "from_user_id"
has_many :connected_users, :through => :connections, :source => :to_user
def positive_connections
connections.where(:value => 1)
end
def negative_connections
...
end
end
But we also now have a framework to create a complex sql query
(again you need to fill in the blanks... but something like)
class User
def positive_connected_users
connected_users.joins(:connections).group("from_user_id").having("SUM(connections.value) > 0")
end
end
this isn't quite going to work
but is kind of pseudo code for a real solution
(it might be better to think in pure sql terms)
SELECT users.* FROM users
INNER JOIN connections ON to_user_id = users.id
WHERE from_user_id = #{user.id}
HAVING SUM(connections.value) > 0

Resources