こんにちは。プロダクトエンジニアの@ymtdzzzです。
この記事では、私が所属する共通データ基盤ユニットで対応した従業員情報への「基本給(月給)」項目追加プロジェクトにおける技術的チャレンジについてご紹介します。
「基本給(月給)」項目追加プロジェクトの概要
SmartHRには「基本機能」と呼ばれる、祖業である労務管理機能を提供している巨大なアプリケーションがあります。そこには「基本機能」それ自身と、各アプリケーションから参照される従業員情報が存在しています。
従業員情報の実態は巨大なテーブル群であり、ベーステーブルである従業員情報テーブルから、「基本情報」や「入退社情報」といった項目グループ毎に関連テーブルが分かれています。
このプロジェクトでは、ここに新たな関連テーブル「給与情報」を追加し、その項目グループに「基本給(月給)」項目を追加することになりました。これにより、タレントマネジメントの各機能で従業員の給与に関する情報を扱えるようになり、より効率的な人事業務の実現が期待されます。
機能的には従業員関連の画面で当該項目が扱えるようにします。下図は項目追加後の画面イメージです。

関連テーブル追加を伴う項目追加の難しさ
通常、関連テーブル追加時はデフォルト値や関連テーブルのNULLを考慮できれば大きな問題が発生するケースは少ないように思われます。しかし、SmartHRには「履歴管理機能」と呼ばれる、リソースの更新履歴を管理する機能があります。この履歴管理機能には制約があり、新しい関連テーブル追加を伴う機能のリリース時に考慮事項が増えることがわかりました。
その制約が「最古履歴は関連テーブルも含めて一致させる必要がある」というものです。
「最古履歴」とは、一番過去の適用日を持つ履歴です。下図の赤枠で囲んだ履歴が該当します。

従業員情報の更新で履歴が作成された場合、通常は変更があった関連テーブルのレコードのみが追加されます。
次の図は、関連テーブル「基本情報」に変更があった場合の例です。この変更により、「基本情報」テーブルにのみ2024年6月12日を適用日とするレコードが新たに追加されています。

しかし、「最古履歴」より適用日がさらに古い履歴(新たな最古履歴)が作成される場合など、特定のケースでは全ての関連テーブルの最古履歴も変更される必要があります。
次の図は、新たに最古履歴が作成される場合の例です。変更の有無に関わらず、すべての関連テーブルに対して最古履歴が追加されます。

このような動作をすることで、いかなる操作をしても関連テーブルの最古履歴が常に一致することが保証されます。
私たちはこの制約を前提として、新たに関連テーブルを追加した場合に従業員情報としてどのような考慮が必要かをまず考えてみました。

この時点で、既存の従業員情報に対して何らかのデータ補正が必要になることは予想できました。ただし、データ補正の裏では常にユーザーによる最古履歴の変更が発生する可能性があるため、リリースにあたっては慎重な戦略の検討が求められました。
各機能への影響と理想状態を整理
まずは従業員項目が関わる操作を洗い出し、理想状態を整理しました。この作業には、社内の有識者である osyo さんにも協力していただきました。
次の図は、実際に洗い出した資料の一部になります。各機能ごとに、その理想的な状態を「備考」列に簡潔に記載しています。

所属チームには自分を含め比較的社歴が浅いメンバーが多かったことから、早めに有識者に確認することで考慮漏れのリスクを減らすことができたと考えています。
リリース戦略の検討
洗い出した情報を元にして当初は以下のような流れでリリースすることを考えていました。
- SmartHRのメンテナンスモードを有効化する
- 既存従業員のうち給与情報レコードが存在しない場合、最古履歴が合致する履歴情報をデータ補正により作成する
- 基本給(月給)項目を各機能から参照・編集できる機能をリリースする
- SmartHRのメンテナンスモードを解除する
しかし、この流れだとデータ補正と機能リリースを同日にセットで対応する必要があります。そのため、以下の懸念が考えられました。
- データ補正対象が多く、メンテナンス時間が長期化するリスク
- 2と3のどこかで問題が発生した場合の切り戻し手順の複雑化
- 上記より、メンテナンス対応を行う各メンバー(自分を含む)への心理的負荷
これらの懸念を最小化するため、以下のようなメンテナンス不要の方針に変更しました。
- 給与情報レコードの有無に関わらず履歴の整合性を担保する機能をリリースする
- 既存従業員のうち給与情報レコードが存在しない場合、最古履歴が合致する履歴情報をデータ補正により作成する
- 基本給(月給)項目を各機能から参照・編集できる機能をリリースする
メンテナンスが不要になり、2のデータ補正の部分実行や、2の作業と3のリリースタイミングを独立して実行可能にする戦略です。自分たちのタイミングで作業および小さくリリースできるため、先述した心理的負荷についても軽減できることが見込まれました。
ここからは、上記で挙げた 1〜3 の対応について、それぞれどのように進めたかを順にご紹介します。
給与情報レコードの有無に関わらず履歴の整合性を担保する機能をリリースする
まずはユーザーからどのような操作をされても給与情報を含めた関連テーブルの履歴の整合性を担保できるようにします。
具体的には下記のような対応をしました(一部抜粋)。
| 操作 | 給与情報 | 対応内容 |
|---|---|---|
| 従業員情報の登録(Web, API等) | - | 給与情報のレコード(履歴)も作成されるようにする |
| 従業員情報の履歴編集(最古履歴の追加) | 存在する | 給与情報にも最古履歴が追加されるようにする |
| 存在しない | 給与情報に対する変更はしない | |
| 従業員情報の削除 | 存在する | 給与情報もシステム削除されるようにする ※単なる削除ではなく、システム期間を更新しシステム上不可視とする |
| 存在しない | 給与情報に対する変更はしない |
関連テーブルが多いことから動的に関連テーブルを引いてくる処理があり、場合によってはNull参照など例外が発生するリスクがあったため、給与情報が存在しない場合に意図した挙動になることも確認しました。また、この対応はフィーチャーフラグを用いて安全にリリースしました。
ここでのポイントは、給与情報の有無に関わらず意図した挙動を実現する点です。操作対象の従業員情報がデータ補正済みかどうかを気にする必要がなくなることで、メンテナンスは不要になり、私たちのペースで実行できるようになりました。
既存従業員のうち給与情報レコードが存在しない場合、最古履歴が合致する履歴情報をデータ補正により作成する
データ補正前後での履歴の整合性が担保されたので、データ補正を実施しました。実行イメージは以下です。

やっていることはシンプルで、最古の適用日を適用開始日とする給与情報レコードを追加してあげるだけのスクリプトを実行しました。対象データ量が多いため、一定件数のチャンク毎にbulk import(一括インポート)するなど速度面の最適化もしています。
ここで、履歴管理の性質上「最古履歴の変更」の実態は新規レコードの作成であることから、通常のDBレイヤーでのロックではデータ補正中の最古履歴の変更を防ぐ手段が無いことに気付きました。
アドバイザリーロックのようなアプリケーション側のロック処理も検討しましたが、最古履歴の一時的なずれによるユーザー影響が軽微であることから、データ補正後に「履歴のずれを検知・修正するスクリプト」を作成し、必要に応じて実行する対応をとりました。このスクリプトは、全体の整合性チェックも兼ねています。
基本給(月給)項目を各機能から参照・編集できる機能をリリースする
上記の対応で履歴の整合性が担保されたので、ユーザーが「基本給(月給)」項目の参照や更新ができるような対応をします。ここは通常の機能追加となりますが、1とは別のフィーチャーフラグを用いることで安全にリリースできるようにしました。
リリースを終えて
各フェーズにおいて意図した状態になっているかを確認しつつ段階的にリリースを進めていったおかげで、安全にリリースをやり切ることができました。リリースから2ヶ月ほど経っていますが、特に問題無くご利用いただけています。
リリース後に想定外の不整合が発生する可能性は最後までゼロではありませんでしたが、ノーメンテの段階リリースにより最小限の心理的負荷に抑えることができたと考えています。
こうした複雑度や不確実性の高い開発では、影響範囲や各フェーズにおける理想状態を整理し、各フェーズが混在しても問題ないような実装にしておくことで、安全かつ心理的負荷の低い状態でリリースを進めることができるという点が学びとなりました。
このような履歴管理方法を採用しているプロダクトはあまり多くないと思いますが、今回のリリース戦略で得られた学びが読者の皆様のお役に立てば幸いです。
We Are Hiring!
SmartHR では一緒に SmartHR を作りあげていく仲間を募集中です!
興味のある方はぜひカジュアル面談でざっくばらんにお話ししましょう!