I have a model projectuser which has_many activities, and activities belongs_to projectuser. Now I have a collection of activities as #activities in a controller and I want to get the unique projectusers. How can I do this?
If #activities is a simple ruby collection (e.g. Array), then use this:
Projectuser.where(id: #activities.map(&:projectuser_id).uniq)
However, if your #activities object is an ActiveRelation (the result of an association call or query api call), you can do a more efficient lookup like this:
Projectuser.joins(:activities).merge(#activities)
The reason it is more efficient is because it avoids building a literal list of ids, which can get significantly harder on a db engine at it gets larger.
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'm looking for orientation for either a concrete or abstract approach in Ruby-Like (Rails 4/5) to model the following requirement or user story:
Given a model, let's call it PurchaseOrder with the following attributes:
amount_to_produce
amount_taken_from_stock
placement_date
delivery_date
product_id
client_id
As a user, i want to be able to see a table list of these PurchaseOrder and, when necessary, group them.
Detail Info: When a collection of PurchaseOrder is grouped, that grouped collection should behave exactly like a PurchaseOrder, in the sense that it must be displayed as a record in the table, filtering operations should work on the grouped record as they do on single PurchaseOrder instances, same goes for pagination and sorting. Moreover, the group must cache or at least i'm thinking it that way, the sum of amount_to_produce, amount_taken_from_stock, the minimum placement_date among all placement dates and last but not least, the minimum delivery_date also among them all.
Im thinking in modelling this implicitly in the PurchaseOrder like this:
Class PurchaseOrder < ApplicationRecord
belongs_to :group, class_name: PurchaseOrder.model_name.to_s, inverse_of: :purchase_orders
# purchase order can represent a "group" of purchase orders
has_many :purchase_orders, inverse_of: :group, foreign_key: :group_id
end
This way it would achieve the purpose of been displayed in the table view easily, filtering pagination and sorting would work out of the box and just by scoping records with group_id nil, the grouped records can be left out of the table.
However i'm foreseeing immediate drawbacks:
When updating a group member attribute, say amount_to_produce, the parent cached amount_to_produce should be updated also, same for the other three attributes. This would probably led to model callbacks before_update, which i tend not to use unless it concerns behaviour of the single instance itself.
When ungrouping a member, same history
Same when destroying a member of the group (it can and will happen).
For 1. we could imply that there's no need to cache the amounts or date attributes in the parent PurchaseOrder, since we can override the getter for those attributes and return the sum / min of the children if purchase_orders.size.nonzero?, however, this smells like something wrong.
So summing it up, i would like if not the best, an optimistic approach to model this scenario and regarding the method to group and ungroup members to / from a group, ideas on what's the best domain place to implement it, i'm thinking of a concern like Groupable.
Pd: For each group, the client_id of the group will be a default seeded client called "Multiple Customers", and the product_id, the same as the product_id of the children, since it's a restriction that only PurchaseOrder with same product_id can be grouped, no groups with different product_id's can be grouped.
Thanks.
I would split this into two models, a PurchaseOrderGroup, and a PurchaseOrder.
class PurchaseOrderGroup < ApplicationRecord
has_many :purchase_orders
belongs_to :product
def aggregate_pos
PurchaseOrder.where(purchase_order_group_id: self.id).
group(:purchase_order_group_id).
pluck('sum(amount_to_produce), min(delivery_date), ...')
end
end
class PurchaseOrder < ApplicationRecord
belongs_to :purchase_order_group
end
I would create a PurchaseOrderGroup for each PurchaseOrder even if there is only one, which maintains the same interface. You can then define delegate methods on the PurchaseOrderGroup which grab the appropriate sum, min, max etc of the children - aggregate queries should make short work of that. See above aggregate_pos() method. Easy enough to cache the results of this in the PurchaseOrderGroup class. Deleting or adding PurchaseOrder objects is easy then, just call aggregate_pos() again.
This also cleans up the product_id dilemma, just put that attribute on the group rather than the PurchaseOrder. That way it is impossible for two PurchaseOrders in the same group to have different product_ids.
Given the following 2 models
class PropertyApplication
has_and_belongs_to_many :applicant_profiles
end
class ApplicantProfile
has_and_belongs_to_many :property_applications
end
I have a query that lists all property_applications and gets the collection of applicant_profiles for each property_application.
The query is as follows and it is very inefficient.
applications = PropertyApplication.includes(:applicant_profile).all.select |property_application| do
property_application.applicant_profile_ids.include?(#current_users_applicant_profile_id)
do
assume #current_users_applicant_profile_id is already defined.
How can I perform one (or few) queries to achieve this?
I want to achieve something like this
PropertyApplication.includes(:applicant_profile).where('property_application.applicant_profiles IN (#current_users_applicant_profile))
I am trying to make an ActiveRecord query that will order the results by the value of one of the columns in an associated model:
I have a Chats model that has a one to many relationship with the messages model.
Chats has_many messages and Message belongs_to chat
And from my controller, I want to get a list of chats, ordered by the created_at of the associated message.first, eg:
#chats = current_user.chats.includes(:messages).order("messages.first.created_at").paginate(page: params[:page])
or something like that.
My question is that how can I achieve this kind of ordering from an associated model with this relationship? All contributions are appreciated.
Also add .references(:messages)
This will pull messages in as a join rather than a separate query.
You can define your order in your association. Try the following:
has_many messages, -> { order(:created_at => :asc) }
# change how you need
So when you call chat.messages it will return messages with the given order.
Thank you all, #Rubyrider and #Andrew. I have been able to order the columns as follow:
#chats = current_user.chats.includes(:messages).order("messages.created_at desc").paginate(page: params[:page])
without the inclusion of either .first or .last on messages. Surprisingly, I did not have to specify which of the messages of the chat is to be used for the ordering. I guess ActiveRecord automatically looks and just takes the latest of the associated model to use for the ordering.
The question below had a good answer to grab associated values of an activerecord collection in one hit using Comment.includes(:user). What about when you have multiple associations that you want to grab in one go?
Rails have activerecord grab all needed associations in one go?
Is the best way to just chain these together like below Customer.includes(:user).includes(:sales).includes(:prices) or is there a cleaner way.
Furthermore, when I am doing this on a loop on an index table. Can I add a method on the customer.rb model so that I can call #customers.table_includes etc and have
def table_includes
self.includes(:user).includes(:sales).includes(:prices)
end
For the record I tested the above and it didn't work because its a method on a collection (yet to figure out how to do this).
In answering this, I'm assuming that user, sales, and prices are all associations off of Customer.
Instead of chaining, you can do something like this:
Customer.includes(:user, :sales, :prices)
In terms of creating an abstraction for this, you do have a couple options.
First, you could create a scope:
class Customer < ActiveRecord::Base
scope :table_includes, -> { includes(:user, :sales, :prices) }
end
Or if you want for it to be a method, you should consider making it a class-level method instead of an instance-level one:
def self.table_includes
self.includes(:user, :sales, :prices)
end
I would consider the purpose of creating this abstraction though. A very generic name like table_includes will likely not be very friendly over the long term.