Calling a class method through an object instance - ruby-on-rails

Railscasts #4 uses this sample code:
class Task < ActiveRecord::Base
belongs_to :project
def self.find_incomplete
find_all_by_complete(:false, :order => "created_at DESC")
end
end
class ProjectsController < ApplicationController
def show
#project = Project.find(params[:id])
#tasks = #project.tasks.find_incomplete
end
end
Using #project.tasks.find_incomplete, only finds incomplete orders that belong to that specific Project instance.
I would expect that call to be equivalent to Task.find_incomplete, but it is not. How can that be? How does Rails (or Ruby) now to just invoke that method for those specific Tasks in that Project instance?

This works because scopes of ActiveRecord relations are merged. It's not that find_incomplete is running on individual task instances.
#project.tasks creates an ActiveRecord scope of the tasks for that project instance and then that scope is still in effect when your find_incomplete method is called.
Take a look at the documentation here: http://guides.rubyonrails.org/active_record_querying.html#scopes
Your find_incomplete method works in the same way as the self.published example in the docs.
Think of the underlying SQL query that would run:
#project.tasks would create a where condition like SELECT * FROM projects WHERE project_id = <project_id>
find_all_by_complete then merges in an and condition for complete = 0
I think the other piece of the puzzle that might help is that #project.tasks is not just a simple array array of Task objects, although it will have been converted to such if you type project.tasks in the Rails console. project.tasks is actually an Active Record relation object (or more precisely a proxy)
There are a number of reasons and benefits for this but the main 2 are that it allows chaining and it allows the underlying query to be run on demand only if/when needed.
The relation has a sequence of rules for how method calls are delegated, one of which is to call class methods on the class of the associated objects. (the relation knows that it's a relation to Tasks)
So you are correct when you wrote tasks.find_incomplete is still equal to Task.find_incomplete except that when find_incomplete is called through project.tasks a scope narrowing down to the project_id is already in effect.

Related

How to know if an "includes" call was made while working with a single record?

Motivation
The motivation was that I want to embed the serialization of any model that have been included in a Relation chain. What I've done works at the relation level but if I get one record, the serialization can't take advantage of what I've done.
What I've achieved so far
Basically what I'm doing is using the method includes_values of the class ActiveRecord::Relation, which simply tells me what things have been included so far. I.e
> Appointment.includes(:patient).includes(:slot).includes_values
=> [:patient, :slot]
To take advantage of this, I'm overwriting the as_json method at the ActiveRecord::Relation level, with this initializer:
# config/initializers/active_record_patches.rb
module ActiveRecord
class Relation
def as_json(**options)
super(options.merge(include: includes_values)) # I could precondition this behaviour with a config
end
end
end
What it does is to add for me the option include in the as_json method of the relation.
So, the old chain:
Appointment.includes(:patient).includes(:slot).as_json(include: [:patient, :slot])
can be wrote now without the last include:
Appointment.includes(:patient).includes(:slot).as_json
obtaining the same results (the Patient and Slot models are embedded in the generated hash).
THE PROBLEM
The problem is that because the method includes_values is of the class ActiveRecord::Relation, I can't use it at the record level to know if a call to includes have been done.
So currently, when I get a record from such queries, and call as_json on it, I don't get the embedded models.
And the actual problem is to answer:
how to know the included models in the query chain that retrieved the
current record, given that it happened?
If I could answer this question, then I could overwrite the as_json method in my own Models with:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
extend Associations
def as_json(**options)
super(options.merge(include: included_models_in_the_query_that_retrieved_me_as_a_record))
end
end
One Idea
One Idea I have is to overwrite the includes somewhere (could be in my initializer overwriting directly the ActiveRecord::Relation class, or my ApplicationRecord class). But once I'm there, I don't find an easy way to "stamp" arbitrary information in the Records produced by the relation.
This solution feels quite clumsy and there might be better options out there.
class ApplicationRecord < ActiveRecord::Base
def as_json(**options)
loaded_associations = _reflections.each_value
.select { |reflection| association(reflection.name).loaded? }
.map(&:name)
super(options.merge(include: loaded_associations))
end
end
Note that this only loads 1st level associations. If Appointment.includes(patient: :person) then only :patient will be returned since :person is nested. If you plan on making the thing recursive beware of circular loaded associations.
Worth pointing out is that you currently merge include: ... over the provided options. Giving a user no choice to use other include options. I recommend using reverse_merge instead. Or swap the placements around {includes: ...}.merge(options).

In Rails, can I filter one-to-many relationships without updating the database

I am using Rails 5 and I want to be able to filter a one-to-many relationship to only send a subset of the child items to the client. The data model is pretty standard, and looks something like this:
class Parent < ApplicationRecord
has_many :children, class_name: 'Child'
end
class Child < ApplicationRecord
belongs_to :parent
end
When the client makes a call, I only want to return some of the Child instances for a Parent.
This is also complicated because the logic about which Child objects should be returned is absurdly complicated, so I am doing it in Ruby instead of the database.
Whenever I execute something like the following, Rails is attempting to update the database to remove the association. I don't want the database to be updated. I just want to filter the results before they are sent to the client.
parent.children = parent.children.reject { |child| child.name.include?('foo') }
Is there a way to accomplish this?
Add an instance method in Parent model
def filtered_children
children.where.not("name like ?", '%foo%')
end
Call filtered_children wherever required, it doesn't make sense to reset the existing association instance variable. The same queries are cached so it doesn't matter if you call them one time or multiple times. But you can always memoize the output of a method to make sure the the method is not evaluated again second time onwards,
def filtered_children
#filtered_children ||= children.where.not("name like ?", '%foo%')
end
Hope that helps!
DB update is happening because filtered records are being assigned back to parent.children. Instead another variable can be used.
filtered_children = parent.children.reject { |child| child.name.include?('foo') }

Rails "includes" Method and Avoiding N+1 Query

I don't understand the Rails includes method as well as I'd like, and I ran into an issue that I'm hoping to clarify. I have a Board model that has_many :members and has_many :lists (and a list has_many :cards). In the following boards controller, the show method looks as follows:
def show
#board = Board.includes(:members, lists: :cards).find(params[:id])
...
end
Why is the includes method needed here? Why couldn't we just use #board = Board.find(params[:id]) and then access the members and lists via #board.members and #board.lists? I guess I'm not really seeing why we would need to prefetch. It'd be awesome if someone could detail why this is more effective in terms of SQL queries. Thanks!
Per the Rails docs:
Eager loading is the mechanism for loading the associated records of
the objects returned by Model.find using as few queries as possible.
When you simply load a record and later query its different relationships, you have to run a query each time. Take this example, also from the Rails docs:
clients = Client.limit(10)
clients.each do |client|
puts client.address.postcode
end
This code executes a database call 11 times to do something pretty trivial, because it has to do each lookup, one at a time.
Compare the above code to this example:
clients = Client.includes(:address).limit(10)
clients.each do |client|
puts client.address.postcode
end
This code executes a database call 2 times, because all of the necessary associations are included at the onset.
Here's a link to the pertinent section of the Rails docs.
Extra
A point to note as of recent: if you do a more complex query with an associated model like so:
Board.includes(:members, lists: :cards).where('members.color = ?', 'foo').references(:members)
You need to make sure to include the appended references(:used_eager_loaded_class) to complete the query.

Ruby delegating class/relation-level methods

EDIT -- I changed the object-oriented modeling to better reflect the not-particularly-intuitive relationship in my app, (thanks, Anthony) so that this question makes more sense. Sorry about that.
In my Rails app, I want certain models to be able to delegate to other models on not just an instance level but also a class/relation level. That is to say, assuming a House model, which has_many Users and defines a class-level method "addresses" that's called on relations of houses, I could do this:
users.addresses
Behind the scenes, this would actually do two things: 1) run users.houses (which would grab all houses with an ID among those plucked from the house_id column of the relation of users), and 2) call addresses on that relation of houses.
My attempt currently looks something like this:
class House
has_many :users
def self.addresses
map(&:address)
end
def address
"#{street_address}, {state}, #{country}"
end
end
class User
belongs_to :house
delegate :address, to: :house
class << self
delegate :addresses, to: :houses
end
def self.houses
Houses.where(id: pluck(:house_id))
end
...
end
This fundamentally seems to work -- almost. If I have a group of users, I can do users.houses and grab the associated relation of houses. If I call addresses on an unrelated relation of houses, the method works great. If I call, users.addresses, it calls the class-level method of that name in houses.rb.
But, the method errors when I try to chain these things together.
If I call users.addresses (or users.houses.addresses, so delegation isn't the issue here per se), the House.addresses method is called, but within that method, self seems to be not the relation of houses that users.houses should (and generally does) return, but just the House class itself. Consequently, if I call any array methods on self within addresses (which would work on a relation), the method throws an error such as undefined method 'map' for <Class:0x1230101239123>.
This problem persists even if I take away my fancy class-level delegation logic and replace it with the explicit version:
class User
...
def self.addresses
houses.addresses
end
end
Same error. Also when I just try users.houses.addresses.
The only version that does work is the following:
class User
...
def self.addresses
houses.map(&:address)
end
end
In other words, the logic chain seems to be identical, but moving the map into the User method and out of the House method fixes things.
I'm super confused by this, because to my eyes, that last (successful) version should fundamentally be identical to the two failing versions above. The only difference is that in the successful version, I'm repeating logic in a way I'd like to avoid.
So I guess the questions are
1) Why is users.houses returning a relation, but users.addresses (and users.houses.addresses) suddenly calling addresses on the class rather than the relation?
2) Given these issues, is there a better, more Railsy way to set up the relationship between House and User such that I can run these class-level query methods (ie users.houses)?
Any opinions on the best way to go about fixing this?

Callback before association is called

I have to set the table name of an associated model (limesurvey), because the table name is dynamic and depends on an attribute (survey_id) of the model (task).
My current implementation sets the table name, when the task is initialized:
class task < ActiveRecord::Base
after_initialize :setTablename
has_one :limesurvey
def setTablename
Limesurvey.table_name = "lime_survey_#{self.survey_id}"
end
end
This implementation works, but it has the disadvantage, that the setTablename-method is called for every task, although it isn't needed.
How can I execute setTablename only before the association limesurvey is loaded?
Caveat: I agree that you are taking on a sea of troubles as the commenters have mentioned. Further, this will likely be worse, since before at lease setTablename was getting called for every task.
class Task < ActiveRecord::Base
has_one :limesurvey
def lime_survey
#table_name || = (Limesurvey.table_name = "lime_survey_#{self.survey_id}")
limesurvey
end
end
This defines a version of limesurvey with an underscore, but checks first if the table name has been set. Call lime_survey instead of limesurvey, and you will have the effect you asked for.
Similar to the approach suggested by Andy. However, although the association is just a method, I'm not sure you can redefine it and call super, since it's not a method in the parent class (or module).
Associations are just methods defined in a module that is included in your model, so you can override them as you would other methods
class Task < ActiveRecord::Base
has_one :limesurvey
def limesurvey
# do something here...
super
end
end
However, as people have mentioned in the comments on the question, what you're doing is a really bad idea. What would happen if you have two tasks available at once, and attempted to access the limesurvey on both of them?
t1 = Task.first
t2 = Task.last
l1 = t1.limesurvey
l2 = t2.limesurvey
l1.update_attributes(foo: "bar")
# Oops... saves into the wrong table...
Even if you manage to avoid doing this explicitly anywhere in your whole app, if you have two concurrent requests it could potentially happen accidentally!

Resources