ruby 将Mongoid / MongoDB数据库迁移到基于ActiveRecord的SQL数据库。一种转换架构的方法,另一种转换数据的方法。应对

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ruby 将Mongoid / MongoDB数据库迁移到基于ActiveRecord的SQL数据库。一种转换架构的方法,另一种转换数据的方法。应对相关的知识,希望对你有一定的参考价值。

# Migrate schema and data from Mongoid to MySQL ActiveRecord
class MongoidMysqlMigrator
  
  def randomize_auto_increment_values(source_models, from=5500, to=10500)
    source_models.each do |model|
      value = rand(from-to)+from
      sql = %(ALTER TABLE #{model.name.tableize} AUTO_INCREMENT=#{value})
      puts sql
      ActiveRecord::Base.connection.execute(sql)
    end
  end
  
  # @param sources (Array)  list of Mongoid document types
  def migrate_data(sources)
    sources.each do |source|
      size = source.count
      migrate_source_collection(source, source.all) if size > 0
    end
    
    sources.each do |source_model|
      puts "Resolving foreign keys for #{source_model}"
      target_class = define_active_record_model_class(source_model)
      
      each_belongs_to(source_model) do |relation|
        field_name = relation.foreign_key
        if relation.polymorphic?
          # Get the list of distinct class names from the polymophic association _type
          # fields and perform an update for each
          class_names = target_class.group("#{relation.name}_type").map(&"#{relation.name}_type".to_sym)
          class_names.map(&:tableize).each do |rel_target_table|
            sql = %(UPDATE #{target_class.table_name} t1
                      INNER JOIN #{rel_target_table} t2 ON
                      t2.mongo_id = t1.#{field_name}_mongo_id
                    SET t1.#{field_name} = t2.id)
            #puts sql
            ActiveRecord::Base.connection.execute(sql)
          end
        else
          rel_target_table = relation.class_name.tableize
          sql = %(UPDATE #{target_class.table_name} t1
                    INNER JOIN #{rel_target_table} t2 ON
                    t2.mongo_id = t1.#{field_name}_mongo_id
                  SET t1.#{field_name} = t2.id)
          #puts sql
          ActiveRecord::Base.connection.execute(sql)
        end
      end
      
      # Wire up embedded docs to parent
      each_embedded_relation(source_model) do |relation|
        embedded_class_name = relation.class_name
        embedded_class = embedded_class_name.constantize
        embedded_mysql_class = define_active_record_model_class(embedded_class)
        embedded_ins = embedded_in_relations(embedded_class)
        raise "too many embedded_in relations" unless embedded_ins.size == 1
        
        # There's no foreign key on the embedded doc so we infer it from
        # the relation name. For polymorphic embeds, this might be something
        # like parent_id or addressable_id
        rel_name = embedded_ins.values.first.name
        field_name = "#{rel_name}_id"
        rel_target_table = source_model.name.tableize
        
        sql = %(UPDATE #{embedded_mysql_class.table_name} SET #{field_name}=(
            SELECT id FROM #{rel_target_table} WHERE
                #{rel_target_table}.mongo_id = #{embedded_mysql_class.table_name}.#{field_name}_mongo_id
        ))
        ActiveRecord::Base.connection.execute(sql)
        #puts sql
      end
    end
  end
  
  def truncate_targets(sources)
    sources.each do |source|
      target_class = define_active_record_model_class(source)
      target_class.destroy_all
    end
  end
  
  # @param sources (Array)  list of Mongoid document types
  def migrate_schema(mongoid_models)
    
    # Build MySQL schema
    mongoid_models.each do |mongoid_model|
      table = mongoid_model.name.tableize
      
      # Drop and create SQL table
      migrator.create_table table, :force => true
      
      # Add mongo id column
      migrator.add_column table, :mongo_id, :string
      migrator.add_index table, :mongo_id
      
      # Add data fields
      each_basic_field(mongoid_model) do |field|
        migrate_field_to_table(table, field)
      end
      
      # Add foreign key fields
      mongoid_model.relations.each do |rel_name, relation|
        if relation.relation == Mongoid::Relations::Referenced::In ||
           relation.relation == Mongoid::Relations::Embedded::In
          field = relation.foreign_key unless Mongoid::Relations::Embedded::In
          field ||= "#{relation.name}_id"
          migrator.add_column table, field, :integer
          migrator.add_index table, field, :name => field
          if relation.relation == Mongoid::Relations::Embedded::In && relation.polymorphic?
            type_field = "#{relation.name}_type"
            migrator.add_column table, type_field, :string
            migrator.add_index table, type_field, :name => type_field
          end
          migrator.add_column table, field+"_mongo_id", :string, :length => 30
          migrator.add_index table, field+"_mongo_id", :name => field+"_mongo_id"
        end
        
        if relation.relation == Mongoid::Relations::Embedded::One
          # See if we need to convert this embeds_one into a composed_of
          klass = relation.class_name.constantize
          #if composable_types.include? [relation.inverse_class_name.constantize, klass]
            prefix = relation.name
            # Migrate all the embedded classes fields into the target
            # table but with prefixed field names
            each_basic_field(klass) do |field|
              migrate_field_to_table(table, field, "#{prefix}_#{field.name}")
            end
          #end
        end
      end
      
      # Carrier wave uploaders
      each_uploader(mongoid_model) do |field, uploader_type|
        migrator.add_column table, field, :string
      end
    end
  end
  
  private
  
  def migrate_field_to_table(table, mongoid_field, field_name = nil)
    indexed_fields = mongoid_field.options[:klass].index_options.keys.map(&:keys).map(&:first)
    
    toSqlType = {
      String => :string,
      Time => :datetime,
      Integer => :integer,
      Boolean => :boolean,
      ::Money => [:decimal, :precision => 8, :scale => 2, :default => 0.00],
      BigDecimal => [:decimal, :precision => 8, :scale => 3, :default => 0.000],
      Date => :date,
      Hash => :text,
      Array => :text
    }
    
    field_name ||= mongoid_field.name
    type = mongoid_field.type
    sql_type = toSqlType[type]
    default = mongoid_field.options[:default]
    default = nil if default.is_a?(Proc) || sql_type == :text
    null = default.nil?
    null = true if sql_type == :text
    
    # Switch some String colums up to text
    if sql_type == :string &&
      (field_name.include?('notes') ||
      field_name == 'value' ||
      field_name == 'content' ||
      field_name.include?('_footer') ||
      field_name.include?('_extra_text'))
      
      sql_type = :text
      default = nil
      null = true
    end
    
    column_options = {
      :default => default,
      :null => null
    }
    
    if sql_type.is_a?(Array)
      column_options.merge!(sql_type[1])
      sql_type = sql_type.first
    end
    
    if sql_type
      migrator.add_column table, field_name, sql_type, column_options
      if indexed_fields.include? field_name.to_sym
        migrator.add_index table, field_name, :name => field_name
      end
    else
      puts colorize("Skipped field #{table}##{field_name} with unknown type #{type}", COLORS[:red])
    end
  end
  
  def embedded_in_relations(model)
    model.relations.select { |k,v| v.relation == Mongoid::Relations::Embedded::In }
  end
  
  # @param source_model (Class)  the Mongoid model class
  # @param collection (Array)  the model instances to migrate 
  def migrate_source_collection(source_model, collection)
    target_class = define_active_record_model_class(source_model)
    #target_class.destroy_all
    
    collection.each do |source|
      target = migrate_source_document(source, target_class)
      
      # Now step through embedded singletons and collections, converting those...
      each_embedded_relation(source_model) do |relation|
        puts "Migrating embedded #{source_model} #{source.id} #{relation.name}"
        embedded_class = relation.class_name.constantize
        
        if relation.relation == Mongoid::Relations::Embedded::One# &&
#          composable_types.include? [source_model, embedded_class]
          puts "Migrating to aggregation #{target.class} #{embedded_class}"
          # it's to be converted to a composed_of
          prefix = relation.name
          embedded_doc = source.send(relation.name)
          if embedded_doc
            each_basic_field(embedded_class) do |field|
              copy_field_value(embedded_doc, target, field, "#{prefix}_#{field.name}")
            end
          end
          target.save!
        else
          value = source.send(relation.name)
          value = [value] if relation.relation == Mongoid::Relations::Embedded::One
          value = value.compact # Embedded document might not exist
          migrate_source_collection(embedded_class, value) do |target|
            # No foreign key back to the parent document exists for
            # embedded documents so we need to set it manually here
            foreign_key_name = source_model.name.tableize.singularize+'_id'
            target.send("#{foreign_key_name}_mongo_id=", source.id.to_s)
          end
        end
      end
    end
  end
  
  def copy_field_value(source, target, mongoid_field, target_field_name = nil)
    target_field_name ||= mongoid_field.name
    type = mongoid_field.type
  
    value = source.send(mongoid_field.name)
    value = value.to_s if type == ::Money
      
    target.send("#{target_field_name}=", value)
  end
  
  def migrate_source_document(source, target_class)
    source_class = source.class
    target = target_class.new
    puts "Migrating #{source.class} #{source.id}..."

    # Store Mongo document ID
    target.mongo_id = source.id.to_s
    
    # Copy fields
    each_basic_field(source_class) do |field|
      copy_field_value(source, target, field)
    end
    
    # For each relation, we store the Mongo document id in
    # a temporary <key field>_mongo_id string column
    each_belongs_to(source_class) do |relation|
      field_name = relation.foreign_key
      value = source.send(field_name).to_s
      target.send("#{field_name}_mongo_id=", value)
    end
    
    each_embedded_in(source_class) do |relation|
      field_name = "#{relation.name}_id"
      value = source.send(relation.name).id.to_s
      target.send("#{field_name}_mongo_id=", value)
      if relation.polymorphic?
        target.send("#{relation.name}_type=", source.send(relation.name).class.name)
      end
    end
    
    each_uploader(source_class) do |field, uploader_type|
      target.send("#{field}=", source.send('[]', field))
    end
    
    target.save!
    target
  end
  
  # Yield block for every basic (not key, uploader etc) field
  def each_basic_field(mongoid_model, &block)
    mongoid_model.fields.each do |field_name, field|
      unless field.name[0] == '_' ||                              # _type or _id
        field.name.ends_with?('_id') && field.type == Object ||   # foreign key
        mongoid_model.uploaders.keys.include?(field_name.to_sym) || # uploader
        field.type == Symbol # something to do with polymorphic relations e.g. commentable_field
        yield(field)
      end
    end
  end
  
  def each_belongs_to(mongoid_model, &block)
    # Add foreign key fields
    mongoid_model.relations.each do |rel_name, relation|
      if relation.relation == Mongoid::Relations::Referenced::In
        yield(relation)
      end
    end
  end
  
  def each_embedded_relation(mongoid_model, &block)
    # Add foreign key fields
    mongoid_model.embedded_relations.each do |rel_name, relation|
      yield(relation)
    end
  end
  
  def each_embedded_in(mongoid_model, &block)
    # Add foreign key fields
    embedded_in_relations(mongoid_model).each do |rel_name, relation|
      yield(relation)
    end
  end
  
  def each_uploader(mongoid_model, &block)
    mongoid_model.uploaders.each do |field, uploader_type|
      yield(field, uploader_type)
    end
  end
  
  def define_active_record_model_class(mongoid_model)
    @_ar_class_cache ||= {}
    arclass = @_ar_class_cache[mongoid_model]
    return arclass if arclass
    target_class_name = "Mysql#{mongoid_model.name}"
    # Define an ActiveRecord model class
    target_class = Class.new(ActiveRecord::Base) do
      set_table_name mongoid_model.name.tableize
    end
    Kernel.const_set(target_class_name, target_class)
    @_ar_class_cache[mongoid_model] = target_class
    #target_class
  end
  
  def migrator
    @_migrator ||= ActiveRecord::Migration.new
  end
  
  def colorize(text, color_code)
    "\033[#{color_code}m#{text}\033[0m"
  end

  COLORS = {
    :black => 30,
    :red => 31,
    :green => 32,
    :yellow => 33,
    :blue => 34,
    :magenta => 35,
    :cyan => 36,
    :white => 37,

    :warn => 36,
    :error => 35
  }
  
end

以上是关于ruby 将Mongoid / MongoDB数据库迁移到基于ActiveRecord的SQL数据库。一种转换架构的方法,另一种转换数据的方法。应对的主要内容,如果未能解决你的问题,请参考以下文章

Ruby on Rails 是 mongodb - mongoid

rails查询mongodb通用查询

Ruby on Rails - Criteria - Mongoid - where 条件为 2 x 2 列

ruby Mongoid数据库可缓存

使用 Mongoid 批量插入/更新?

如何将内存中的 MongoDB 与 Rails、Mongoid 和 Rspec 一起使用?