How to use unscoped on associated relations in Rails3?
Asked Answered
B

7

74

I have a default scope on products due to information security constraints.

class Product < ActiveRecord::Base
  has_many :photos

  default_scope where('visible = 1')
end

In my associated Photo model, however, I also have to find products that should not be visible.

class Photo < ActiveRecord::Base
  belongs_to :product
end

my_photo.product

In other cases, I can use unscoped in order to bypass the default_scope, e.g. in Product.unscoped.find_by_title('abc'). However:

How to remove the scope when using associations of a record?

my_photo.unscoped.product does not make sense as my_photo does not have a method called unscoped. Neither does my_photo.product.unscoped make sense as my_photo.product may already be nil.

Bogusz answered 21/1, 2011 at 11:16 Comment(6)
Just a comment: I'd use sti in your case.Quasar
Wough. You would "cast" the objects around instead of using a boolean attribute?Bogusz
I definitely believe that if you need a default_scope, it's better to use sti with dedicated objects.Quasar
I have to "wough!" again. First, I already use STI for my model, having SimpleProduct, ConfigurableProduct < Product. So then I would have VisibleSimpleProduct, SimpleProduct, VisibleConfigurableProduct, ConfigurableProduct, and Product? Second, I use polymorphic associations on them, which is already painful. In my comments table, they are saved as commentable_type = ConfigurableProduct and tried to retrieved as Product. When trying to use ActiveRecord#becomes, all runtime product instances were broken. STI is the biggest weakness of Ruby/Rails. How do I 'cast' objects at runtime with Ruby?Bogusz
Besides (though not part of this question), we are using around_filter in all relevant Controllers with Product.scoping { FinancialProduct.scoping { InsureanceProduct.scoping { yield }}}. In that way, we do not need a default_scope.Bogusz
As of Rails 4.1 you can do belongs_to :product, ->{ unscope(where: :visible) }Metallo
B
75

Oh. I fooled myself. Thought the following would not work... but it does:

Product.unscoped do
  my_photo.product
end

Notice that you have to call unscoped on the model with the default_scope that should be bypassed.

Also, inheritance has to be respected. If you have class InsuranceProduct < Productand class FinancialProduct < Product and a default_scope in Product, all of the following two combinations will work:

InsuranceProduct.unscoped do
  my_record.insurance_products
end

FinancialProduct.unscoped do
  my_record.financial_products
end

Product.unscoped do
  my_record.products
end

However, the following will not work although the scope is defined in Product:

Product.unscoped do
  my_record.financial_products
end

I guess that's another quirk of STI in Ruby / Rails.

Bogusz answered 21/1, 2011 at 13:8 Comment(1)
There's a a gotcha with this solution though: the query has to be executed inside the block, so one has to make sure #all or #to_a are called on the has_many relation. See here for details: github.com/rails/rails/issues/5591Stringpiece
A
60

Another option is to override the getter method and unscope super:

class Photo < ActiveRecord::Base
  belongs_to :product

  def product
    Product.unscoped{ super }
  end
end

I ran into the same situation where I had one associated model that needed to be unscoped, but in almost every other case it needed the default scope. This should save you the extra calls to unscoped if you are using the assocation getter in more than one place.

Altostratus answered 13/6, 2012 at 15:43 Comment(5)
Thanks! Feels pretty hacky though... Wish there was a way to write belongs_to :product, :unscoped => true which would prevent us from having to override the method in every model.Brassica
There is such a thing in Rails 3. It's broken in Rails 4: github.com/rails/rails/issues/10643Thick
Great! Is there a way to call product.photos.unscoped if photos were scoped by default?Stan
A fix is pending for Rails 4: github.com/rails/rails/commit/…Bolton
Also take note that this produces an extra database query even if using Photo.includes(:product).Revue
L
33

I'm probably a bit late to the party, but some time ago I found myself in the same situation and I wrote a gem to do this easily: unscoped_associations.

Usage:

belongs_to :user, unscoped: true

Support for:

  • belongs_to
  • has_one
  • has_many

Polymorphic associations are also supported.

Lucas answered 25/11, 2013 at 19:17 Comment(5)
Thanks @markets, your gem saves me an odd case where default_scope cannot be excluded.Gulch
I'm very suprised this requires a gem! This should be a rails default feature!Brassica
Worked great for me in the specific case where I used .includes to pull in an association.Prostomium
The most elegant solution in my opinion!Fascicle
Warning this gem is is now archived on GitHub and hasn't been updated in 5 years.Wincer
D
22

If you need for a specific association to always be unscoped, you can unscope it when defining the association:

belongs_to :product, -> { unscope(where: :visible) }

For some reason, the specific where key wasn't loading correctly for me, so I just unscoped the entire where, which is another option that happens to work out in my case:

belongs_to :product, -> { unscope(:where) }

The other answers are worth looking at too, but this is another option for Rails 4.1+.

Dustydusza answered 29/10, 2016 at 17:32 Comment(1)
be very careful about using belongs_to :product, -> { unscope(:where) } it removes the whole scope, so if you have something.product it will just return the first product in the db. kills the where id =...Sudor
F
7

In Rails 4 you can use the association with an explicit unscope of the undesirable filter i.e. my_photo.product.unscope(where: :visible)

Fetch answered 5/5, 2016 at 14:8 Comment(0)
L
1

It's not on the main topic but on your problem with ActiveRecord#becomes: We (hopefully) fixed it with an initializer

 class ActiveRecord::Base

   def becomes_with_association_cache(klass)
     became = becomes_without_association_cache(klass)
     became.instance_variable_set("@association_cache", @association_cache)
     became
   end
   alias_method_chain :becomes, :association_cache

 end

https://gist.github.com/2478161

Larisalarissa answered 24/4, 2012 at 9:21 Comment(0)
P
0

New answer

This question should help you figure out how to bypass the default where clause for your association.

It's worth repeating though that if you're regularly having to avoid a scope then it probably should be a default. Create a visible non-default scope and use that explicitly in your associations.

Pict answered 21/1, 2011 at 11:56 Comment(2)
Sorry. I was in a hurry and completely messed up my example. It was only meant for illustrating the use case and has almost nothing to do with my real implementation. Please read my question again.Bogusz
Ok, that's better. I suspect this is a non-issue though; my_photo.product shouldn't invoke the default_scope.Pict

© 2022 - 2024 — McMap. All rights reserved.