RAILS中利用YAML文件完成数据对接

最近在作的Ruby on Rails项目中,须要将远程数据库中的数据对接到项目数据库中,可是远程的数据不只数据表名跟字段命名奇葩,数据结构自己跟项目数据结构出入比较大,在数据导入过程当中代码经历了几回重构,最后使用了YAML文件解决了基本数据1对接的问题。在此写一篇博文,我会尽可能重现一路过来的代码变动,算是分享一下个人思考过程,也算是祭奠一下本身的苦逼岁月。html

假设以及数据结构预览

由于远程数据库服务器为Oracle Server,我在项目中使用到了Sequel这个gem用于链接数据库以及数据查询,由于数据库链接的内容不是本文的重点,故后续代码直接用remote_database表示数据库链接,而根据Sequel的用法,咱们能够直接使用remote_database[table_name]链接到具体的表。数据库

本次须要从远程数据库中导入的基本数据主要有学生信息表(包含班级名称)、老师信息表以及专业信息表,相应地,项目中(如下称为“本地”)也已经建立好了对应的model。其中学生信息表的表名以及部分数据字段的从本地到远程的映射关系如表所示:服务器

表名或字段名 本地 远程
表名 students XSJBXX
姓名 name XM
学号 number XH
年级 grade NJ
班级 belongs_to :klass     BJMC(班级名称)

老师信息表的表名以及部分数据字段的映射关系为:数据结构

表名或字段名 本地 远程
表名 teachers JZGJBXX
姓名 name XM
职称 title ZC
证件号码 id_number ZJHM

数据对接初版:属性方法显式赋值

第一个导入的数据表是学生的信息表,在最开始的时候,由于只须要考虑一张单独的表,因此代码写得简单粗暴,基本过程就是:根据须要的信息,查询对应的远程数据字段,而后使用属性方法赋值,最后保存接入的数据。对接方法的部分相关代码示例(为了方便阅读以及保护项目敏感信息,本文对项目中原有代码进行了缩减以及修改):oracle

# app/models/student.rb
class Student < ActiveRecord::Base
  def import_data_from_remote
    remote_students = remote_database[:xsjbxx].page(page)

    remote_students.each do |remote_student|
      name, number, grade = *remote_student.values_at(:xm, :xh, :nj)
      class_name = remote_student[:bjmc]

      klass = Klass.find_or_create_by name: class_name
      student = Student.find_or_create_by name: name,
                                          number: number,
                                          grade: grade,
                                          klass: klass
    end
  end
end

上面的代码,呃,中规中矩,基本体现了各取所需的指导思想,可是总以为怎么有点很差呢?app

数据对接第二版:经过本地到远程数据库字段映射关系自动匹配赋值

在初版的代码中,最大的坏味道在于:代码中须要把全部须要对接的字段列举出来,一旦遇到字段增删修改的状况,就须要同时更新原来的逻辑代码,太不灵活了,并且列举全部字段自己就是一件很是繁琐枯燥的事情。再假设字段不少的状况下,要从代码中一个个检查字段的名称,确定是件多么可怕的事情啊。less

那么怎么修改呢?用映射表!仔细观察第一段的代码,其实代码所作的工做如此简单:无非是先从远程数据中取值,而后赋值到本地数据对象的对应属性中,这种“本地-远程”的字段映射关系,不就是咱们天天面对的“键-值”对的特征吗?那直接用一个Hash来保存这种对应关系不就行了。.net

话很少说,咱们开始重构:code

# app/models/student.rb
class Student < ActiveRecord::Base
  LOCAL_TO_REMOTE_FIELDS_MAP = {
    number: :xh,
    name: :xm,
    age: :nj
  }

  LOCAL_TO_REMOTE_ASSOCIATION_MAP = {
    klass: {
      association_field_name: :name,
      remote_field_name: :bjmc
    }
  }

  def import_data_from_remote
    remote_students = remote_database[:xsjbxx].page(page)

    remote_students.each do |remote_student|
      student = Student.find_or_initialize_by xxx: xxx
      LOCAL_TO_REMOTE_FIELDS_MAP.keys.each do |attribute|
        # 逐一调用属性赋值方法,完成Student属性的赋值
        student.send("#{attribute}=", remote_student[LOCAL_TO_REMOTE_FIELDS_MAP[attribute]])
      end

      LOCAL_TO_REMOTE_ASSOCIATION_MAP.each do |association_name, association_fields_map|
        # 把远程数据赋给对应的本地数据字段
        association_field_name = association_fields_map[:association_field_name]
        remote_value = remote_student[association_fields_map[:remote_field_name]]

        # 查找或建立关联对象
        related_object =
          reflect_on_association(association_name).klass.find_or_create_by association_field_name => remote_value
        # 创建关联关系
        local_object.send("#{association_name}=", related_object)
      end

      student.save
    end
  end
end

在上面的示例中,咱们用常量LOCAL_TO_REMOTE_FIELDS_MAP保存Student这个model自己的字段跟远程数据字段的映射关系,这样咱们就能够经过相似LOCAL_TO_REMOTE_FIELDS_MAP[:number]知道学生的姓名在远程数据表中对应的字段是:xm了。另外值得一提的是,我用了LOCAL_TO_REMOTE_ASSOCIATION_MAP这个常量保存了学生与班级关联关系,同时保存了关联的klass的数据字段映射关系。htm

在声明了必要的字段映射关系以后,我就在代码中遍历了每个字段,而且经过对应的远程字段名称查找对应的数值,而且使用send方法调用了对象的属性赋值方法,将数据自动对接到本地数据对象上。

到目前为止,代码行数虽然反而多了,可是却实现了字段映射关系与逻辑代码的分离,咱们能够独立管理映射关系了。之后就算须要加入新的对接字段,只要在LOCAL_TO_REMOTE_FIELDS_MAP中添加新的键值对就行了,甚至能够在LOCAL_TO_REMOTE_ASSOCIATION_MAP添加相似klass的简单关联关系的数据接入,而这些都无需修改逻辑代码。

数据对接第三版:教职工信息也须要导入了,代码拷贝之旅开始了

毫无疑问,若是只是知足于学生信息的对接,相信上面的代码也都够用了,代码的重构也能够告一段落了。

可是,前面说了,除了学生的信息,还有教职工的信息须要作接入,并且从最开始的假设以及数据结构预览一节看到,老师的数据结构跟学生的数据结构极其类似,因此,时间紧迫,我就直接拷贝代码而后简单删改了一下:

# app/models/teacher.rb
class Teacher < ActiveRecord::Base
  LOCAL_TO_REMOTE_FIELDS_MAP = {
    number: :xh,
    title: :zc,
    id_number: :zjhm
  }

  def import_data_from_remote
    remote_teachers = remote_database[:jzgjbxx].page(page)

    remote_teachers.each do |remote_teacher|
      teacher = Teacher.find_or_initialize_by xxx: xxx
      LOCAL_TO_REMOTE_FIELDS_MAP.keys.each do |attribute|
        teacher.send("#{attribute}=", remote_teacher[LOCAL_TO_REMOTE_FIELDS_MAP[attribute]])
      end

      teacher.save
    end
  end
end

注意在上面的代码中,Teacher中比起Student,少了LOCAL_TO_REMOTE_ASSOCIATION_MAP常量,而且也删除了相关的代码,虽然代码已经知足需求了,教职工的数据导入也是无比顺利,但是面对着一堆重复的代码,真心别扭!

数据对接第四版:抽象逻辑,代码共享

其实我多少也是有代码洁癖的,大片Copy的代码岂不是搞得本身逼格好Low?怎么能够忍受,继续重构!

这一次重构其实就简单多了,把重复的核心逻辑代码抽取出来,而后放到一个专门负责数据对接的Concern里边,最后在须要此concern的model里include一下就好了。话很少说,上Concern代码:

# app/models/concerns/import_data_concern.rb
module ImportDataConcern
  extend ActiveSupport::Concern

  module ClassMethods
    def import_data_from_remote
      remote_objects = remote_database[self::REMOTE_TABLE_NAME].page(page)

      remote_objects.each do |remote_object|
        object = self.find_or_initialize_by xxx: xxx
        self::LOCAL_TO_REMOTE_FIELDS_MAP.keys.each do |attribute|
          # 逐一调用属性赋值方法,完成Student属性的赋值
          object.send("#{attribute}=", remote_object[self::LOCAL_TO_REMOTE_FIELDS_MAP[attribute]])
        end

        if self::LOCAL_TO_REMOTE_ASSOCIATION_MAP
          self::LOCAL_TO_REMOTE_ASSOCIATION_MAP.each do |association_name, association_fields_map|
            # 把远程数据赋给对应的本地数据字段
            association_field_name = association_fields_map[:association_field_name]
            remote_value = remote_object[association_fields_map[:remote_field_name]]

            # 查找或建立关联对象
            related_object =
              reflect_on_association(association_name).klass.find_or_create_by association_field_name => remote_value
            # 创建关联关系
            local_object.send("#{association_name}=", related_object)
          end
        end

        object.save
      end
    end
  end
end

在上面的代码中,咱们把核心对接逻辑抽了出来,而且抽象了远程数据表名的配置,另外经过if self::LOCAL_TO_REMOTE_ASSOCIATION_MAP兼容关联关系的导入。
为了在Teacher以及Student中正常运行上面的代码,咱们还须要在这两个model分别include当前的concern,而且声明必要的常量:

# app/models/student.rb
class Student < ActiveRecord::Base
  include ImportDataConcern

  REMOTE_TABLE_NAME = 'XSJBXX'
  LOCAL_TO_REMOTE_FIELDS_MAP = {
    number: :xh,
    name: :xm,
    age: :nj
  }

  LOCAL_TO_REMOTE_ASSOCIATION_MAP = {
    klass: {
      association_field_name: :name,
      remote_field_name: :bjmc
    }
  }
end
# app/models/teacher.rb
class Teacher < ActiveRecord::Base
  include ImportDataConcern

  LOCAL_TO_REMOTE_FIELDS_MAP = {
    number: :xh,
    title: :zc,
    id_number: :zjhm
  }
end

通过上面的重构,本来重复的代码已经变成了一个Concern,经过Concern来管理独立的业务逻辑,也使得代码管理起来更方便了。可是,等等,咱们的重构之旅还在继续!

数据对接第五版:砍掉恶心的常量,使用YAML配置映射关系

当时在写代码的过程当中,我就一直感受一大堆的常量使人没法直视,可是,若是不用常量,我还能怎么作?尽管前面两个表的数据导入任务完成了,我仍是纠结于代码中那恶心死了的常量(实际上,我当时写的常量比大家如今看到的更多,文章中的只不过是示例)。而庆幸的是,那天脑洞一开:“这些映射关系本质上不就是一堆配置信息吗?而我在代码中的常量也就是用Hash存储的,那用YAML文件不就恰好了吗?”。是啊,像config/database.yml这类的文件,一直以来都是用于保存配置信息的啊,一个是符合Rails的使用习惯,另外一个也确实符合数据结构的要求。Awesome,这就开始动工。

首先第一件事,我就把那些常量搬到了yaml文件中,而且放在了项目的config/目录下:

default:
  remote_unique_field_name: number

models:
  student:
    remote_table_name: xsjbxx
    local_to_remote_fields_map:
      number: xh
      name: xm
      grade: nj
    local_to_remote_association_map:
      klass:
        association_field_name: name
        remote_field_name: bjmc

  teacher:
    remote_table_name: jzgjbxx
    local_to_remote_fields_map:
      name: xm
      title: zc
      id_number: zjhm

配置好了yaml,那么又要如何方便地读取配置信息呢?个人方法是在config/iniitializers/目录下新建了一个initializer,主要用于在项目启动时加载配置信息,关键代码段:

module RemoteDatabase
  def self.fields_map
    return @fields_map if @fields_map

    @fields_map =
      YAML::load_file(Rails.root.join('config', 'local_to_remote_oracle_database_map.yml'))
  end
end

因此,之后只要使用RemoteDatabase.fields_map就能读取到全部数据字段映射关系了!

万事俱备以后,我最后须要作的事情就是把Concern中的常量替换为从YAML中读取到的配置就行了,重构后的代码为:

module ImportDataConcern
  extend ActiveSupport::Concern

  module ClassMethods
    def importing_fields_map
      return @fields_map if @fields_map

      @fields_map =
        RemoteDatabase.fields_map[:default].merge(
          RemoteDatabase.fields_map[:models][self.name.underscore]
        )
    end

    def import_data_from_remote
      remote_objects = remote_database[importing_fields_map[:remote_table_name]].page(page)

      remote_objects.each do |remote_object|
        # 经过值惟一的属性查找对象
        remote_unique_field_name = importing_fields_map[:remote_unique_field_name]
        remote_unique_field = remote_object[importing_fields_map[:local_to_remote_fields_map][remote_unique_field_name]]
        local_object = find_or_initialize_by(remote_unique_field_name => remote_unique_field)

        local_to_remote_fields_map = importing_fields_map[:local_to_remote_fields_map]
        # 逐一设置本地对象须要对接的各个属性
        local_to_remote_fields_map.keys.each do |attribute|
          local_object.send("#{attribute}=", remote_object[importing_fields_map[:local_to_remote_fields_map][attribute]])
        end

        # ... 关联关系的保存

        next unless local_object.changes.any?

        local_object.save
      end
    end
  end
end

上面代码中,importing_fields_map读取与当前Model匹配的字段映射关系,其内部先经过RemoteDatabase.fields_map[:default]加载了默认的配置,而后经过mergeRemoteDatabase.fields_map[:models][self.name.underscore]获得当前model专属的配置,其中的self.name.underscore的值相似于'student'或者'teacher'

在后续的代码中,基本跟前面列举的代码一致,只是将各类常量对应替换为经过local_to_remote_fields_map存储的配置,而且删除Student以及Teacher的多余常量,在此就不列举示例代码了。

在整个重构的过程当中,代码是愈来愈抽象的,可是代码自己却也所以变得愈来愈灵活,而至此,咱们已经彻底将字段映射关系从Ruby代码中剥离,假使之后还须要导入其余数据,咱们只须要修改YAML文件,而再也不须要碰任何Ruby代码,除非咱们须要修改配置项的结构。

收获重构后的果实:专业数据的导入

在经历过了几回重构后,今天开始导入学生专业的数据,而我所须要作的所有事情,仅仅只是在yaml文件中加入专业相关的配置,而且在专业的modelMajorinclude一下数据导入的Concern就好了。整个过程几分钟就完成了,简直丝般顺滑啊!

总结

最后简单总结一下重构完的代码的特色吧:

  • 避免了在model或者concern中生命一堆常量或者方法,处处定义的常量会让映射关系的管理很是分散
  • 避免不一样命名空间下的同名常量,好比Student::LOCAL_TO_REMOTE_FIELDS_MAP以及Teacher::LOCAL_TO_REMOTE_FIELDS_MAP
  • 更集中的字段映射关系配置,避免错漏
  • 逻辑跟映射关系解耦,更简洁稳健的代码
  • 自适应新的数据表导入,不须要再修改或者添加Ruby代码,配置即插即用

问题

  • 若是涉及复杂关联,如何更好地扩展?
    如今的数据对接是有限制的,就是数据自己比较规则,几乎是一张表到一张表的对接,可是若是涉及一张表到多张表之间的对接,是否能够继续再将以上代码扩展?

  1. 说是基本数据,是由于这篇文章介绍的方案目前仅针对数据关联不是特别复杂的场景,并且介绍的场景,数据的导入也比较简单,基本是从远程数据库中取值,而后再直接赋值到项目数据库的记录中。对于须要在数据导入过程当中作复杂的数据分析的案例,我暂时也没有尝试过,不过我预计能够尝试使用Ruby中的代码块的方式解决,可是在此不赘述。 

相关文章
相关标签/搜索