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中的代碼塊的方式解決,可是在此不贅述。 

相關文章
相關標籤/搜索