named scope based on number of associated records - ruby-on-rails

I am doing some basic sql logic and I want to use a named scope. I'm trying to find out how many members of a season have also participated in another season (i.e. they are returning members).
class Season
has_many :season_members
has_many :users, :through => :season_members
def returning_members
users.select { |u| u.season_members.count > 1 }
end
end
class SeasonMember
belongs_to :season
belongs_to :user
end
class User
has_many :season_members
end
Is it possible to use :group and friends to rewrite the returning_members method as a scope?
I happen to be using Rails 2.3 but I'll also accept solutions that rely on newer versions.

Not sure if you really want to put this scope on Season, since that would imply that you are looking for Seasons which have repeat users. But, I assume you want Users that have repeat seasons. With that assumption, your scope would be:
class User < ActiveRecord::Base
scope :repeat_members,
:select=>"users.*, count(season_members.season_id) as season_counter",
:joins=>"JOIN season_members ON season_members.user_id = users.id",
:group=>"users.id",
:having=>"season_counter > 1"
end
which would result in the following query:
SELECT users.*, count(season_members.season_id) as season_counter FROM "users" JOIN season_members ON season_members.user_id = users.id GROUP BY users.id HAVING season_counter > 1
Confirmed with: Rails 3.1.3 and SQLite3

Related

How do I get the records with exact has_many through number of entries on rails

I have a many to many relationship through a has_many through
class Person < ActiveRecord::Base
has_many :rentals
has_many :books, through rentals
end
class Rentals < ActiveRecord::Base
belongs_to :book
belongs_to :person
end
class Book < ActiveRecord::Base
has_many :rentals
has_many :persons, through rentals
end
How can I get the persons that have only one book?
If the table for Person is called persons, you can build an appropriate SQL query using ActiveRecord's query DSL:
people_with_book_ids = Person.joins(:books)
.select('persons.id')
.group('persons.id')
.having('COUNT(books.id) = 1')
Person.where(id: people_with_book_ids)
Although it's two lines of Rails code, ActiveRecord will combine it into a single call to the database. If you run it in a Rails console, you may see a SQL statement that looks something like:
SELECT "persons".* FROM "persons" WHERE "deals"."id" IN
(SELECT persons.id FROM "persons" INNER JOIN "rentals"
ON "rentals"."person_id" = "persons"."id"
INNER JOIN "books" ON "rentals"."book_id" = "books"."id"
GROUP BY persons.id HAVING count(books.id) > 1)
If this is something you want to do often, Rails offers what is called a counter cache:
The :counter_cache option can be used to make finding the number of belonging objects more efficient.
With this declaration, Rails will keep the cache value up to date, and then return that value in response to the size method.
Effectively this places a new attribute on your Person called books_count that will allow you to quite simply filter by the number of associated books:
Person.where(books_count: 1)

Rails, find results based on shared association using Active Record Query or AREL

I'm working on a rails project where I have a User, that has_many Teams through a Membership model. The User has a privacy setting which can be (public, private or protected)
On the current user's homepage, I want to display all of the users who have their profiles set to public, but also the users who have their profile set to protected and share a team with the current user.
I could do this as two separate queries and then combine the resulting arrays but I'm assuming it's 'better' to keep it as one - I think it will also then behave better with will_paginate.
I thought I might need to use Arel because of the .or condition but just can't get my head around the joins needed to work out shared teams.
I'm new to AREL, SQL and Stackoverflow for that matter, so apologies if this doesn't make much sense.
I'm assuming the following setup:
class User < AR
has_many :memberships
has_many :teams, through: :memberships
end
class Membership < AR
belongs_to :user
belongs_to :team
end
class Team < AR
has_many :memberships
has_many :teams, through: :memberships
end
Then this should work:
sql = <<QUERY
SELECT users.* FROM users WHERE users.privacy_setting = 'public'
UNION
SELECT users.* FROM users JOIN memberships ON users.id = memberships.user_id
WHERE memberships.team_id IN (#{current_user.team_ids})
AND users.privacy_setting = 'protected'
QUERY
# use the paginated find_by_sql method (provided by will_paginate)
User.paginate_by_sql(sql, page: params[:page], per_page: 50)
Of course the column names, etc depend on your setup...
PS: No need for AREL, I think...

Retrieving all items through 2 different associations

Let's say
User has many projects (projects created by user)
User has many memberships
Also
User has many joined_projects through memberships (projects created by other users)
What is the best way to join 'projects' and 'joined_projects' to get all the projects user has access to hopefully without using sql.
Other option would be creating membership for all projects even if user owns the project, but that generates duplicate data on the database.
The join method in ActiveRelation always uses inner joins, so there's no way to do this "properly" from an SQL perspective. However, you can always set up a counter_cache and query it like so:
class User < ActiveRecord::Base
has_many :projects, :counter_cache => true
has_many :joined_projects, :through => :memberships, :counter_cache => true
class << self
def has_projects
where('projects_count > 0 OR joined_projects_count > 0')
end
end
def all_projects
projects + joined_projects
end
end
It might hit a normalization nerve but should get the job done.

In Rails 3 how can I select items where the items.join_model.id != x?

In my Rails models I have:
class Song < ActiveRecord::Base
has_many :flags
has_many :accounts, :through => :flags
end
class Account < ActiveRecord::Base
has_many :flags
has_many :songs, :through => :flags
end
class Flag < ActiveRecord::Base
belongs_to :song
belongs_to :account
end
I'm looking for a way to create a scope in the Song model that fetches songs that DO NOT have a given account associated with it.
I've tried:
Song.joins(:accounts).where('account_id != ?', #an_account)
but it returns an empty set. This might be because there are songs that have no accounts attached to it? I'm not sure, but really struggling with this one.
Update
The result set I'm looking for includes songs that do not have a given account associated with it. This includes songs that have no flags.
Thanks for looking.
Am I understanding your question correctly - you want Songs that are not associated with a particular account?
Try:
Song.joins(:accounts).where(Account.arel_table[:id].not_eq(#an_account.id))
Answer revised: (in response to clarification in the comments)
You probably want SQL conditions like this:
Song.all(:conditions =>
["songs.id NOT IN (SELECT f.song_id FROM flags f WHERE f.account_id = ?)", #an_account.id]
)
Or in ARel, you could get the same SQL generated like this:
songs = Song.arel_table
flags = Flag.arel_table
Song.where(songs[:id].not_in(
flags.project(:song_id).where(flags[:account_id].eq(#an_account.id))
))
I generally prefer ARel, and I prefer it in this case too.
If your where clause is not a typo, it is incorrect. Code frequently uses == for equality, but sql does not, use a single equals sign as such:
Song.joins(:accounts).where('account_id = ?', #an_account.id)
Edit:
Actually there is a way to use activerecord to do this for you, instead of writing your own bound sql fragments:
Song.joins(:accounts).where(:accounts => {:id => #an_account.id})

How to filter by more than 1 habtm association

I'm pretty new at Rails, so don't kill me if this a stupid question =P
I have the following models:
class Profile < ActiveRecord::Base
has_and_belongs_to_many :sectors
has_and_belongs_to_many :languages
class Sector < ActiveRecord::Base
has_and_belongs_to_many :profiles
end
class Language < ActiveRecord::Base
has_and_belongs_to_many :profiles
end
I'm looking for an elegant way (without writing sql joins or anything, if possible) to get all the profiles that have a particular sector and a particular language.
I've googled but all I could find is how to do it for 1 habtm, but I need it for 2.
All I have is the following:
def some_method(sector_id, language_id)
Sector.find(sector_id).profiles
end
But I don't know then how to add the filter by language_id without messing with joins conditions or writing sql, and of course, all in one query... Is there a clean/elegant way to do this?
Thanks!
In your example above you've already generated 2 sql requests,
first Sector.find(#id) (select on
sectors table to get record
with id == #id)
second .profiles (select on profiles
table to get all profiles with
following sector - in this select
you already have inner join
profiles_selectors on
profiles_selectors.profile_id =
profiles.id generated automatically by rails)
I hope this is what you are looking for: (but I use :joins key)
class Profile < ActiveRecord::Base
has_and_belongs_to_many :sectors
has_and_belongs_to_many :languages
def self.some_method(language_id, sector_id)
all(:conditions => ["languages.id = ? and sectors.id = ?", language_id, sector_id], :joins => [:languages, :sectors])
end
end
Result of this method is 1 sql query and you get profiles filtered by language and sector.
Best regards
Mateusz Juraszek
Try this:
Profile.all(:joins => [:sectors, :languages],
:conditions => ["sectors.id = ? AND languages.id ?", sector_id, language_id])

Resources