.NETで作る!

.NETに関するあれこれ(C#、VB.NET)

サブシステム間のデータ連携フレームワークKeyMapSync

以前にサブシステムについてまとめていた訳ですが、

mk3008net.hatenablog.com

最近、サブシステムを大量に扱うことがありまして、そろそろ簡単に管理できるようなものを作らないとマズイなと思い、ライブラリ化しました。

github.com

さっそく使い方を説明したいところですが、その前段として、KeyMapSyncがどういうものかを説明します。

実現できること

  1. 別サブシステムにあるテーブル、またはクエリ(通称データソース)を自サブシステムに取り込むことができます。(ex. Aサブシステムにある売上伝票と、Bサブシステムにある売上伝票を分析システムに取り込む 等)これが大前提の機能です。

  2. フレームワークを意識したテーブル設計はほぼ不要です。*1

  3. データソースへの逆引きを保証します。データの劣化はありません。

  4. 取り込みは追加だけに対応します。更新はサポートしません。

フレームワークを使用せずに実現する方法を考える

たとえば、Aサブシステムにある売上伝票とBサブシステムにある売上伝票を分析するシステムCというものを作ることを考えます。 登場するテーブルは以下とします。

サブシステムA売上伝票|A_ID、・・・

サブシステムB売上伝票|B_ID、・・・

分析システムC売上伝票|C_ID、・・・

もっともシンプルな実装は、「Aを共通のフォーマットC(分析システムの売上伝票)の形に変えてデータを流す、Bも同様」という方法です。この場合、確かにCのデータは出来上がります。しかし、Cのデータはどうやってできたのかはわかりません。また、データソースであるAやBと結合することが不可能なので、Cで管理できる項目を増やしたいとなったとき、相当な苦労をすることになります。

リレーション情報は維持すべきであるため、以下のように構造を変えましょう。

サブシステムA売上伝票|A_ID、・・・
│0..1
│  サブシステムB売上伝票|B_ID、・・・
│  │0..1
│  │
│1  │1
分析システムC売上伝票|C_ID、・・・、A_ID、B_ID

CにAとBの主キーをNULL許可保持するようにします。リレーション情報は維持されますが、Cのテーブル構造はデータソースに依存するようになっています。データソースが増えるたびに列を足す必要があり、そのほとんがNULLになります。これも保守性がよくありません。

リレーション情報をCから切り出すことを考えましょう。そうすることでCはデータソース依存から開放されます。

サブシステムA売上伝票|A_ID、・・・
│1
│
│0..1
サブシステムAキー情報変換表|A_ID、C_ID
│0..1
│
│  サブシステムB売上伝票|B_ID、・・・
│  │1
│  │
│  │0..1
│  サブシステムBキー情報変換表|B_ID、C_ID
│  │0..1
│  │
│1  │1
分析システムC売上伝票|C_ID、・・・

Cはデータソース(A、B)から開放されました。「~キー情報変換表」なるテーブルが必要にはなりますが、Cは無傷です。データソースが増えたとしても変換表が増えるだけです。結構良くなってきましたが、「Cのある行のデータソースはどれか?」という逆引きは面倒くさいです。「サブシステムAキー情報変換表」と「サブシステムBキー情報変換表」の2つに外部結合をし、NULLでないものがデータソースだとわかります。

データソースが2つぐらいならそれでもいいでしょう。5,10となってくるとかなりしんどいです。そこで「どこから来たのか」という視点を一元管理できる構造を考えます。

サブシステムA売上伝票|A_ID、・・・
│1
│
│0..1
サブシステムAキー情報変換表|A_ID、C_ID
│0..1
│
│  サブシステムB売上伝票|B_ID、・・・
│  │1
│  │
│  │0..1
│  サブシステムBキー情報変換表|B_ID、C_ID
│  │0..1
│  │
│1  │1
分析システムC売上伝票|C_ID、・・・
│1 
│
│0..1
データソース情報|C_ID、情報変換表名

「データソース情報」というもの新しく定義しました。同テーブルの中身は

C_ID=1のデータは、サブシステムAから作られた

C_ID=2のデータは、サブシステムBから作られた

のような情報を格納するものとします。

というわけで、ERのだけでは表現できないレベルの話になり、アプリがちゃんと「データソース情報」を作ってくれること前提になってしまいました。「実装される方、ちゃんと作ってね」という話です。

フレームワークを使用するとどうなるのか

実は上記の同じことをフレームワークがやってくるだけです。「データソース情報」の書き忘れもありません。

この他に

  • 「キー情報変換表」が存在しなければ自動でテーブル作成します。

  • 「データソース情報」が存在しなければ自動でテーブル生成します。

など、DDLのアシスト機能もあるため、プログラマの労力を本題である

データソースをどう加工したらCの形式になるのか?

に注力することができます。

サンプルプログラムを見てみましょう。

using KeyMapSync;
using System;
using System.Collections.Generic;

namespace PostgresSample
{
    internal class CustomerDatasource : SingleTableDatasource
    {
        public override string MappingName => "customer";

        public override string DatasourceTableName => "customer";

        public override string DatasourceQuery => @"
with datasource as (
    select
        customer_name as client_name
        , customer_id
        , 'test' as remarks
    from
        customer
    where
        customer_name like '%' || :name || '%'
    order by
        customer_id
)";

        public override Func<object> ParameterGenerator => () => new { name = "1" };
    }
}

これだけです。コードの大半は定数で、残りは加工方法(DatasourceQuery )を定義するだけです。

「キー情報変換表」がどうだとか、「データソース情報」がどうだとかとうことは気にしなくても大丈夫です。

もう少し細かく見ていきましょう。

SingleTableDatasourceクラス

データソースが1つのテーブルであることを示しています。(グループ化などをしていないという意味)。DatasourceTableName プロパティを実装する必要があるので、データソースとなるテーブル名を指定しましょう。

MappingNameプロパティ

連携するときの名前になります。通常はデータソースのテーブル名と同じにすればよいでしょう。1テーブルを複数の加工方法で連携したいことがあります。たとえば「論理削除で管理しているテーブルを赤黒」で連携することをイメージしてください。 全レコードは黒、論理削除されているレコードは赤で連携することになります。論理削除されているレコードは黒と赤、2回連携する必要がありますが、キーが同じであるため2回連携できません。

このような場合は

  • 黒伝を連携するためのクラスを定義し、データソーステーブル名を「sales」、マッピング名を「sales_black」にする
  • 赤伝を連携するためのクラスを定義し、データソーステーブル名を「sales」、マッピング名を「sales_red」にする

とすることで別データソース扱いにすることができます。

なお、マッピング名は先に説明したER図でいうところの「情報変換表」に相当するものです。

DatasourceQuery プロパティ

加工方法をSQLで記述してください。ただし、以下のルールに沿ってください。

  • with句だけを記述してください。

  • with句のエイリアス名は「datasource」にしてください。*2

  • with句には「datasource」以外のエイリアスがあっても構いません。

  • パラメータは使用可能です。

サンプル

with datasource as (
    select
        customer_name as client_name
        , customer_id
        , 'test' as remarks
    from
        customer
    where
        customer_name like '%' || :name || '%'
    order by
        customer_id
)

Func ParameterGeneratorプロパティ

SQLパラメータを使用しない場合はNULLを返却する無名関数を定義してください。 SQLパラメータを使用する場合はDapperと同じ書式でパラメータ値を返却してください。

サンプル

public override Func<object> ParameterGenerator => () => new { name = "1" };

連携の仕方

上記のようなデータソースクラスが用意できたらあとは

  • MappingDefinitionBuilderクラスを使ってマッピング定義に変換する
  • Synchronizer クラスを使ってデータを連携する

だけになります。

サンプル

using (var cn = new NpgsqlConnection(cnstring))
{
    cn.Open();

    var exe = new DbExecutor(new PostgresDB(), cn);
    var builder = new MappingDefinitionBuilder() { DbExecutor = exe, Destination = "client" };
    var dsList = new IDatasource[] { new CustomerDatasource(), new CorporationDatasource() };
    foreach (var ds in dsList)
    {
        var def = builder.Build(ds);
        var sync = new Synchronizer() { DbExecutor = exe };
        sync.Insert(def);
    }
}

複数のDBMSに対応することを意識したため、お約束ごと(回りくどい実装)があります。

DbExecutorクラス

SQL発行処理を管理。一部のDDL処理はDBMSによって実装がことなるので、IDBMSを実装したクラスが必要。現時点ではPostgresのみ対応しています。

MappingDefinitionBuilderクラス

どこのテーブルに連携するのか(Destination)を指定。データソースをマッピング定義クラスに変換する。

Synchronizerクラス

マッピング定義をもとにINSERT処理を実装します。

*1:さすがに主キーぐらいは定義してください。

*2:変更は可能ですが煩雑になるだけなのでおすすめしません

. .