Ideal way to get association of ActiveRecord::Relation - ruby-on-rails

Consider the following:
class User < ActiveRecord::Base
has_many :posts
end
Now I want to get posts for some banned users.
User.where(is_banned: true).posts
This produces a NoMethodError as posts is not defined on ActiveRecord::Relation.
What is the slickest way of making the code above work?
I can think of
User.where(is_banned: true).map(&:posts).flatten.uniq
But this is inefficient.
I can also think of
user_scope = User.where(is_banned: true)
Post.where(user: user_scope)
This requires the user association to be set up in the Post model and it appears to generate a nested select. I don't know about the efficiency.
Ideally, I would like a technique that allows traversing multiple relations, so I can write something like:
User.where(is_banned: true).posts.comments.votes.voters
which should give me every voter (user) who has voted for a comment on a post written by a banned user.

Why not just use joins?
Post.joins(:user).where(users: {is_banned: true})
This will generate SQL to the effect of
SELECT *
FROM posts
INNER JOIN users ON posts.user_id = users.id
WHERE users.is_banned = true
This seems to be exactly what you are looking for. As far as your long chain goes you can do the same thing just with a much deeper join.

In your code:
User.where(is_banned: true)
will be and ActiveRecord::Relation and you need one record. So doing if from the User model would be more complicated. Depending on how the relationship is set up you could add a scope in your Post model.
scope :banned_users, -> { joins(:users).where('is_banned = ?', true) }
Then you would just call Post.banned_users to get all the post created by banned users.

Here's a start of a solution for your ideal technique. It probably doesn't work as written with extended chaining, and performance would probably be pretty bad. It would also require that you define the inverse_of for each association —
module LocalRelationExtensions
def method_missing(meth, *args, &blk)
if (assoc = self.klass.reflect_on_association(meth)) && (inverse = assoc.inverse_of)
assoc.klass.joins(inverse.name).merge(self)
else
super
end
end
end
ActiveRecord::Relation.include(LocalRelationExtensions)
But really you should use the comment of #engineersmnky.

Related

Devise and acts_as_votable: where votes_for voter_id => current_user.id

I'm using the devise and acts_as_votable gems to allow users to cast their votes on posts. My current objective is to query a list of the posts liked by the user.
Looking around in the Rails console I found out that when a post is voted on a votes_for table gets appended on containing information like the timestamp, vote weight and the voter_id which is what I would like to call upon.
Here's what the user controller looks like (user controller because I'm attempting to query the likes on the users/show page):
#user = current_user
#links = Link.all
#likes = #links.where.votes_for(:voter_id => #user.id) // line I'm trying to figure out, I reckon it'd look something like this
I could definitely be going about this the wrong way altogether, if that's the case I'd just appreciate some direction.
Explanation would be appreciated... I'm learning the fundamentals of Rails and finding its naming conventions convoluted and hard to follow.
If you're using Devise, I guess the current_user method should be available in your view and you shouldn't need a reassignment to #user
To understand how ActiveRecord works you might want to look into the documentation here.
As for the links and the voter_id, here's the way I think your query should be:
#likes = Link.where(votes_fors: {voter_id: current_user.id}) # OR Link.where(voter: current_user)
Basically the query will be transformed to a SQL query which says:
"SELECT * FROM links INNER JOIN votes_fors ON links.id = votes_fors.votable_type='Link' WHERE votes_fors.voter_id = ?" [voter_id, 1]
You get the idea?
A better approach would be to use the ActiveRecord methods to fetch these collections e.g
class User
has_many :votes_fors, foreign_key: :voter_id
has_many :favorites, through: :votes_fors, source: :votable # not really sure of this syntax but it should work I think.
end
With the above, you should be able to do:
current_user.favorites which would fetch you all the links voted by the user.

Filter ActiveRecord objects based on model method?

Is there a way to use a filter criterion in where, which is not a DB column. If I have a Movie model with the following method:
def blockbuster?
imdb_rating > 8
end
is there a way to do something like Movie.where(:blockbuster? => true). I know that in this particular example it's possible to just use the imdb_rating attribute (Movie.where('imdb_rating > ?', 8)), but there are cases, when a method does a more complex logic. Currently, if I want to do this, I must call Movie.all.select(&:blockbuster?), but I want to do this at the DB level. Thank you.
P.S. I am sure that similar questions are asked many times, but I can't seem to think of the right keywords to find them here or on Google. Please, post a link if this is answered elsewhere.
Have you tried making it into a scope? There is some information on scopes in the Rails documentation.
Basically, with your method, you'd do something like:
class Movie < ActiveRecord::Base
scope :blockbuster, -> { where('imdb_rating > ?', 8) }
end
Movie.blockbuster # returns all relevant objects as an ActiveRecord relation
Class methods are also available as scopes:
class Movie < ActiveRecord::Base
def self.blockbuster?
where('imdb_rating ?', 8)
end
end
Movie.blockbuster? # also returns all relevant objects as an ActiveRecord relation

filter data via boolean wildcard

This is somewhat of an updated question from a problem I had earlier. I am creating a 'match.com' style app for local dog adoptions, where users and dogs have a profile. In each profile, there are Boolean fields. I am trying to show the end user only the dogs that match their user profile. A simple one-to-one match does not work however since the logic changes depending on the user's Boolean. For example, if a user has kids (user.kids_under_10: true) then I simply match it to a dog that can be placed with kids (dog.kids_under_10:true). The model would look like
class Dog < ActiveRecord::Base
def self.by_profile(user)
if user.kids_under_10 == true
Dog.where(kids_under_10: user.kids_under_10)
end
end
If the user does not have kids however, dogs that answer both true and false to this question can be displayed, since dogs that are AND are not kid friendly can be placed with this user. I realize that if this were the only criteria for matching, I could simply add a else, Dog.all statement to the above method. Since there will other Boolean fields to match however (i.e. user.has_cats and dog.cat_friendly), this solution would not work. I think I need a variable containing an array here, but not really sure... any suggestions.
Try to use something like this:
class Dog < ActiveRecord::Base
def self.by_profile(user)
dogs = scoped
dogs = dogs.where(kids_under_10: true) if user.kids_under_10
dogs = dogs.where(cat_friendly: true) if user.own_cats
# other conditions
dogs
end
end
Basically the scoped method gives you an empty relation. You can then use it to conditionally add where statements (or anything else AR supports).
So I found a solution, however I 'm not sure it's the most efficient solution, so any suggestions are very welcome (I am new to programming and looking to learn the 'best' way to handle different situations). Here is how I solved the issue:
class Dog < ActiveRecord::Base
def self.by_profile(user)
if user.kids_under_10?
kids_under_10 = true
else
kids_under_10 = [true, false]
end
Dog.where(kids_under_10: kids_under_10)
end
end
I can foresee this solution becoming a bit cumbersome as I add more parameters. The conditional statements could get quite long. Thanks for any suggestions!
def self.by_profile(user)
user.kids_under_10 ? Dog.where(kids_under_10: true) : Dog.all
end
not very elegant, but should work.

How to refactor that complex singelton model method for create nested models in Rails?

I have following complex method which I cut off from controller:
def self.create_with_company_and_employer(job_params)
company_attributes = job_params.delete(:company_attributes)
employer_attributes = job_params.delete(:employer_attributes)
new(job_params) do |job|
job.employer = Employer.find_or_create_by_email(employer_attributes)
company_attributes[:admin_id] = job.employer.id if Company.find_by_nip(company_attributes[:nip]).nil?
job.company = Company.find_or_create_by_nip(company_attributes)
Employment.create(employer_id: job.employer.id, company_id: job.company.id)
end
end
I using here two nested_attributes functionality for create company and employer.
Whole code you can find here: https://gist.github.com/2c3b52c35df763b6d9b4
company_attributes[:admin_id] = job.employer.id if Company.find_by_nip(company_attributes[:nip]).nil?
Employment.create(employer_id: job.employer.id, company_id: job.company.id)
Basically I would like to refactor that two lines:
I looked at your gist and i think this is a design issue.
your Employment and Job models seem somewhat redundant, but i don't know what are their actual purpose exactly so i can't really help for now on this matter (i have a hunch that your schema could be remodeled with the employements belonging to the jobs). However, if you really want to, you can use an after_create callback to manage the replication :
class Job < ActiveRecord::Base
after_create :create_corresponding_employment
def create_corresponding_employment
Employment.create( employer_id: employer.id, company_id: company.id )
end
end
this gets you rid of the last line of your method.
the other line you want to get rid of is tricky : you assign an admin_id to your company, but why would you want to do that ? In fact, you're just creating a 'hidden' relation between Company and Employer (a belongs_to one). Why do you need that ? Give more information and i can help.
one more thing: it is not advised to delete keys form the params, or even modify the hash directly. Use a copy.

Override mongomapper has_many association

I use Rails 3 with MongoMapper.
I want to add some records to the result of has many association.
For example, user has_many posts
class User
include MongoMapper::Document
many :posts
end
By default it will show only posts which belongs to the user, but if he/she specify special option in query (or in the user's settings menu, say show-commented=true), then I also need to add posts where user left any comments. So I think to override posts method
def posts
super + (show_commented_posts ? commented_posts : [])
end
But of course it doesn't work. How can I correctly override this method using mongo_mapper? Or is there any better approach for that problem?
Overriding methods on mongomapper is a very bad idea, you should try to refrain from doing it as it creates a lot of problems that are hard to trace back (I've been burned before by this).
Instead, you should consider using a scope such as
class Post
scope :related_to_user, lambda {|user| where('$or' => [ {user_id: user.id}, {'comments.user_id' => user.id}]) }
end
Then you can call
Post.related_to_user(current_user)

Resources