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 inuser
as@owner
, the collection of hisblogs
as@target
, and the@reflection
object represents a:has_many
macro.
This class delegates unknown methods to@target
viamethod_missing
.
So the order of things that are happening is as follows:
delegate
definesall_have_title?
instance method inhas_many
scope inUser
model on initialization;- when called on
user
all_have_title?
method is delegated to thehas_many
association; - as there is no such method defined there it is delegated to
Blog
classall_have_title?
method viamethod_missing
; all
method is called onBlog
withcurrent_scope
which holdsuser_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 usingin
, 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
Single Occurrence Event with Ice_Cube Gem Using Start_Time and End_Time
Ruby Array to Hash: Each Element the Key and Derive Value from It
Comma Separated Array with a Text_Field in Rails
Carrierwave File Upload with Different File Types
Carrierwave How to Get the File Extension
Attempting to Install Libv8, "Failed to Build Gem Native Extension"
How to Tell If I'm Running from Jruby VS. Ruby
Sleep Until Condition Is True in Ruby
How to Run and Debug Ruby on Rails from Visual Studio Code
Rails 3: User Created Custom Forms
How to Make a Ruby Script Run Once a Second