こんにちは。SmartHRの基本機能プロダクトで開発をしている三好です!
基本機能プロダクトではLeSS(Large-Scale Scrum)を用いた複数チームでのScrum開発をしており、その中で私が所属しているチームの取り組みをご紹介します。
入社してわかったSmartHR本体の難しさ - SmartHR Tech Blog の記事でも書かれた通り、SmartHR基本機能が持つデータ構造は難解です。
私の所属するチームはその中でも 部署
に関する問題に対応しています。
「え、部署ってあの部署...?」
「難しいところなんてあるのか」
と思われている方もいると思いますのでざっと経緯からお話できれば...
簡単な経緯
牧歌的だった時代の部署データの構造について
はるか昔、SmartHRがまだ履歴情報をもっていなかった時代に部署のデータ構造は生まれました。
従業員は兼務の部署も管理できるように、データ構造としては以下のように1:nの関係でした。
この時は特に大きな問題もなく動いてました。
BiTemporal Data Modelが来るまでは...
BiTemporal Data Modelがやってきた
とある偉い人は言いました。
「従業員に履歴を持てるようにしたい」
「ある従業員が今だけでなく過去どうだったかも管理できるようにしてほしい」
「タレントマネジメント!履歴!必須!」
こうして従業員に履歴情報を持たせることになりました。
そして従業員と関連のある部署情報にも履歴を持たせようとしました。
入社してわかったSmartHR本体の難しさ - SmartHR Tech Blog の記事でも書かれた BiTemporal Data Model
です。
ただこのBiTemporal Data Model、 部署のデータ構造との組み合わせはすこぶる悪いことがわかりました。
過去の情報を変更したタイミングで紐づいていた他の履歴情報と整合性が取れなくなったりします。
例を見てみましょう。
上記は1/1に従業員が作成されて、1/15に営業部に所属し、2/1に広報部を兼任、2/15に広報部専任になったものの、3/1からまた営業部の兼任になった従業員の履歴です。
波乱万丈ですね。
ここで、昨年の履歴を入れようと思い、12/15の時点では開発部の所属だった情報を入れるとします。
現時点では1/15に営業部だった履歴があるので、12/15から1/15まで開発部だったという履歴を追加するのが正しいでしょうか?
いいえ、答えは【12/15から1/1まで開発部だった履歴を追加する】が正解です。
なぜなら、1/1から1/15のどこにも所属していない期間もまた有効な履歴だからです。
1:nの履歴においては、存在しないことも有効な履歴として扱わなくてはいけません。
この問題は修正出来ましたが、履歴登録がうまくできたとしても表示に関してはどうでしょう。
部署の履歴を表示するには、所属する部署状況の一連の履歴を表示すれば解決するでしょうか?
社員番号など従業員と1:1で紐づくデータの場合は単純に履歴の全期間で情報を検索し表示すればOKです。
しかし部署の場合は複雑怪奇です。
部署情報は1:nであるため、1件のみ表示する場合もあれば3件、もしくはそもそも表示するべき部署情報が無い場合もあります。
さらに同じ名前の履歴でも異なるタイミングなどで作られている場合(今回の例だと営業部)などもあり、それらは別々の履歴情報として扱わなければなりません。
上の画像例では営業部の履歴は一見どちらも同じ名前の履歴ですが、一度作られた後、削除されて再び同じ名前で作成されただけかもしれませんし、そうではないかもしれません。
この問題も混迷を極めましたがなんとか対応出来ました。
...が、まだ続きます。
- 履歴の削除はどうしましょう?
- 履歴を上書きしたい場合は?
- etc...
... とにかく難しいことは想像できるかと思います。
これら様々な問題に対応するため、色々調整を試みましたが 修正は困難を極め、我々はある結論に達しました。
1:n がだめなら 1:1 にすればいいじゃない ...と
正規化を捨てるかわりに得られるシンプルな解決方法...素晴らしいですね。
これぞプランC.(※)
※SmartHRのコアバリューの一つ
そんなわけで現在に至るまで続いている 部署問題対応プロジェクト
が発足し、私のチームが対応することになったのです。
今やっていること
2023年現在に至るまで部署情報は1:nのデータ構造のままデータが作成され運用されています。
これを1:1のデータ構造に変更をしようとしていますが、変更するデータの規模はかなり大きく、さらに別のアプリケーションと連携する仕組みになっているため、改修の難易度も非常に高いです。
いましばらくお待ちいただけますと幸いです...
部署問題対応プロジェクトの規模感について
部署を利用している箇所はかなり多く改修するには労力と時間を要します。
ざっとあげると以下の機能に影響があります。
- SmartHR基本機能内で部署データを利用している機能
- おおよそ数百箇所以上
- SmartHR基本機能と連携しているすべてのアプリケーション
これらの機能を現行の部署情報の動作を担保した状態で、1:1の新しい部署情報に対応していく必要があります。
また、対応プロジェクト中に既存機能の仕様変更や新たな要件の追加が発生した場合、それにも対応する必要があり、開発が複雑になっています。
従業員部署データの変換
1:1のデータ構造に変えるからといって、今あるデータを捨ててお客さまに一から部署データを作ってもらおう!
というのは現実的ではありません。
そのため 1:nの部署データを 1:1に変換する必要があります。
当然ですが、たとえ過去の情報であったとしても間違ったデータに変更することは許されません。
しかし...
- 今あるデータは前述の通り非常に厄介な1:nのBiTemporal Data Modelです
- 既存のデータを正しく判断することも重要です
- 2023年6月現在のSmartHR従業員情報は数百万レコードあります
- 単純にデータを変換するためにも膨大な時間がかかります
- データにも様々なパターンがあるため、ロジックも複雑になりその分実行時間もかさみます
- あらゆるところで部署情報を参照しているため影響範囲も甚大です
- SmartHR基本機能で使っている機能だけでなく、連携しているすべてのアプリケーションが移行後も正しく動作するように社内外に働きかけています
これらを乗り越えるためにやってきたことの一部を紹介します。
差分コンバート機能
SmartHRには膨大な数の従業員レコードがあり、その情報が毎日追加・更新されています。
動いているサービスをできるだけ止めないまま、既存の1:nの部署情報を1:1に変換する必要があります。
もし仮に数百万ある従業員レコードを一度のメンテナンス中にすべて変換しようとすると、1ヶ月以上時間がかかってしまうため、現実的ではありません。
そのためバックグラウンドジョブで非同期に処理を行えるようにし、複数回に分けて変換しようと計画しています。
この方法は処理を複数回に分けて行え、また実際のリリース前に少しずつ対応できるなどのメリットがありますが、変換後に新たに変換すべきデータが増えてしまう可能性がある、というデメリットもあります。
(例: ある従業員の部署履歴情報を変換後、ユーザーによって新しい部署履歴が作成されたなど)
その場合は新たに更新された部署の情報を差分として検知・更新する仕組みが必要です。
部署情報の更新を検知する方法としてActive Recordのモデルを改造するなどいくつか手法を考えたのですが、最終的にはPostgreSQLにtriggerを作成し、差分コンバート専用のテーブルに格納する手法を採用しました。
この手法だとDBレベルで変更を確実に検知でき、検知漏れなく変換を行えます。
作成したTrigger
CREATE FUNCTION insert_crew_departments_updates_to_convert_table() RETURNS trigger AS $div_proc$ BEGIN INSERT INTO department_converts (crew_id, tenant_id, triggerd_at, created_at, updated_at) VALUES (NEW.crew_id, NEW.tenant_id, NOW(), NOW(), NOW()); RETURN NEW; END; $div_proc$ LANGUAGE plpgsql; CREATE TRIGGER crew_departments_convert_trigger BEFORE INSERT OR UPDATE OR DELETE ON crew_departments FOR EACH ROW EXECUTE PROCEDURE insert_crew_departments_updates_to_convert_table();
おわりに・・・
2021年から始まり、足掛け2年、このプロジェクトを続けています。
きっとこのプロジェクトがリリースされた暁には、盛大に打ち上げていただけることを願って……
We are hiring!
SmartHRの基本機能は広大で複雑ですが、キャッチアップできる土壌もしっかり整っています。
もしこのSmartHR基本機能という巨大な課題の解決にご興味ある方がいましたらカジュアル面談お待ちしております!
ウェブアプリケーションエンジニア(バックエンド) https://open.talentio.com/r/1/c/smarthr/pages/45049
ウェブアプリケーションエンジニア(フロントエンド) https://open.talentio.com/r/1/c/smarthr/pages/45050