Update parent model total as new child created - ruby-on-rails

I have a package model which has_many sales.
I'd like to sum up all the sales revenue and update the package model's total_revenue after each new sale.
How do I do that?

You want to use an active record callback. I would probably use after_create. You can add code like this:
class Sale < ActiveRecord::Base
belongs_to :package
after_create :update_package_revenue
def update_package_revenue
package.update(total_revenue: package.sales.sum(:revenue)) # substitute the correct code here
end
end
This allows you to run code every time you create a new sale.

A simplistic way to accomplish this would be with a setup like:
class Package < ActiveRecord::Base
has_many :sales
def calculate_and_update_total_revenue
update_attributes(:total_revenue, sales.sum(:revenue))
end
end
class Sales < ActiveRecord::Base
belongs_to :package
after_create :update_parent_package
private
def update_parent_package
package.calculate_and_update_total_revenue
end
end
It gives you a race-condition resistan method to bump the total (you could just add a +self.revenue for each new sale, but then you'd be modifying a global state from multiple execution contexts.
Still, if you write logic like this you'll end up with fat models, really hard to manage in a big application.
What about using mediator objects instead?

Related

Consistently using associated model subclasses

I have a situation where I have basic models that I want to add business logic to. For example, I might have something like this.
class List < ApplicationRecord
has_many :subscriptions
has_many :subscribers, though: :subscriptions
end
class Subscriber < ApplicationRecord
has_many :subscriptions
has_many :lists, through: :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :list
belongs_to :subscriber
end
Subscribing and unsubscribing is easy via the normal association methods.
# Subscribe
list.subscriptions.create(
subscriber: subscriber
)
# Unsubscribe
list.subscriptions.destroy(subscription)
# Unsub from all lists
subscriber.subscriptions.destroy_all
But there's logging and tracking and metrics and hooks and other business logic. I could do this with callbacks. However I'd like to keep the basic models simple and flexible. My desire is to separate the core functionality from the extra business logic. Right now this is to simplify testing. Eventually I'll need to add two different sets of business logic on top of the same core.
Currently I'm using a service object to wrap common actions with all the current business logic. Here's a simple example, there's a lot more.
class SubscriptionManager
def subscribe(list, subscriber)
list.subscriptions.create( subscriber: subscriber )
log_sub(subscription)
end
def unsubscribe(subscription)
subscription.list.subscriptions.destroy(subscription)
log_unsub_reason(subscription)
end
def unsubscribe_all(subscriber)
subscriber.subscriptions.each do |subscription|
unsubscribe(subscription)
end
subscriber.lists.reset
subscriber.subscriptions.reset
end
end
But I'm finding it increasingly awkward. I can't use the natural subscriber.subscriptions.destroy_all, for example, but must be careful to go through the SubscriptionManager methods instead. Here's another example where this system caused a hard to find bug.
I'm thinking about eliminating the SubscriptionManager and instead writing subclasses of the models which have the extra logic in hooks.
class ManagedList < List
has_many :subscriptions, class_name: "ManagedSubscription"
has_many :subscribers, though: :subscriptions, class_name: "ManagedSubscriber"
end
class ManagedSubscriber < Subscriber
has_many :subscriptions, class_name: "ManagedSubscription"
has_many :lists, through: :subscriptions, class_Name: "ManagedList"
end
class ManagedSubscription < Subscription
belongs_to :list, class_name: "ManagedList"
belongs_to :subscriber, class_name: "ManagedSubscriber"
after_create: :log_sub
after_destroy: :log_unsub
end
The problem is I'm finding I have to duplicate all the associations to guarantee that Managed objects are associated to other Managed objects.
Is there a better and less redundant way?
I don't really understand why do you need to define the associations again in the subclasses. However, I have a tip that you could use directly in your Subscription model.
If you want to keep your model simple, and don't overload it with callbacks logic, you can create a callback class to wrap all the logic that will be used by the model.
In order to do that, you need to create a class, for example:
class SubscriptionCallbacks
def self.after_create(subscription)
log_sub(subscription)
end
def self.after_destroy(subscription)
log_unsub_reason(subscription)
end
end
Then in Subscription model:
class Subscription < ApplicationRecord
belongs_to :list
belongs_to :subscriber
after_destroy SubscriptionCallbacks
after_create SubscriptionCallbacks
end
That way, your model stand clean and you can destroy a subscription and apply all custom logic without using a service.
UPDATE
Specifically, what I don't understand is why are you making Single Table Inheritance on three models just to add callbacks to one of them. The way you wrote your question, for the three subclasses you override the associations to use the subclasses created. Is that really necessary? I think that no, because what you want to achieve is just refactor your service as callbacks in order to use destroy and destroy_all directly in the Subscription model, I take that from here:
But I'm finding it increasingly awkward. I can't use the natural subscriber.subscriptions.destroy_all, for example, but must be careful to go through the SubscriptionManager methods instead.
Maybe with conditional callbacks is enough, or even just normal callbacks on your Subscription model.
I don't know how the real code is wrote, but I found tricky to use Single Table Inheritance just to add callbacks. That doesn't make your models "simple and flexible".
UPDATE 2
In a callback class, you define methods with the name of the callback that you want to implement, and pass the subscription as a parameter. Inside that methods, you can create all the logic that you want. For example (assuming that you will use different logic given a type attribute):
class SubscriptionCallbacks
def after_create(subscription)
if subscription.type == 'foo'
log_foo_sub(subscription)
elsif subscription.type == 'bar'
log_bar_sub(subscription)
end
end
private
def log_foo_sub(subscription)
# Here will live all the logic of the callback for subscription of foo type
end
def log_bar_sub(subscription)
# Here will live all the logic of the callback for subscription of bar type
end
end
This could be a lot of logic that will not be wrote on Subscription model. You can use destroy and destroy_all as usual, and if a type of subscription is not defined in the if else, then nothing will happen.
All the logic of callbacks will be wrapped in a callback class, and the only peace of code that you will add to the subscription model will be:
class Subscription < ApplicationRecord
belongs_to :list
belongs_to :subscriber
after_create SubscriptionCallbacks.new
end

Rails 4.1 nested model form fields

Booking -< Orders -< Transactions
class Booking < ActiveRecord::Base
has_many :orders
end
class Order < ActiveRecord::Base
belongs_to :booking
has_many :transactions
end
class Transaction < ActiveRecord::Base
belongs_to :order
end
I need to be able to create a Transaction without an Order or Booking existing.
I'm trying to achieve the following:
When a Transaction is created an Order and a Booking is automatically created. The transaction form can take a Booking.booking_number which will be saved to the above automatically created Booking.
I'm very new to rails and have tried a combination of accepts_nested_attributes_for, Ryan Bates' nested model form part1 screencast and form_fields_for without success.
Some guidance, not necessarily code, would be much appreciated.
My routes look like:
I need to be able to create a Transaction without an Order or Booking
existing.
Bad system design - surely a transaction would follow an order or booking?
From your question, I'd highly recommend creating a booking or order first. This will allow you to create a transaction as a bolt-on to the order or booking:
#app/controllers/bookings_controller.rb
Class BookingsController < ApplicationController
def create
booking = Booking.new(booking_params)
booking.save
end
end
#app/models/booking.rb
Class Booking < ActiveRecord::Base
before_create :build_transaction #-> creates a blank transaction which can be populated later
end
Nonetheless, there's nothing stopping you creating a transaction & assigning an order later
You can do this:
#app/controllers/transactions_controller.rb
def create
Transaction.new(transaction_params)
end
#app/models/transaction.rb
Class Transaction < ActiveRecord::Base
after_create :order
def order
self.order.create!([order_details?])
end
end
If you tell me some more about what you're building, I'll be able to create a more refined response!
Try this it may be work.
In your model
accepts_nested_attributes_for :order, :allow_destroy => true
change whether true/false depending on your form

Complex After save association in ruby on rails

Theory :- after create of a record in customer bill, i am sending two sets of data two different models. one set of data is sent to ledger and one set of data is sent to ledger_line_item. the complexity is that after sending of data i want the ledger_id to be stored in ledger_line_item. the code is as follows
code :-
class CustomerBill < ActiveRecord::Base
after_create :creating_ledger_line_items, :creating_ledger_items
def creating_ledger_items
CustomerLedger.create(:customer_id =>self.customer_id,/*rest of attributes*/)
end
def creating_ledger_line_items
CustomerLedgerLineItem.create(:customer_id =>self.customer_id,/*rest of attributes*/)
end
end
in ledger i have written
class CustomerLedger < ActiveRecord::Base
after_save :update_record_line_items
def update_record_line_items
a = CustomerLedgerLineItem.find_by_customer_id(self.customer_id)
a.update_attributes(:customer_ledger_id => self.id)
end
end
the above code works fine without error but the ledger_id is not been posted in ledger_line_items. i am not able to determine why this error is happening? is there any other way i can achieve my goal of posting ledger_id in ledger_line_items after a bill is created?
Guidance Required. Thanking you in advance.
You can change your models something as follows.:
I am assuming you have Customer Model.
class Customer < ActiveRecord::Base
has_one :customer_ledger
has_many :customer_ledger_line_items, :through => :customer_ledger
accepts_nested_attributes_for :customer_ledger
end
class CustomerLedger < ActiveRecord::Base
has_many :customer_ledger_line_items
accepts_nested_attributes_for :customer_ledger_line_items
end
class CustomerBill < ActiveRecord::Base
belongs_to :customer
after_create :creating_ledger_items, :creating_ledger_line_items
def creating_ledger_line_items
cl = self.customer.customer_ledger.build(your_attributes)
cl.save!
end
def creating_ledger_items
cli = self.customer.customer_ledger.customer_ledger_items.build(your_attributes)
cli.save!
end
end
In case you want to create the models on an *after_create* hook, I'll explain what's the problem.
When you create a model in rails, and you have hooks like *after_create*, *before_update*, etc. all the updates happens in a Transaction, so if any of them throws an exception, nothing is updated.
In this case, within a Transaction, you are trying to get the ID of a CustomerLedger that doesn't exists yet, because since everything is within a Transaction, the record is not saved to the database until the transaction is executed, and thats the reason that on CustomerLedger#update_record_line_items, self.id is always nil.
Using the nested attributes proposed by codeit is probably the best solution to your problem, but if you feel that nested attributes its an advance topic, you can do something like:
class CustomerBill < ActiveRecord::Base
after_create :created_leder_data
def create_ledger_data
customer_ledger = CustomerLedger.build(customer_id: self.customer_id, # Rest of attributes)
customer_ledger.customer_ledger_line_items.build(customer_id: self.customer_id, # Rest of attributes)
customer_ledger.save!
end
end

How to add methods to a has_many collection

Assuming a typical has_many association
class Customer < ActiveRecord::Base
has_many :orders
end
class Order < ActiveRecord::Base
belongs_to :customer
end
How can I add a method to the collection of orders? For the sake of code organization, I'm trying to reactor this method (this is a made-up example) inside of my Customer class:
def update_orders
ThirdPartyAPI.look_up(self.orders) do |order|
# Do stuff to the orders
# May need to access 'self', the Customer...
end
end
I don't like this because it puts a lot of knowledge about the Order class inside my Customer class. I can't use an instance method off of an order, since the ThirdPartyAPI can do a batch lookup on multiple orders. I could make a static method off of Order and pass in the array of orders, and their parent customer, but this feels clunky.
I found this in the rails docs, but I couldn't find any good examples of how to use this in practice. Are there any other ways?
I think this should do it
has_many :entities do
def custom_function here
end
def custom_function here
end
end

In Rails, how to calculate a value based on a set of child records and store it in the parent record

I have invoices that are made up of invoice items. Each item has a profit and I want to sum these up and store the total profit at the invoice level.
Previously I was doing this calculation on-the-fly, but to improve performance I now need to store this value in the database.
class InvoiceItem < ActiveRecord::Base
belongs_to :invoice
end
class Invoice< ActiveRecord::Base
has_many :invoice_items
def total_profit
invoice_items.sum(:profit)
end
end
I want the total_profit to always be correct, so it needs to be updated whenever an invoice item is added, edited or deleted. Also the total_profit probably should be protected from being directly edited.
you may try the 'after create', 'after save' and 'before destroy' callback methods to add or subtract the amount from the parents total profit. In this way your parent object will be updated only if changes are made to the invoice items.
Best regards,
Joe
edit:
to give you some untested pseudocode hints:
class InvoiceItem < ActiveRecord::Base
belongs_to :invoice
before_destroy { |item| item.invoice.subtract(item.amount) }
after_create { .. }
after_save { .. }
end
Joe's on the right track, but his answer doesn't address all your issues. You also need to set up the total_profit attribute in the Invoice. First you'll need to add the field with the appropriate migration. Then you'll want to protect that attribute with
attr_protected :total_profit
Or better yet:
attr_accessible ALL_NON_PROTECTED_ATTRIBUTES
It also doesn't hurt to set up a means of forcing a recalculation of the total_profit as well. In the end you'd have something like this:
class Invoice < ActiveRecord::Base
has_many :invoice_items
attr_protected :total_profit
def total_profit(recalculate = false)
recalculate_total_profit if recalculate
read_attribute(:total_profit)
end
private
def recalculate_total_profit
new_total_profit = invoice_items.sum(:profit)
if new_total_profit != read_attribute(:total_profit)
update_attribute(:total_profit, new_total_profit)
else
true
end
end
end
Of course this may be a bit overkill for your specific application but hopefully it gives you some ideas of what may be best for you.
So my solution was as Peter suggested adding the total_proft to Invoices with the appropriate migration.
Then as Johannes suggested, I used ActiveRecord::Callbacks on my child model:
class InvoiceItem < ActiveRecord::Base
belongs_to :invoice
def after_save
self.update_total_profit
end
def after_destroy
self.update_total_profit
end
def update_total_profit
self.invoice.total_profit = self.invoice.invoice_items.sum(:profit)
self.sale.save
end
end
class Invoice< ActiveRecord::Base
has_many :invoice_items
def total_profit
invoice_items.sum(:profit)
end
end
PLEASE NOTE: For some reason, the above code does not work when when creating an invoice and invoiceitem together. It starts ok, an INSERT SQL statement fires first for the Invoice. Then with the new Invoice ID, the InvoiceItem record can be saved. However after this my above code triggers the query ...
SELECT sum(`invoice_items`.profit) AS sum_profit
FROM `invoice_items`
WHERE (`invoice_items`.invoice_id = NULL)
For some reason the invoice_id is NULL, even though it has just been used to insert the invoice_item.

Resources