How Rails Delegate Method Works

Understanding rails delegate method

delegate :count, to: :tasks, prefix: "total"

This is just meta programming that creates a method:

def total_count
tasks.send(:count)
end

This is not really a good fit for delegate though as you should be using size instead of count as the latter always causes a DB query even if the association has been eager loaded.

def tasks_total
tasks.size # prevents n+1 query issue.
end

Why you would want to create a method for this is beyond me though as its actually one more character to type.

How do the delegate methods in this Ruby class work?

delegate can be thought of as "send these methods to this target", so

self.cancelled?

expands to

(events.last.try(:state) || STATES.first).inquiry.cancelled?

and this is the inquiry method: http://apidock.com/rails/String/inquiry

So basically, it checks that the last event state in the data model (or the default "incomplete") has a string value equal to the method name (minus question mark), and returns true if it matches.

Understanding delegate and scoped methods in Rails

According to the documentation, delegate:

Provides a delegate class method to
easily expose contained objects’
methods as your own. Pass one or more
methods (specified as symbols or
strings) and the name of the target
object via the :to option (also a
symbol or string)

So this delegates the methods in the list to the scoped method, which is defined in ActiveRecord::NamedScoped::ClassMethods, and which returns an anonymous scope.

As to why ActiveRecord does this, it is so we can continue to use familiar methods such as find while behind the scenes AR is actually calling the fancy new Arel methods. For instance, when you do

Post.find(37)

What actually gets executed is:

Post.where(primary_key.eq(37))

Rails delegate - How it works

prefix: true specifies that you must include the model name as a prefix to the delegate method. So for a Widget instance you would call it like so:

widget.person_id
widget.person_guid

Not specifying prefix means you call the delegate without the model name prefix, e.g.

widget.last_name
widget.image_url

etc.

use ActiveSupport ``delegate to:'' to DRY presenters instance methods

This:

delegate :my_instance_method, to: :product_presenter

...doesn't work because :product_presenter is a symbol, not an instance of ProductPresenter. Perhaps try something more like:

class CartPresenter

delegate :my_instance_method, to: product_presenter

def product_presenter
@product_presenter ||= ProductPresenter.new
end

end

...and...

class ProductPresenter

def my_instance_method
# do something
end

end

This statement:

I'm currently instantiating presenters inside views

...is a little concerning to me since you're creating tight coupling between the view and the presenter. It's a longer topic, but if I were generating that view you show in your code it would look something more like:

<% @presenter = local_assigns[:presenter] if local_assigns[:presenter] %>

<div class="card" style="width: 14rem;">
<%= @presenter.card_content %>
</div>

Then, naturally, whatever presenter you pass in using locals needs to implement card_content. Now, your view knows nothing about presenter or its methods beyond that one method, card_content. You can do whatever you want in card_content and make changes in the future to product_presenter methods without ever having to worry about updating your view. Decoupled!

Delegating method to has_many association ignores preloading

Explanation

The reason why all_have_title? delegation doesn't work properly in your example is that your are delegating the method to blogs association, but yet defining it as a Blog class method, which are different entities and thus receivers.

At this point everybody following would be asking a question why there is no NoMethodError exception raised when calling user.all_have_title? in the second example provided by OP. The reason behind this is elaborated in the ActiveRecord::Associations::CollectionProxy documentation (which is the resulting object class of the user.blogs call), which rephrasing due to our example namings states:

that the association proxy in user.blogs has the object in user as @owner, the collection of his blogs as @target, and the @reflection object represents a :has_many macro.

This class delegates unknown methods to @target via method_missing.

So the order of things that are happening is as follows:

  1. delegate defines all_have_title? instance method in has_many scope in User model on initialization;
  2. when called on user all_have_title? method is delegated to the has_many association;
  3. as there is no such method defined there it is delegated to Blog class all_have_title? method via method_missing;
  4. all method is called on Blog with current_scope which holds user_id condition (scoped_attributes at this point is holding {"user_id"=>1} value), so there is no information about preloading, because basically what is happening is:

    Blog.where(user_id: 1)

    for each user separately, which is the key difference in comparison with the preloading that was performed before, which queries associated records by multiple values using in, but the one performed here queries a single record with = (this is the reason why the query itself is not even cached between these two calls).

Solution

To both encapsulate the method explicitly and mark it as a relation-based (between User and Blog) you should define and describe it's logic in the has_many association scope:

class User
delegate :all_have_title?, to: :blogs, prefix: false, allow_nil: false

has_many :blogs do
def all_have_title?
all? { |blog| blog.title.present? }
end
end
end

Thus the calling you do should result in the following 2 queries only:

user = User.includes(:blogs).first
=> #<User:0x00007f9ace1067e0
User Load (0.8ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
Blog Load (1.4ms) SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`user_id` IN (1)
user.all_have_title?
=> true

this way User doesn't implicitly operate with Blog's attributes and you don't lose you preloaded data. If you don't want association methods operating with title attribute directly (block in the all method ), you can define an instance method in Blog model and define all the logic there:

class Blog
def has_title?
title.present?
end
end

What is `delegate` in a Rails form object file?

Delegate is used if you want to easily access a value on an associated object

In your example, your Form has a user. In order to access email, you could continually type: my_form.user.email or you could use delegation (as in your example) which means you can just type my_form.email and the form figures out where to get the email from.

It allows you to reduce typing, but also to hide away implementation-details.

http://apidock.com/rails/Module/delegate explains it pretty well if you want more

Typically this is used for Form objects so that you can build a flatter params-structure in the view that contains the form. eg if you delegate both :email and :email= then you can name a field :email in the my_form form... and then in the controller you can just use my_form = MyForm.new(params[:my_form]) instead of having to separately instantiate the associated objects and pass the attributes specific to each.

Delegate methods to the result of a method

I figured it out and it's incredibly simple. Just delegate to the name of the method. Here's a working example:

class MyClass
extend Forwardable
delegate %w([] []=) => :build_hash

def build_hash
return {'a'=>1}
end
end

In Rails, can I order a query by a delegate method?

Yes, but it requires instantiating all your records first (which will decrease performance).

@measurements = Measurement.joins(:identifier).sort_by &:name

This code loads all Measurements and instantiates them and then sorts them by the Ruby method .name

Explanation

delegate only affects instances of your ActiveRecord model. It does not affect your SQL queries.

This line maps directly to a SQL query.

Measurement.includes(:identifier).order(:name)

As you have noticed, it looks for a name column on the measurements table and doesn't find anything. Your ActiveRecord model instances know that name only exists on identifier, but your SQL database doesn't know that, so you have to tell it explicitly on which table to find the column.



Related Topics



Leave a reply



Submit