2

I have Users in the system which can be soft_deleted using paranoia (2.0.2) and TimeRecords which keep track of how many :hours a user worked on a given assignment and what their total :cost was for the entire assignment (:cost = :rate * :hours to keep it simple).

These records persist so even if a user's rate is changed in the future, you will still have an accurate snapshot of what they charged for a given task in the past. Enter the soft deletes. A user can be removed from the system using a soft delete (setting deleted_at: Time.now) but I need their name to still show up linked to the :hours and :cost they charged in the past. I have a solution that works but feels too hacky for me and I haven't been able to find a more elegant solution. I'd appreciate any suggestions/help people may have to do this the right way instead of the easy way.

Current solution:

class TimeRecord < ActiveRecord::Base
  belongs_to :user

  delegate :name, to: :user, prefix: true, allow_nil: true

  def name
    user_name || "#{User.with_deleted.find(user_id).name}" rescue 'n/a'
  end
end
David Routen
  • 349
  • 4
  • 21

3 Answers3

2

It's better to pretend that soft deleted records are deleted for real to keep referential integrity. Since you already said a TimeRecord keeps a "Snapshot" of the User at that time, the solution should be clear: add name to the fields you store in TimeRecord as a snapshot.

This is a perfect usecase for this kind of de-normalization even if the User is not allowed to change its name.

David Ongaro
  • 3,568
  • 1
  • 24
  • 36
  • Thanks for the answer David; this is what I'm going to do--I would award you the **answer** for this question but Arjan got there two minutes earlier with roughly the same answer! – David Routen Feb 26 '16 at 16:21
1

If your TimeRecord needs the name, regardless of whether the User still exists or not, then I would recommend storing the user_name on TimeRecord as well as on User instead of delegating it.

When the name changes on the User, I would update the relevant TimeRecords accordingly.

Arjan
  • 6,264
  • 2
  • 26
  • 42
1

If you want to include associated soft-deleted objects, you can simply unscope the association like this:

class TimeRecord < ActiveRecord::Base
  belongs_to :user, -> { with_deleted } # associate soft-deleted user
  delegate :name, to: :user, prefix: true, allow_nil: true
end
Hieu Pham
  • 6,577
  • 2
  • 30
  • 50
  • Thanks for answering Hieu; unfortunately this doesn't seem to work on Rails 4.0.0 (or it could be that I'm doing something wrong). – David Routen Feb 26 '16 at 16:25
  • That query does work, but when calling `#user_name` created by `#delegate`, it returns: TimeRecord Load (0.4ms) SELECT `time_records`.* FROM `time_records` ORDER BY `time_records`.`id` DESC LIMIT 1 User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = 62 ORDER BY `users`.`id` ASC LIMIT 1 => nil – David Routen Feb 26 '16 at 16:45
  • How do you define `user_name`? – Hieu Pham Feb 26 '16 at 16:47
  • `#delegate` does that automatically since I used `prefix: true` (it will create `#user_name`, prefixing `user` in front of `name` because of the class it was delegated to). If `prefix: false`, then it would be called `name`. – David Routen Feb 26 '16 at 16:53
  • how about if you try to change a litle in `TimeRecord` to `belongs_to :user, -> { unscope(where: :deleted_at) }` – Hieu Pham Feb 26 '16 at 17:24