摘要:最近在做的项目中,需要将远程数据库中的数据对接到项目数据库中,但是远程的数据不仅数据表名跟字段命名奇葩,数据结构本身跟项目数据结构出入比较大,在数据导入过程中代码经历了几次重构,最后使用了文件解决了基本数据对接的问题。
最近在做的Ruby on Rails项目中,需要将远程数据库中的数据对接到项目数据库中,但是远程的数据不仅数据表名跟字段命名奇葩,数据结构本身跟项目数据结构出入比较大,在数据导入过程中代码经历了几次重构,最后使用了YAML文件解决了基本数据1对接的问题。在此写一篇博文,我会尽量重现一路过来的代码变更,算是分享一下我的思考过程,也算是祭奠一下自己的苦逼岁月。
假设以及数据结构预览因为远程数据库服务器为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 |
第一个导入的数据表是学生的信息表,在最开始的时候,因为只需要考虑一张多带带的表,所以代码写得简单粗暴,基本过程就是:根据需要的信息,查询对应的远程数据字段,然后使用属性方法赋值,最后保存接入的数据。对接方法的部分相关代码示例(为了方便阅读以及保护项目敏感信息,本文对项目中原有代码进行了缩减以及修改):
# 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
上面的代码,呃,中规中矩,基本体现了各取所需的指导思想,但是总觉得怎么有点不好呢?
数据对接第二版:通过本地到远程数据库字段映射关系自动匹配赋值在第一版的代码中,最大的坏味道在于:代码中需要把所有需要对接的字段列举出来,一旦遇到字段增删修改的情况,就需要同时更新原来的逻辑代码,太不灵活了,而且列举所有字段本身就是一件非常繁琐枯燥的事情。再假设字段很多的情况下,要从代码中一个个检查字段的名称,肯定是件多么可怕的事情啊。
那么怎么修改呢?用映射表!仔细观察第一段的代码,其实代码所做的工作如此简单:无非是先从远程数据中取值,然后赋值到本地数据对象的对应属性中,这种“本地-远程”的字段映射关系,不就是我们每天面对的“键-值”对的特征吗?那直接用一个Hash来保存这种对应关系不就好了。
话不多说,我们开始重构:
# 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的数据字段映射关系。
在声明了必要的字段映射关系之后,我就在代码中遍历了每一个字段,并且通过对应的远程字段名称查找对应的数值,并且使用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代码,配置即插即用
问题如果涉及复杂关联,如何更好地扩展?
现在的数据对接是有限制的,就是数据本身比较规则,几乎是一张表到一张表的对接,但是如果涉及一张表到多张表之间的对接,是否可以继续再将以上代码扩展?
说是基本数据,是因为这篇文章介绍的方案目前仅针对数据关联不是特别复杂的场景,而且介绍的场景,数据的导入也比较简单,基本是从远程数据库中取值,然后再直接赋值到项目数据库的记录中。对于需要在数据导入过程中做复杂的数据分析的案例,我暂时也没有尝试过,不过我预计可以尝试使用Ruby中的代码块的方式解决,但是在此不赘述。 ↩
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/17446.html
摘要:所以我们选一个仓库仓库比较多,我这里选用,都行,根据需求自行选择访问端口,然后就没有然后了功能没有那么强大,不过占用资源少,速度快,我们稳定运行了几年了。 kubernetes集群三步安装 CI 概述 用一个可描述的配置定义整个工作流 程序员是很懒的动物,所以想各种办法解决重复劳动的问题,如果你的工作流中还在重复一些事,那么可能就得想想如何优化了 持续集成就是可以帮助我们解决重复的代码...
摘要:今天小数给大家带来的是一篇代码级干货文章,与大家分享一些利用以微服务形式设置应用的经验与心得。为何选择加在我效力的企业中,我们一直在利用为全部工程师构建开发环境。运行命令,从而利用构建镜像并安装。 今天小数给大家带来的是一篇代码级干货文章,与大家分享一些利用Rails API以微服务形式设置应用的经验与心得。 为何选择Docker加Rails API? 在我效力的企业中,我们一直在利用...
摘要:前言云帮目前支持对接或者主流代码托管平台的公开项目,后期会考虑接入其他类型的服务。对接通过应用市场进行安装安装应用由于依赖和,首先安装和应用。云平台代码,选择自建,然后将授权添加应用的列表里即可。 前言 云帮目前支持对接GitLab、Gogs、Github,或者主流代码托管平台的公开项目,后期会考虑接入其他类型的Git服务。 私有云 GitLab是一个用于仓库管理系统的开源项目,私有云...
摘要:这个速查表主要是分享互联网上一些比较常用的工具和技术常用内容,如编辑器的快捷键的命令行的选择器的属性等,这个列表简单收集了常用的工具,可以收藏用于平时的备忘录,需要用到的时候可以及时查阅。 这个速查表主要是分享互联网上一些比较常用的工具和技术常用内容,如编辑器的快捷键、git的命令行、jQuery的API选择器、CSS的flexbox属性等,这个列表简单收集了常用的工具,可以收藏用于平...
阅读 2600·2021-11-15 11:38
阅读 2618·2021-11-04 16:13
阅读 17979·2021-09-22 15:07
阅读 1013·2019-08-30 15:55
阅读 3260·2019-08-30 14:15
阅读 1663·2019-08-29 13:59
阅读 3206·2019-08-28 18:28
阅读 1575·2019-08-23 18:29