Reusing activerecord scopes on has many through associations - ruby-on-rails

Say I have a few activerecord models in my rails 3.1 project that look like this:
class Component < ActiveRecord::Base
has_many :bugs
end
class Bug < ActiveRecord::Base
belongs_to :component
belongs_to :project
scope :open, where(:open => true)
scope :closed, where(:open => false)
end
class Project < ActiveRecord::Base
has_many :bugs
has_many :components_with_bugs, :through => :bugs, :conditions => ["bugs.open = ?", true]
end
In Short: I have a has_many through association (components_with_bugs) where I want to scope the "through" model. At present I'm doing this by duplicating the code for the scope.
Is there any way to define this has many through association (components_with_bugs) such that I can reuse the Bug.open scope on the through model, while still loading the components in a single database query? (I'm imagining something like :conditions => Bug.open)

Rails 4 answer
Given you have:
class Component < ActiveRecord::Base
has_many :bugs
end
class Bug < ActiveRecord::Base
belongs_to :component
belongs_to :project
scope :open, ->{ where( open: true) }
scope :closed, ->{ where( open: false) }
end
You have two possibilities:
class Project < ActiveRecord::Base
has_many :bugs
# you can use an explicitly named scope
has_many :components_with_bugs, -> { merge( Bug.open ) }, through: :bugs, source: 'component'
# or you can define an association extension method
has_many :components, through: :bugs do
def with_open_bugs
merge( Bug.open )
end
end
end
Calling projet.components_with_bugs or project.components.with_open_bugs will fire the same sql query:
SELECT "components".* FROM "components"
INNER JOIN "bugs" ON "components"."id" = "bugs"."component_id"
WHERE "bugs"."project_id" = ? AND "bugs"."open" = 't' [["project_id", 1]]
Which one is better to use depends on your application. But if you need to use many scopes on the same association, I guess association extensions could be clearer.
The real magic is done with merge which allows you to, as the name says, merge conditions of another ActiveRecord::Relation. In this case, it is responsible for adding AND "bugs"."open" = 't' in the sql query.

Apart from your scopes , write the default scope as:
default_scope where(:open => true) in your "through" model Bug.
class Bug < ActiveRecord::Base
belongs_to :component
belongs_to :project
default_scope where(:open => true)
scope :open, where(:open => true)
scope :closed, where(:open => false)
end
And in the Project model remove :conditions => ["bugs.open = ?", true]
class Project < ActiveRecord::Base
has_many :bugs
has_many :components_with_bugs, :through => :bugs
end
I think the above will work for you.

Try using the following.
has_many :components_with_bugs, :through => :bugs do
Bug.open
end

Can't you use something like this ?
has_many :components_with_bugs, :through => :bugs, :conditions => Bug.open.where_values
I haven't tested it, just proposing an path for investigation

The http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html specifies
:conditions Specify the conditions that the associated object must meet in order to be included as a WHERE SQL fragment, such as authorized = 1.
Hence you can do it as:
class Project < ActiveRecord::Base
has_many :bugs
has_many :components_with_bugs, :through => :bugs do
def open
where("bugs.open = ?", true)
end
end
end
EDIT:
You can't specify another model's scope as a condition. In your case, they way you have it implemented is right. You can implement it another way as
has_many :components_with_bugs, :through => :bugs # in this case, no need to use the relation.
def open_bugs
self.bugs.openn # openn is the scope in bug. Don't use name 'open'. It's a private method of Array.
end

Related

How to use scopes to join across multiple tables

I am new to ROR and I am trying to understand scopes. In my current implementation I am getting all the Processors and displaying it in the view.
class ProcessorsController
def index
#processors = Processor.all
end
end
I want to modify this so I can get only the processors where the user is admin. This is how my relations are set up.
class Processor
belongs_to :feed
#SCOPES (what I have done so far)
scope :feed, joins(:feed)
scope :groups, joins(:feed => :groups).join(:user).where(:admin => true)
end
class Feed < ActiveRecord::Base
has_and_belongs_to_many :groups
end
class Group < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_many :groups
scope :admin, where(:admin => true)
end
I was able to do this in my pry
pry(main)> Processor.find(63).feed.groups.first.user.admin?
PS: could someone provide some good resources where I could learn how to use scopes if the relationships are complex.
scope :with_admin, -> { joins(:feed => { :groups => :user }).where('users.admin' => true) }
As for the resources, have you gone through the official documentation on ActiveRecord joins?
you do not need scopes... you can get only the processors where the user is admin using relations and conditions:
class Feed < ActiveRecord::Base
...
has_one :user, through: :groups
end
class Processor
...
has_one :admin, through: :feed, source: :user, conditions: ['users.admin = 1']
end

Relation Conditions with Variables

I have the following models:
class Publication < ActiveRecord::Base
has_many :reviews
has_many :users, :through => :owned_publications
has_many :owned_publications
end
class User < ActiveRecord::Base
has_many :publications, :through => :owned_publications
has_many :owned_publications
end
class OwnedPublication < ActiveRecord::Base
belongs_to :publication
belongs_to :user
has_one :review, :conditions => "user_id = #{self.user.id} AND publication_id = #{self.publication.id}"
end
In the third model, I'm trying to set a condition with a pair of variables. It seems like the syntax works, except that self is not an instance of OwnedPublication. Is it possible to get the current instance of OwnedPublication and place it into a condition?
The solution requires the use of :through and :source options, as well as a proc call:
has_one :review, :through => :publication, :source => :reviews,
:conditions => proc { ["user_id = ?", self.user_id] }
Proc is the trick to passing in dynamic variables to ActiveRecord association conditions, at least as of Rails 3.0. Simply calling:
has_one :conditions => proc { ["publication_id = ? AND user_id = ?",
self.publication_id, self.user_id] }
will not work, though. This is because the association will end up searching the reviews table for a 'reviews.owned_publication_id' column, which does not exist. Instead, you can find the proper review through publication, using publication's :reviews association as the source.
I think your best bet is to just have the Review record belong_to an OwnedPublication, and setup your Publication model to get the reviews via a method:
def reviews
review_objects = []
owned_publications.each do |op|
review_objects << op
end
review_objects
end
Might be a more efficient way if you use a subquery to get the information, but it removes the concept of having unnecessary associations.

Selection of records through a join table in Ruby on Rails

I have three models
class Collection < ActiveRecord::Base
has_many :presentations
has_many :galleries, :through => :presentations
end
class Gallery < ActiveRecord::Base
has_many :presentations
has_many :collections, :through => :presentations
end
class Presentation < ActiveRecord::Base
belongs_to :collection
belongs_to :gallery
end
How do I get all the collections that do not belong to a given gallery?
My SQL knowledge is only rudimentary. I also want to let Rails (2.3) do the work without using explicitly a SQL expression.
Out of the box, you technically have to write some SQL (where clause)...
gallery_to_exclude = Gallery.first
Collection.find(:all,
:include => :presentations,
:conditions => ['presentations.gallery_id IS NULL OR presentations.gallery_id != ?',
gallery_to_exclude.id])
If you want to use Searchlogic, you can avoid this, though:
Collection.presentations_gallery_id_is_not(gallery_to_exclude.id).all

Rails named_scope across multiple tables

I'm trying to tidy up my code by using named_scopes in Rails 2.3.x but where I'm struggling with the has_many :through associations. I'm wondering if I'm putting the scopes in the wrong place...
Here's some pseudo code below. The problem is that the :accepted named scope is replicated twice... I could of course call :accepted something different but these are the statuses on the table and it seems wrong to call them something different. Can anyone shed light on whether I'm doing the following correctly or not?
I know Rails 3 is out but it's still in beta and it's a big project I'm doing so I can't use it in production yet.
class Person < ActiveRecord::Base
has_many :connections
has_many :contacts, :through => :connections
named_scope :accepted, :conditions => ["connections.status = ?", Connection::ACCEPTED]
# the :accepted named_scope is duplicated
named_scope :accepted, :conditions => ["memberships.status = ?", Membership::ACCEPTED]
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :members, :through => :memberships
end
class Connection < ActiveRecord::Base
belongs_to :person
belongs_to :contact, :class_name => "Person", :foreign_key => "contact_id"
end
class Membership < ActiveRecord::Base
belongs_to :person
belongs_to :group
end
I'm trying to run something like person.contacts.accepted and group.members.accepted which are two different things. Shouldn't the named_scopes be in the Membership and Connection classes?
However if you try putting the named scopes in the Membership and Connection classes then you get this error (because Person.find(2).contacts returns an array of Persons which doesn't have an 'accepted' method:
>> Person.find(2).contacts.accepted
NoMethodError: undefined method `accepted' for #<Class:0x108641f28>
One solution is to just call the two different named scope something different in the Person class or even to create separate associations (ie. has_many :accepted_members and has_many :accepted_contacts) but it seems hackish and in reality I have many more than just accepted (ie. banned members, ignored connections, pending, requested etc etc)
You answered your own question:
Shouldn't the named_scopes be in the Membership and Connection classes?
Yes, they should be. This will let you call them as you wanted. It's also logically where they belong.
If you want something on Person that checks both, you can do something like:
named_scope :accepted, :conditions => ["connections.status = ? OR memberships.status = ?", Connection::ACCEPTED, Membership::ACCEPTED]
Maybe you want this to be an AND? not sure.
I'm sure this is not the best way and I believe you can do this on person and group models, but I also believe the following will work for you:
# models
class Person < ActiveRecord::Base
has_many :connections
has_many :contacts, :through => :connections
has_many :memberships
has_many :groups, :through => :memberships
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :members, :through => :memberships
end
class Connection < ActiveRecord::Base
belongs_to :person
belongs_to :contact, :class_name => "Person", :foreign_key => "contact_id"
named_scope :accepted, :conditions => ["status = ?", Connection::ACCEPTED]
end
class Membership < ActiveRecord::Base
belongs_to :person
belongs_to :group
named_scope :accepted, :conditions => ["status = ?", Membership::ACCEPTED]
end
# controller
# get person's accepted contacts
#person = Person.first
#person.connections.accepted.map(&:contact)
# get group's accepted members
#group = Group.first
#group.memberships.accepted.map(&:person)

Rails order by in associated model

I have two models in a has_many relationship such that Log has_many Items. Rails then nicely sets up things like: some_log.items which returns all of the associated items to some_log. If I wanted to order these items based on a different field in the Items model is there a way to do this through a similar construct, or does one have to break down into something like:
Item.find_by_log_id(:all,some_log.id => "some_col DESC")
There are multiple ways to do this:
If you want all calls to that association to be ordered that way, you can specify the ordering when you create the association, as follows:
class Log < ActiveRecord::Base
has_many :items, :order => "some_col DESC"
end
You could also do this with a named_scope, which would allow that ordering to be easily specified any time Item is accessed:
class Item < ActiveRecord::Base
named_scope :ordered, :order => "some_col DESC"
end
class Log < ActiveRecord::Base
has_many :items
end
log.items # uses the default ordering
log.items.ordered # uses the "some_col DESC" ordering
If you always want the items to be ordered in the same way by default, you can use the (new in Rails 2.3) default_scope method, as follows:
class Item < ActiveRecord::Base
default_scope :order => "some_col DESC"
end
rails 4.2.20 syntax requires calling with a block:
class Item < ActiveRecord::Base
default_scope { order('some_col DESC') }
end
This can also be written with an alternate syntax:
default_scope { order(some_col: :desc) }
Either of these should work:
Item.all(:conditions => {:log_id => some_log.id}, :order => "some_col DESC")
some_log.items.all(:order => "some_col DESC")
set default_scope in your model class
class Item < ActiveRecord::Base
default_scope :order => "some_col DESC"
end
This will work
order by direct relationship has_many :model
is answered here by Aaron
order by joined relationship has_many :modelable, through: :model
class Tournament
has_many :games # this is a join table
has_many :teams, through: :games
# order by :name, assuming team has this column
def teams
super.order(:name)
end
end
Tournament.first.teams # are returned ordered by name
For anyone coming across this question using more recent versions of Rails, the second argument to has_many has been an optional scope since Rails 4.0.2. Examples from the docs (see scopes and options examples) include:
has_many :comments, -> { where(author_id: 1) }
has_many :employees, -> { joins(:address) }
has_many :posts, ->(blog) { where("max_post_length > ?", blog.max_post_length) }
has_many :comments, -> { order("posted_on") }
has_many :comments, -> { includes(:author) }
has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
has_many :tracks, -> { order("position") }, dependent: :destroy
As previously answered, you can also pass a block to has_many. "This is useful for adding new finders, creators and other factory-type methods to be used as part of the association." (same reference - see Extensions).
The example given there is:
has_many :employees do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by(first_name: first_name, last_name: last_name)
end
end
In more modern Rails versions the OP's example could be written:
class Log < ApplicationRecord
has_many :items, -> { order(some_col: :desc) }
end
Keep in mind this has all the downsides of default scopes so you may prefer to add this as a separate method:
class Log < ApplicationRecord
has_many :items
def reverse_chronological_items
self.items.order(date: :desc)
end
end

Resources