データ連携に使用するImportモデルの紹介
はじめに
こんにちは。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_count
、imported_count
、error_count
カラムが存在することでファイルの状態がよく分かります。
ファイルの格納と、Model作成の分離
取り込みファイルをレコードに格納する処理と、その後のModel作成の処理が分離しています。
例えば、S3からデータを取り込んでも、Webブラウザからアップロードしても、ExamResultImport
さえ作成してしまえば、ExamResult
の作成処理は分離して呼び出すことが可能です。Webブラウザからアップロードした場合は非同期処理で行うようなパターンも適用しやすいでしょう。
まとめ
データ連携時のファイル取り込みの手法について解説しました。
ファイルの実態を格納するImportモデルを利用することで、エラーの調査や非同期処理がやりやすくなります。
Railsでデータ連携する際の設計の参考にしてみてはいかがでしょうか。
Discussion