Need a replacement for `after_load` callback!

Ok, so I know after_load and all the other callbacks are generally a bad idea, but I genuinely have a use case for after_load now and I’m struggling to find a replacement.

So, we have a database table that stores a billing term (monthly, quarterly, annually), and the start and end dates of the current term. What I need to do is, every time a record is loaded, to ensure that the start and end dates are still valid, and if not, to calculate the correct dates and set those correct values.

I can’t just add this as part of a query function because these records are sometimes loaded as an association. This can’t be a virtual field because we need to persist the last start date in order to calculate the next one. And I also can’t use a custom Ecto.Type because I need the billing term, which wouldn’t be passed to my cast or load functions.

Right now the only idea I’ve come up with, which I really don’t like, is to have some sort of chron job that just goes through and updates these fields daily. This is kind of overkill since this data might not be shown often, so that’s a lot of wasted computing power there. This really should be able to be done lazily as each record is loaded.

So, what’s a dev to do without an after_load callback here?

  • What do you mean by “start date” or “end date” here?

  • How can they somehow become “invalid” out of the blue without anybody mutating them?

  • …Or is another system updating those fields under your feet without telling you?

  • Why would you do that on every single load of those fields? That’s wasting much more computing power compared to a daily cron job.

Give us more context.

So the start date is the beginning of a billing cycle, and the end date is the end of a billing cycle. For example, if the billing term is monthly, a start date might be 2018-1-1, and the end date would be 2018-1-31.

This would then be invalid because time moves forward. When the record is loaded sometime in February, the start date would need to be updated to 2018-2-1, and the end date would need to be updated to 2018-2-28.

These records aren’t shown all that often, even though there are many of them. On any given day 99.999% of records wouldn’t need any change, so a daily chron job would be very wasteful.

You could just select the ones that need to change from the db and update just those I’d imagine. I mean it it’s based on days there’s no point in checking more often than once (or a few times for a global audience) a day. At that time you just update all records that need updates and be done for the day.

So are these date fields basically stating “this is when I was last used in task X”?

Also, it looks like you already can access the records you need without dates. So can we understand why do you update these date fields at all?

I think you could do it with a database view…

I think a better solution is to use recurrence rules and dynamically compute the valid billing term right when it’s requested. Even if the date is 1/1/2015, you can derive a valid term for right now using a recurrence rule. At the same time, you can have a function that sends the record to a service that checks if it’s invalid and update if necessary i.e.

def update_if_invalid(billing_term) do
    TermValidityService.maybe_update_term(billing_term) # do this asynchronously
    billing_term
end

So that the next time it is computed, not as much work has to be done.

1 Like