I have the following models for my user:
class User < ActiveRecord::Base
has_many :facebook_friendships
has_many :facebook_friends, :through => :facebook_friendships, :source => :friend
def mutual_facebook_friends_with(user)
User.find_by_sql ["SELECT users.* FROM facebook_friendships AS a
INNER JOIN facebook_friendships AS b
ON a.user_id = ? AND b.user_id = ? AND a.friend_id = b.friend_id
INNER JOIN users ON users.id = a.friend_id", self.id, user.id]
end
end
class FacebookFriendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => 'User'
end
If user with id 53 and user with id 97 are friends with each other, then you would have rows [53, 97] and [97, 53] in the facebook_friendships table in the database. Here is the raw sql query that I have come up with to calculate mutual friends:
SELECT users.* FROM facebook_friendships AS a
INNER JOIN facebook_friendships AS b
ON a.user_id = :user_a AND b.user_id = :user_b AND a.friend_id = b.friend_id
INNER JOIN users ON users.id = a.friend_id
I would mutual_friends_with to return a relation instead of an Array. This way, I could chain the result with other conditions such as where(college: 'NYU') and get all of that ActiveRecord goodness. Is there a good way to do this?
Have you tried #find_by_sql?
http://guides.rubyonrails.org/active_record_querying.html#finding-by-sql
If you’d like to use your own SQL to find records in a table you can
use find_by_sql. The find_by_sql method will return an array of
objects even if the underlying query returns just a single record. For
example you could run this query:
Client.find_by_sql("SELECT * FROM clients
INNER JOIN orders ON clients.id = orders.client_id
ORDER clients.created_at desc")
find_by_sql provides you with a simple way of making custom calls to
the database and retrieving instantiated objects.
This is what I use to get mutual friends.
has_many :company_friendships, autosave: true
has_many :company_friends, through: :company_friendships, autosave: true
has_many :inverse_company_friendships, class_name: "CompanyFriendship", foreign_key: "company_friend_id", autosave: true
has_many :inverse_company_friends, through: :inverse_company_friendships, source: :company, autosave: true
def mutual_company_friends
Company.where(id: (company_friends | inverse_company_friends).map(&:id))
end
Related
We have a lot of through relations in a model. Rails correctly joins the relations, however I am struggling in figuring out how to apply a where search to the joined table using active record.
For instance:
class Model
has_one :relation1
has_one :relation2, through: :relation1
has_one :relation3, through: :relation2
end
If all the relations are different models, we easily query using where. The issue arise rails starts aliasing the models.
For instance, Model.joins(:relation3).where(relation3: {name: "Hello"}) wont work, as no table is aliased relation3.
Is it possible using active record, or would I have to achieve it using arel or sql?
I am using rails 6.0.4.
In a simple query where a table is only referenced once there is no alias and the table name is just used:
irb(main):023:0> puts City.joins(:country).where(countries: { name: 'Portugal'})
City Load (0.7ms) SELECT "cities".* FROM "cities" INNER JOIN "regions" ON "regions"."id" = "cities"."region_id" INNER JOIN "countries" ON "countries"."id" = "regions"."country_id" WHERE "countries"."name" = $1 [["name", "Portugal"]]
In a more complex scenario where a table is referenced more then once the scheme seems to be association_name_table_name and association_name_table_name_join.
class Pet < ApplicationRecord
has_many :parenthoods_as_parent,
class_name: 'Parenthood',
foreign_key: :parent_id
has_many :parenthoods_as_child,
class_name: 'Parenthood',
foreign_key: :child_id
has_many :parents, through: :parenthoods_as_child
has_many :children, through: :parenthoods_as_child
end
class Parenthood < ApplicationRecord
belongs_to :parent, class_name: 'Pet'
belongs_to :child, class_name: 'Pet'
end
irb(main):014:0> puts Pet.joins(:parents, :children).to_sql
# auto-formatted edited for readibility
SELECT "pets".*
FROM "pets"
INNER JOIN "parenthoods"
ON "parenthoods"."child_id" = "pets"."id"
INNER JOIN "pets" "parents_pets"
ON "parents_pets"."id" = "parenthoods"."parent_id"
INNER JOIN "parenthoods" "parenthoods_as_children_pets_join"
ON "parenthoods_as_children_pets_join"."child_id" = "pets"."id"
INNER JOIN "pets" "children_pets"
ON "children_pets"."id" =
"parenthoods_as_children_pets_join"."child_id"
For more advanced queries you often need to write your own joins with Arel or strings if you need to reliably know the aliases used.
Given:
class Account < ApplicationRecord
belongs_to :super_account, class_name: 'Account', optional: true, foreign_key: 'account_id'
has_many :sub_accounts, class_name: 'Account'
end
What would be the rails way to find all accounts with no sub_accounts?
Account.left_joins(:sub_accounts)
.where(sub_accounts_accounts: { id: nil })
sub_accounts_accounts is what the joined table is aliased as in the query:
SELECT "accounts".* FROM "accounts"
LEFT OUTER JOIN "accounts" "sub_accounts_accounts"
ON "sub_accounts_accounts"."account_id" = "accounts"."id"
WHERE "sub_accounts_accounts"."id" IS NULL LIMIT $1
.left_joins (aka left outer joins) was introduced in Rails 5. In Rails 4 you need to use .joins with a sql string.
Account.joins(%q{
LEFT OUTER JOIN accounts sub_accounts
ON sub_accounts.account_id = accounts.id
}).where(sub_accounts: { id: nil })
The Rails way is to add :counter_cache to this associations. How to
So you need to add column sub_accounts_count to Account
And add counter_cache: true in model SubAccount
belongs_to :account, counter_cache: true
After this you can just call Account.where(sub_accounts_count: 0) as example.
Okay, I feel like I'm starting to come to SO for every activerecord query I have to write now and I'm starting to drag out my user/pet/parasite metaphor but here we go again.
In the following setup;
class User < ActiveRecord::Base
has_many :pets
has_many :parasites, :through => :pets
end
class Pet < ActiveRecord::Base
has_many :parasites
belongs_to :user
end
class Parasite < ActiveRecord::Base
belongs_to :pet
end
I want to write a search that will return all of the parasites that belong to Bob's cat (i.e. User.name = 'Bob' and Pet.animal = 'Cat').
I realise I can do this with the fairly drawn out and ugly
User.where(:name => 'Bob').first.pets.where(:animal => 'Cat').first.parasites
but I thought there should be a more succinct way of doing this.
All of my attempts to write a join statement to make this happen result in an ActiveRecord::Configuration error so I suspect I am going about this backwards. Once again, this seems like it should be easier than it is.
Thanks.
You try to achieve a has_many through has_many association. This won't work using Rail's eager loading associations.
What you have to do is:
join the users
join the pets
scope users down by username
scope pets down by user_id and the animal field
In ActiveRecord:
Parasite.joins(:pet).joins('INNER JOIN users').where('users.name = ? AND pets.user_id = users.id AND pets.animal = ?', #username, #animal)
Alternatively you can create a named scope:
class Parasite < ActiveRecord::Base
belongs_to :pet
scope :parasites_of, lambda {|owner, animal_type| joins(:pet).joins('INNER JOIN users').where('users.name = ? AND pets.user_id = users.id AND pets.animal = ?', owner, animal_type) }
end
Now you can call Parasite.parasites_of('Bob', 'Cat')
The resulting SQL Query will look like:
SELECT * FROM parasites
INNER JOIN users,
INNER JOIN pets ON pets.id = parasites.pet_id
WHERE
users.name = 'Bob'
AND
pets.user_id = users.id
AND
pets.animal = 'Cat'
(Hint: The .to_sql method will show you the plain SQL query)
I have a set of Active Record models, setup like this:
Account
has_many :account_groups
has_many :groups, :through => :account_groups
Posts
has_many :post_groups
has_many :groups, :through => :post_groups
This is a security configuration, and I need to be able to query Posts that belong to a group that the Account has access to....so, I'd like to be able to say:
#account.posts
And get a filtered query of posts that belong to groups that overlap the groups the account has access to....but I can't seem to figure out the right syntax.
Help.
To clarify, the SQL I'm looking to generate would look like:
SELECT DISTINCT posts.* FROM accounts
JOIN account_groups ON account_groups.account_id = accounts.id
JOIN groups ON groups.id = account_groups.group_id
JOIN post_groups ON post_groups.group_id = groups.id
JOIN posts ON posts.id = post_groups.post_id
WHERE accounts.id = 2
I'd really like it to be a named scope or relation and not just finder sql, too.
The closest I could get:
class Account < ActiveRecord::Base
def posts
Post.includes(:post_groups => [:group => [:account_groups => :account]]).where('account_groups.account_id = ?',1)
end
end
You could probably extract it to a scope, instead:
class Post < ActiveRecord::Base
has_many :post_groups
has_many :groups, :through => :post_groups
scope :for_account, lambda {|account| joins(:post_groups => [:group => [:account_groups => :account]]).where('account_groups.account_id = ?',account.id)}
end
Both produce:
SELECT "posts".* FROM "posts" INNER JOIN "post_groups" ON "post_groups"."post_id" = "posts"."id" INNER JOIN "groups" ON "groups"."id" = "post_groups"."group_id" INNER JOIN "account_groups" ON "account_groups"."group_id" = "groups"."id" INNER JOIN "accounts" ON "accounts"."id" = "account_groups"."account_id" WHERE (account_groups.account_id = 1)
Both of these... require that PostGroup belongs_to :group and group has_many account_groups
I'm trying to create a named scope like User.not_in_project(project) but I can't find the right way.
I have Users, Projects and Duties as a join model:
class User < ActiveRecord::Base
has_many :duties, :extend => FindByAssociatedExtension
has_many :projects, :through => :duties
end
class Duty < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
class Project < ActiveRecord::Base
has_many :duties
has_many :users, :through => :duties
end
I tried with a named_scope similar to this find clause:
User.all(:joins => :duties, :conditions => ['duties.project_id != ?', my_project])
But that doesn't return me users who don't have my_project but users that have a project other than my_project.
In other words, I want the named scope to behave exactly like this method:
def self.not_present_in p
self.all.reject{|u| u.projects.include?(p)}
end
How can I do that?
Thinking in SQL, the query should be something like:
select id
from users
where id not in (select id
from users join duties on users.id = duties.user_id
join projects on duties.project_id = projects.id
where projects.id = %)
But I'm not too sure how it would work using named_scope. I'd say use something like
def self.not_present_in p
find_by_sql ["select id from users where id not in (select id from users join duties on users.id = duties.user_id join projects on duties.project_id = projects.id where projects.id = ?)", p]
end
Not as pretty as using AR, but will work (and save you some queries, probably).