Search code examples
ruby-on-railsrubyruby-on-rails-4domain-data-modelling

Modelling a collection of records that act as as a group


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:

  1. 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.
  2. When ungrouping a member, same history
  3. 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.


Solution

  • 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.