Extract all data about a particular user with ActiveRecord

I want to extract all the data of a specific user!

When running a service, on rare occasions you may want to extract all the data about a particular user. I'm sure the day will come when we will work on horizontal division and recovery of data that has been physically deleted.

For those of you who have to do such a troublesome task, this time we will introduce how to extract and import data for a specific user.

Step 1. Get all the data associated with a particular user

If you use ActiveRecord, all tables are associated in the model layer. Therefore, if you trace the relationship starting from the User class, you should be able to pull all the necessary data.

This time, I will use my own active_record_depth_query.rb to pull related records in one shot. By using this, you can search the foreign key and get the record by passing Array/Hash as an argument like the preload that you usually use.

In no time, I was able to extract all the records about the user. Easy.

depth_query = ActiveRecordDepthQuery.new(user, [:addresses, { cart: :items, orders: [:line_items, :address }]])
depth_query.each do |relation|
  relation #=>ActiveRecord of the record specified by the second argument::Relation comes in turn
end

Step 2. Copy the extracted data to another DB

Introduce the process of copying the data extracted in step 1 to another DB. This time, I used Rails 6's multiple DB function and activerecord-import, so if you have a different environment, please rewrite it as appropriate.

In the following process, in order to move the data from the old DB to the new DB, After extracting records from the old DB, I switch the connection to the new DB and then bulk insert.

Since the work is simple, I was able to extract and save the data of a specific user with the code below.

require 'activerecord-import'

class RecordsFromBackupDatabase
  #Related list to be imported
  USER_ASSOCIATIONS = [:addresses, { cart: :items, orders: [:line_items, :address] }].freeze

  #Old DB connection destination
  OLD_DATABASE_HOST = 'old-database.host'

  #New DB connection destination
  NEW_DATABASE_HOST = 'new-database.host'

  # @param user_ids [Array<Integer>]User ID to be imported
  # 
  # @return [void]
  def perform(user_ids)
    #Old DB/Set up a new DB. Work on the rails console instead of the web server as it affects the sharing process
    reconnect_database

    ActiveRecord::Base.transaction do
      #Connect to old DB
      connected_to_old_database do
        users = User.where(id: user_ids)

        #Recover users first
        import_relation(users)

        users.find_each do |user|
          #Recover data for each user
          restore_user(user)
        end
      end
    end
  end

  private

  def reconnect_database
    #Disconnect the connected DB
    ActiveRecord::Base.connection.disconnect!

    # writing/Define two systems of reading so that you can switch between old and new DB
    config = ActiveRecord::Base.connection_pool.spec.config
    ActiveRecord::Base.configurations = {
      "#{Rails.env}": config.merge(host: NEW_DATABASE_HOST, replica: false),
      "#{Rails.env}_replica": config.merge(host: OLD_DATABASE_HOST, replica: true)
    }

    ActiveRecord::Base.connects_to(
      database: {
        writing: Rails.env.to_sym,
        reading: :"#{Rails.env}_replica"
      }
    )
  end

  def connected_to_old_database(&block)
    ActiveRecord::Base.connected_to(role: :reading, &block)
  end

  def connected_to_new_database(&block)
    ActiveRecord::Base.connected_to(role: :writing, &block)
  end

  def restore_user(user)
    #Search user associations in sequence and import records from the old DB into the new DB
    ActiveRecordDepthQuery.new(user, USER_ASSOCIATIONS).each do |relation|
      relation.find_in_batches do |records|
        klass = relation.klass

        #Import records into production DB
        connected_to_new_database do
          klass.import(records, validate: false)
        end
      end
    end
  end
end

Summary

This time, I introduced ActiveRecordDepthQuery, which extracts records with a preload-like interface, and how to recover data using it. Hopefully, this process won't be used again, but if you need such an operation, please use it.

Recommended Posts

Extract all data about a particular user with ActiveRecord
How to clear all data in a particular table
Extract a part of a string with Ruby
A story about creating a library that operates next-generation sequencer data with Ruby ruby-htslib
Extract subqueries from Relation objects assembled with ActiveRecord
About getting a tens digit with "(two-digit integer) / 10% 10"
Creating a common repository with Spring Data JPA