UPDATE:
How do I add a virtual attribute to a model and preserve the active record relation.
I tried the below, but .each returns an array, not active record. What other method can I use?
My set_listable_for method is converting an activerecord relation into an array. I want to preserve the ActiveRecord Relation.
At runtime, I added an attr_access to an active record model.
def add_listable_attribute_to(*relation)
relation.each do |rel|
rel[1].first.class.class_eval do
attr_accessor :listable
end
end
end
Then I used this method to set the value of the attribute to the same value for all records....
def set_listable_for(relation, object)
relation.each do |record|
record.listable = object
end
end
However, my ActiveRecord relation gets converted to an Array afterwords.
How to I preserve the Active Record relation, as I don't want an array. Since I continue to use it here and continue to scope and query on it...
def union_scope(*relation)
add_listable_attribute_to(*relation)
listable = relation.first[0]
combined = set_listable_for(relation.first[1], listable)
relation.drop(1).each do |relation_set|
listable = relation[0]
set_listable_for(relation_set[1], listable)
combined = combined.or(relation_set[1])
end
combined
end
Thank you
The .each call executes the query and iterates over the result. It wouldn't be a problem if this happens in the controller after scoping and pagination, but if it gets called before scoping and pagination, the whole model dataset will be loaded which isn't good.
To avoid that, you'll need to set up the listable as late as possible after data is retrieved from the database. I can see three approaches to handle this:
Use a decorator to wrap instances of your relation after it is loaded into the controller or view. This is simpler to understand but pulls the functionality out of the model layer.
Set listable in an after_initialize callback. This keeps the functionality in the model layer, but adds a lot of complexity.
Ensure that you only call set_listable_for in the controller after scoping. Variant of #1.
By adding an 'AS' Statement in Select, I was able to return an ActiveRecord model. Only issue is that when I call .count, I need to use .count(:all), or .count(:id), to prevent errors.
def union_scope(*relation)
listable = relation.first[0]
scope = relation.first[1]
combined = scope.select("#{scope.table_name}.*, \'#{listable.class.name}\' as listable")
relation.drop(1).each do |relation_set|
listable = relation_set[0]
scope = relation_set[1].select("#{scope.table_name}.*, \'#{listable.class.name}\' as listable")
combined = combined.or(scope)
end
combined
end
Related
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') }
I wondering if it is posible to use a model instance method as a where clause query. I mean. I have a model School with a method defined
class School < ActiveRecord::Base
def my_method
users.where(blablabla).present?
end
end
Is it posible to get something like:
School.all.where(my_method: true)
I know that I can do something like:
School.all.map{|x| x if x.my_method}
But this way has a huge penalization in performance compared to where query. Furthermore, the return of what I'm searching is an ActiveRecord Relation and map returns an array.
UPDATE:
Also there is another way to do it like:
School.all.joins(:users).where("users.attribute = something")
But this do not fit exactly what I want for several reasons.
Thanks in advance
I don't really know the relations between your models.
but where clause gets a hash of key - value.
So you can for example return the ID's of the users in a some kind of a hash and then use it.
def my_method
{user_id: users.where(blablabla).ids}
end
and use it:
School.all.where(my_method)
I have a class method where I want to modify the records that are currently grabbed by an ActiveRecord::Relation object. But I don't know how to refer to the current scope in a class method. self does not do it.
Example:
class User < ActiveRecord::Base
...
def self.modify_those_records
#thought implicitly #to_a would be called on currently grabbed records but doesn't work
temp_users_to_a = to_a
...
end
end
I would use it like this:
User.some_scope.modify_those_records
So User.some_scope would return to me an ActiveRecord::Relation that contains a bunch of User records. I then want to modify those records within that class method and then return them.
Problem is: I don't know how to explicitly refer to "that group of records" within a class method.
You can use current_scope:
def self.modify_those_records
current_scope.each do |user|
user.do_something!
end
end
If you want to order Users based on their admin rights, you would be better to use ActiveRecord:
scope :order_admins_first, order('CASE WHEN is_admin = true THEN 0 ELSE 1 END, id')
User.some_scope.order_admins_first
This code implies that you have a boolean column is_admin on the users table.
I would argue that a combination of a scope with each and an instance method is easier to understand than a class method. And as a bonus it is easier to test, because you can test all steps in isolation:
Therefore instead of User.some_scope.modify_those_records I would do something like:
User.some_scope.each(&:modify)
and implement a instance method:
# in user.rb
def modify
# whatever needs to be done
end
If you only want to modify the order of the records - better way is to add a sort field (if you do not have it already) to the model and sort by that.
User.some_scope.order(name: :asc).order(is_admin: :desc)
Is there any way to initialize a record from session. for e.g I have a organization object and I put this in session object like
session[:organization] = organization
Now I made a custom method current_organization (I know about devise) like
def current_organization
Organization.new(session[:organization])
end
This will return organization object. My organization belongs_to a team a devise model and team has_many :organizations but when I call
current_team.organizations.includes?(current_organization)
in view. It is returning false even if it is included in team's organizations but doing this
current_team.organizations.reload.includes?(current_organization)
is returning true. I set the session variable with organization object before calling view where i am using above method. Is there any thing which I missed like I am not able to figure out the reason for not returning true even it is included?
Try saving the record first.
Until you save it into the database, it is likely not to show up when you query for the team's organization children.
session[:organization_id] = organization.id
def current_organization
Organization.find session[:organization_id]
end
Ok after some googling i found that instead of using
Organization.new(session[:organization])
I should use
Organization.instantiate(session[:organization])
From apidock I found that
instantiate(attributes, column_types = {}) public
Given an attributes hash, instantiate returns a new instance of the
appropriate class. Accepts only keys as strings.
For example, Post.all may return Comments, Messages, and Emails by
storing the record’s subclass in a type attribute. By calling
instantiate instead of new, finder methods ensure they get new
instances of the appropriate class for each record.
I have a set of locations which have a relationship to a global_identificiation table which has id, arc_id (the object_id) and arc_type (the object_type) (ie a compound foreign key). How would I set up the association so that this global information is eagerly loaded on each find of Location? such that:
#l=Location.find(23)
#l['id']=23
#l['name']='some place'
#l['global_info']['id']=145
#l['global_info']['arc_id']=23
#l['global_info']['arc_type']='Location'
Right now, for saving, I just do an after_save callback which is how it gets into database but don't have any other associations with it.
thx for any help
edit:
perhaps this could just be done as an after_find on the classes that need it?
Either use after_find, or just create a method in your model to call, then just pass in the data as params.
Or better yet make a module that all your location based classes can include, then this module can define all sorts of location based methods.
module Locations
def find_with_data(id)
x = Location.find(id)
x['id']=id
x['name']='some place' #you could pass this in params
x
end
end
class Location < ActiveRecord::Base
include Locations
end
then call
new_loc = Location.new
new_loc.find_with_data(35)