clowne-rb / clowne

A flexible gem for cloning models
https://clowne.evilmartians.io
MIT License
316 stars 18 forks source link

Is it possible to clone a record to a different table? #54

Open gee-forr opened 4 years ago

gee-forr commented 4 years ago

Hey there,

I've got a requirement that requires me to keep a point-in-time snapshot of some data whose associations and attributes change over time.

In order to go back to old records and understand the makeup of it at that point in time, we need to make sure that once that record is at a certain stage, it has a snapshot taken of it, and all its associations, and values of all their attributes.

I really don't want to have to do this manually, and would love to use something like Clowne. In order for me to do that though, I'll need to clone my records to their equivalent *Snapshot models instead of the standard models. Here's a non-real-world example in case I'm not making too much sense:

As we either release new modules for a course, or remove old modules, or update existing modules, I don't want a certificate's scores to be affected.

My goal is that once each module is completed, all the data is snapshot from its CourseModule to a CourseModuleSnapshot record. I'd like to probably do a little bit of postprocessing too, but my main goal is to clone the model to a different but similar model.

Is this at all possible?

BTW - thank you evil martians for all the amazing open source you put out ❤️

ssnickolay commented 4 years ago

Hey @gee-forr . Hm, you have an interesting case...AFAIU you could try to do something like this:

class Course < ApplicationRecord
  has_many :course_modules

  # other associations
end

class CourseSnapshot < Course
  self.table_name = 'course_snapshots'

  # foreign key to keep tables equal between each other
  has_many :course_modules, class_name: 'CourseModuleSnapshot', foreign_key: :course_id
  # other associations with overrided class_name & foreign_key options
end

class CourseModule < ApplicationRecord; end

class CourseModuleSnapshot < CourseModule
  self.table_name = 'course_module_snapshots'
end

# Magic here:
class SnapshotAdapter < Clowne::Adapters::ActiveRecord
  class << self 
    def dup_record(record)
      # we cannot mix snapshot and origin models because of https://api.rubyonrails.org/classes/ActiveRecord/AssociationTypeMismatch.html
      # so we override dup logic to always have snap models
      # if you have witelisted models that should be cloned as is 
      # you can `return super if %w(SomeModel).include?(record.class)`
      target_class = "#{record.class.name}Snapshot".constantize

      target_class.new(record.dup.attributes).tap do |clone|
        # you can skip these lines if you dont use after_persist hook (see https://clowne.evilmartians.io/#/after_persist)
        operation = operation_class.current
        operation.add_mapping(record, clone)
      end
    end
  end
end

class CourseSnapshoter < BasePostCloner
  adapter SnapshotAdapter # override default adapter

  include_association :course_modules
end

# and use

CourseSnapshoter.call(Course.last).to_record
# => <# CourseSnapshote ... >

I checked locally with a similar example and everything seems to work

gee-forr commented 4 years ago

OMG! This is amazing! Thank you so much!

bugthing commented 1 year ago

@ssnickolay thanks answering this 2 years back :laughing: .. it has partially addressed a problem I am having trying to use clowne. When I clone an AR model into a different one (as demoed above), I also need to rename an association.. is this possible?