🚚

データ連携に使用するImportモデルの紹介

2022/10/25に公開

はじめに

こんにちは。Linc'wellに転職して1年が経過しました。早いながらも充実したエンジニアライフを送っています。
さて、私は主にRailsによるバックエンドの開発を行っています。よく実装する処理として各種臨床検査データのインポートがあります。
患者さんから採取した検体をクリニックから臨床検査会社に送り、検査結果をデータとして連携しWebサービスを通じて素早く結果を表示してあげるのがミッションです。
今回紹介する仕組みは私が作ったものではないのですが、とてもセンスが良い設計だと感じたのでここで共有しようと思います。

データ連携の流れ

今回紹介するデータ連携とは、データの取り込みを指します。データ取り込みと聞いて、これを読まれているバックエンドエンジニアの方は次のような処理の流れを想像するのではないでしょうか?

ファイルの受信

ローカルディスク経由・AWS S3・Webhook・画面からのアップロードなど、何らかの経路でファイルを受け取ります。

パース

固定長テキストやCSVやJSON形式のファイルから容易にデータにアクセスできる形式に変換します。

変換

RailsのModelに取り込むための下準備です。文字列型で格納されている数字をIntに変換したり、コードからオブジェクトへ変換したりします。

モデルへの格納

連携データに基づき、RailsのModelを新規作成、もしくは更新します。

現場で直面する問題

開発当初設計した仕様通りのファイルが日々送信されていれば何の心配もありません。しかし、仕様と異なるデータが送信されたり、データの連携方法を変更したいという要望はよく発生します。

  • エラーの調査
    エラーが発生した連携ファイルの中身を調査したい。
  • データの連携方法を変更したい
    通常はS3で連携されるファイルをWebから取り込みたい。

Importモデルの紹介

現場で直面する問題に柔軟に対応するため、Linc'wellではImportモデルを採用しています。(名前は私が適当に付けました)
連携元のファイル自体をModelに格納することで、様々なメリットが生まれます。

モデル

仮に臨床検査データを取り込むことを想定しています。なお、このモデルは説明用に簡略化しています。

ExamResultImport(検査結果インポート)

カラム名 注釈
id bigint
import_from integer 0:S3, 1:Web, 2:etc.
file_name string 連携ファイル名
data text 連携ファイルの実態
total_count integer データのレコード数
imported_count integer 取り込み済みレコード数
error_count integer 取り込みエラーレコード数

ExamResult(検査結果)

カラム名 注釈
id bigint
exam_result_import_id bigint
exam_name string 検査名
result integer 0: 陰性、 1:陽性、2:再検査

ER図

処理の流れを復習しましょう。

ファイルの受信

rake taskやWebからのアップロードで受け取ったファイルは ExamResultImport#data にそのままテキストで格納します。

client = Aws::S3::Client.new
file_object = client.get_object(bucket: ENV['BUCKET'], key: '2022-10-24.csv')
ExamResultImport.create(data: file_object, import_from: 0, file_name: '2022-10-24.csv')

パース

ExamResultImport#dataに格納されたファイルを読み取り可能な形式へ変換します。

class ExamResultImport < ApplicationRecord
  has_many :exam_results

  def execute
    CSV.parse(data, headers: true) do |row| # パース
    end
  end
end

変換

パースされた内容をDBに登録可能な形式へ変換します。

class ExamResultImport < ApplicationRecord
  has_many :exam_results

  RESULTS = %w[陰性 陽性 再検査]

  def execute
    CSV.parse(data, headers: true) do |row| # パース
      # 変換
      exam_result = exam_results.build(
        exam_name: row['exam_name'],
        result: RESULTS[row['result']]
      )
    end
  end
end

保存

検査結果格納モデルに保存します。

class ExamResultImport < ApplicationRecord
  has_many :exam_results

  RESULTS = %w[陰性 陽性 再検査]

  def execute
    total_count = 0
    imported_count = 0
    error_count = 0

    CSV.parse(data, headers: true) do |row| # パース
      total_count += 1
      exam_result = exam_results.build(
        # 変換
        exam_name: row['exam_name'],
        result: RESULTS[row['result']]
      )
      if exam_result.save # 保存
        imported_count =+ 1
      else
        error_count =+ 1
      end
    end
    self.update(total_count: total_count, imported_count: imported_count, error_count: error_count)
  end
end

解説

単純なデータ取り込みと比較し若干複雑なのですが、この構成にはいくつか利点があります。

エラーの調査

取り込み元ファイルをそのままレコード内に保持しているため、取り込み時のエラー調査が容易になります。
ExamResultImport から見ると、 has_many で指定されている ExamResult に変換後のデータが格納されているのが分かります。
逆に ExamResult から見ると、 belongs_to で指定されている ExamResultImport に変換前のCSVデータが格納されているのが分かります。
また、連携ファイルが正常であるかどうかの情報は、ファイルの実態を格納するImportモデルが担当する責務です。total_countimported_counterror_count カラムが存在することでファイルの状態がよく分かります。

ファイルの格納と、Model作成の分離

取り込みファイルをレコードに格納する処理と、その後のModel作成の処理が分離しています。
例えば、S3からデータを取り込んでも、Webブラウザからアップロードしても、ExamResultImport さえ作成してしまえば、ExamResult の作成処理は分離して呼び出すことが可能です。Webブラウザからアップロードした場合は非同期処理で行うようなパターンも適用しやすいでしょう。

まとめ

データ連携時のファイル取り込みの手法について解説しました。
ファイルの実態を格納するImportモデルを利用することで、エラーの調査や非同期処理がやりやすくなります。
Railsでデータ連携する際の設計の参考にしてみてはいかがでしょうか。

Linc'well, inc.

Discussion