Collectionproxy VS Associationrelation

CollectionProxy vs AssociationRelation

ActiveRecord::Relation are simple query objects before being turned into a query and executed, CollectionProxy on the other hand are a little bit more sophisticated.

First of all you get association extentions, you probably saw something that looks like this, assume a book store model that has many books

class Store < ActiveRecord::Base
has_many :books do
def used
where(is_used: true)
end
end
end

This way you get to call used books in a store using a syntax that looks like this

Store.first.books.used

But this is the most basic uses, you could use the attributes that are exposed to you in the collection proxy, which are owner, reflection and target

Owner

The owner provides a a reference to the parent object holding the association

Reflection

The reflection object is an instance of ActiveRecord::Reflection::AssocciationReflection and contains all the configuration options for the association.

Target

The target is the association collection objects (or single object when has_one and belongs_to).

Using those methods you could do some conditons in your association extention, for example if we have a blog, we give access to all deleted posts to users who are admins (lame example I know)

Class Publisher < ActiveRecord::Base
has_many :posts do
def deleted
if owner.admin?
Post.where(deleted: true)
else
where(deleted: true)
end
end
end
end

You also get access to two more methods which are reset and reload, the first one (reset) clears the cached association objects, the second one (reload) is more common and is used to reset and then loads the associated objects from the database.

I hope this explains how having a CollectionProxy class would be so useful

Many to many through: Going from Associations::CollectionProxy to AssociationRelation

You can operate on ActiveRecord::Associations::CollectionProxy as you would normally do on ActiveRecord_AssociationRelation.

But if you really want to have the latter, just call, for example, all:

user.favorites.all.class #=> Client::ActiveRecord_AssociationRelation

How do I get a relation with all the clients?

You have defined the association:

has_many :clients, through: 'favorites'

Thus getting a relation of associated clients is as easy as:

user.clients

Rails: Model.association vs Model.association.all

CollectionProxy vs AssociationRelation

Here is an explanation from a previous SO question that might help you out.

ActiveRecord Class Methods / Relations self

Something along the lines of

class Blah < ActiveRecord::Base   
def self.thing
MyProxyObject.new(all)
end
end

works for me (prior to rails 4, use scoped rather than all):

Blah.where(:foo => 1).thing

results in the proxy object holding a relation with the appropriate conditions applied.

Is it possible to get the ActiveRecord::Relation object for an association

For a few minutes I used the where(nil) hack, then I had a brainwave and tried something random:

User.first.posts.scoped

That's it! :D

Yeah, Rails + Arel is really poorly documented. Looking forward to it maturing to the point where I can actually look things up and get actual answers.

Use send() on a related collection

The problem here is that the name collides with Kernel#open which is used to open IO streams.

irb(main):001:0> z = Zone.create
(0.2ms) BEGIN
Zone Create (0.8ms) INSERT INTO "zones" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id" [["created_at", "2018-10-19 16:29:40.018339"], ["updated_at", "2018-10-19 16:29:40.018339"]]
(0.7ms) COMMIT
=> #<Zone id: 10, created_at: "2018-10-19 16:29:40", updated_at: "2018-10-19 16:29:40">
irb(main):002:0> z.orders.send(:open)
Creating scope :open. Overwriting existing method Order.open.
ArgumentError: wrong number of arguments (given 0, expected 1..3)
from (irb):2:in `initialize'
from (irb):2:in `open'
from (irb):2
irb(main):003:0> z.orders.method(:open)
=> #<Method: Order::ActiveRecord_Associations_CollectionProxy(Kernel)#open>
irb(main):004:0> z.orders.method(:open).call
ArgumentError: wrong number of arguments (given 0, expected 1..3)
from (irb):4:in `initialize'
from (irb):4:in `open'
from (irb):4:in `call'
from (irb):4
irb(main):005:0> z.orders
Order Load (0.8ms) SELECT "orders".* FROM "orders" INNER JOIN "customers" ON "orders"."customer_id" = "customers"."id" WHERE "customers"."zone_id" = $1 LIMIT $2 [["zone_id", 10], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy []>
irb(main):006:0> z.orders.open
Order Load (0.9ms) SELECT "orders".* FROM "orders" INNER JOIN "customers" ON "orders"."customer_id" = "customers"."id" WHERE "customers"."zone_id" = $1 AND "orders"."status" = $2 LIMIT $3 [["zone_id", 10], ["status", "open"], ["LIMIT", 11]]
=> #<ActiveRecord::AssociationRelation []>

As you can see the call goes to Kernel#open first and then goes to the method defined by the scope after the collection is loaded. I'm guessing this is due to ActiveRecord::Associations::CollectionProxy doing some sort of lazy proxing to the association target class. Since the CollectionProxy already has an open method (Kernel#open) it uses it instead.

A simple solution here instead of dynamic calling would be to use a scope that takes an argument:

class Order < ApplicationRecord
belongs_to :customer
scope :with_status, ->(status){ where(status: status.to_s) }
end

Or even better use ActiveRecord::Enum which is smart enough to properly work with the collection proxy.

class Order < ApplicationRecord
belongs_to :customer
# you need to change the DB column to an integer type
enum status: [:open, :closed]
end

How can an ActiveRecord::Relation object call class methods

There's not much documentation on the application on class methods to ActiveRecord::Relation objects, but we can understand this behavior by taking a look at how ActiveRecord scopes work.

First, a Rails model scope will return an ActiveRecord::Relation object. From the docs:

Class methods on your model are automatically available on scopes. Assuming the following setup:

class Article < ActiveRecord::Base
scope :published, -> { where(published: true) }
scope :featured, -> { where(featured: true) }

def self.latest_article
order('published_at desc').first
end

def self.titles
pluck(:title)
end
end

First, invoking a scope returns an ActiveRecord::Relation object:

Article.published.class
#=> ActiveRecord::Relation

Article.featured.class
#=> ActiveRecord::Relation

Then, you can operate on the ActiveRecord::Relation object using the respective model's class methods:

Article.published.featured.latest_article
Article.featured.titles

It's a bit of a roundabout way of understanding the relationship between class methods and ActiveRecord::Relation, but the gist is this:

  1. By definition, model scopes return ActiveRecord::Relation objects
  2. By definition, scopes have access to class methods
  3. Therefore, ActiveRecord::Relation objects have access to class methods


Related Topics



Leave a reply



Submit