qinfengge

qinfengge

醉后不知天在水,满船清梦压星河
github
email
telegram

mybatis plus マルチテナントプラグイン

引言#

マルチテナントは主にデータの隔離に使用され、通常は SAAS システム内で、ソフトウェアをサービスとして企業に提供します。各企業は一つのテナントであり、各テナントは自テナント内のデータのみを見ることができます。したがって、各テナントに対してデータの隔離が必要です。

データ隔離#

マルチテナントのデータ隔離ソリューションは、三つのタイプに分けられます:

DATASOURCE モード:独立したデータベース
SCHEMA モード:共有データベース、独立したスキーマ
COLUMN モード:共有データベース、共有スキーマ、共有データテーブル

DATASOURCE モード#

一つのテナントに一つのデータベース。このソリューションのユーザーデータ隔離レベルは最高で、安全性も最も高いですが、コストも高いです。

image

DATASOURCE モード

利点:異なるテナントに独立したデータベースを提供し、データモデルの拡張設計を簡素化し、異なるテナントの独自のニーズを満たすのに役立ちます;故障が発生した場合、データの復元が比較的簡単です。
欠点:データベースのインストール数が増加し、それに伴いメンテナンスコストと購入コストが増加します。

SCHEMA モード#

複数またはすべてのテナントがデータベースを共有しますが、一つのテナントに一つのテーブルがあります。

SCHEMA モード

image

利点:安全性が高いテナントに対して一定の論理データ隔離を提供しますが、完全な隔離ではありません;各データベースはより多くのテナント数をサポートできます。
欠点:故障が発生した場合、データの復元が比較的困難です。なぜなら、データベースの復元は他のテナントのデータに関わるからです;テナント間でデータを集計する必要がある場合、一定の困難があります。

COLUMN モード#

共有データベース、共有データ構造。テナントは同じデータベース、同じテーブルを共有しますが、テーブル内で tenant_id フィールドを通じてテナントのデータを区別します。これは共有度が最も高く、隔離レベルが最も低いモードです。

COLUMN モード

image

利点:メンテナンスと購入コストが最も低く、各データベースがサポートできるテナント数が最も多いです。
欠点:隔離レベルが最も低く、安全性も最低で、設計開発時に安全性への配慮を増やす必要があります;データのバックアップと復元が最も困難で、テーブルごとに逐次バックアップと復元が必要です。

まとめ#

ほとんどの場合、DATASOURCECOLUMN モードが最もよく使用されます。
予算が十分な場合やテナントが安全性、隔離性を高く要求する場合はDATASOURCE モードを選択します。
通常の場合はCOLUMN モードで十分で、開発や運用が簡単で、最小限のサーバーで最大のテナントにサービスを提供します。

原理#

ここでは、最も一般的なCOLUMN 共有データベースのモードを用いてマルチテナントの実装原理を説明します。
あなたはすでに気づいていると思いますが、このモードでは通常の単一テナントアプリケーションとの唯一の違いは、データベーステーブルに新たに tenant_id フィールドが追加されたことです。
その通り、このフィールドはユニークなテナント識別子であり、プログラムはこのフィールドを使用して各テナントのデータを判断する必要があります。
あなたは考えるかもしれません、SQL を書くときにテナントフィールドを直接結合すればいいのではないかと?
もちろんそうすることもできますが、明らかに優雅ではありません。

mybatis plus マルチテナントプラグイン#

では、どのように優雅なコードでマルチテナントを実現するのでしょうか?
mybatis は国内の開発者に最も使用され、最も馴染みのある ORM フレームワークであり、ネイティブのマルチテナントプラグインを提供しています。
公式マルチテナントプラグイン

public interface TenantLineHandler {

    /**
     * テナント ID 値の式を取得します。単一の ID 値のみをサポートします。
     * <p>
     *
     * @return テナント ID 値の式
     */
    Expression getTenantId();

    /**
     * テナントフィールド名を取得します。
     * <p>
     * デフォルトのフィールド名は: tenant_id
     *
     * @return テナントフィールド名
     */
    default String getTenantIdColumn() {
        // このフィールドが固定でない場合は、SqlInjectionUtils.checkで安全性を確認してください
        return "tenant_id";
    }

    /**
     * テーブル名に基づいてマルチテナント条件の結合を無視するかどうかを判断します。
     * <p>
     * デフォルトでは、すべてのテーブルが解析され、マルチテナント条件が結合されます。
     *
     * @param tableName テーブル名
     * @return 無視するかどうか、true:無視、false:解析してマルチテナント条件を結合する必要があります
     */
    default boolean ignoreTable(String tableName) {
        return false;
    }
}

説明:
マルチテナント!= 権限フィルタリング、乱用しないでください。テナント間は完全に隔離されています!!!
マルチテナントを有効にすると、すべての実行されるメソッドの SQL が処理されます。
自作の SQL は規範に従って記述してください(SQL が複数のテーブルに関わる場合、各テーブルにはエイリアスを付ける必要があります。特に inner join の場合は標準の inner join を記述してください)。

このプラグインを使用すると、SQL の結合操作は mybatis-plus フレームワークが行ってくれます。その実装方法はページネーションプラグイン(インターセプター)に基づいています。

進階#

上記は単純な例に過ぎません。次に、ruoyi-vue-plus スキャフォールドを使用してマルチテナントの機能を拡張します。

プロジェクトアドレス

yml 設定#

yml 設定ファイルに以下の設定を追加します。

# マルチテナント設定
tenant:
    # マルチテナントモードを有効にするか
    enable: true
    # テナントフィールド名
    column: tenant_id
    # マルチテナントを判断する必要があるテーブル名
    includes:
      - sr_card
      - sr_room
      - sr_seat
      - sr_room_seat
      - sr_swiper
      - sys_oss

コンポーネントに対応する設定ファイルの値を記述します。

@Data
@Component
@ConfigurationProperties(prefix = "tenant")
public class TenantProperties {

    /**
     * マルチテナントモードを有効にするか
     */
    private Boolean enable;

    /**
     * テナントフィールド名
     */
    private String column;

    /**
     * マルチテナントを判断する必要があるテーブル名
     */
    private List<String> includes;
}

mybatis-plus 設定#

次に、mybatis-plus の設定ファイルにマルチテナントプラグインを登録し、プラグインのロジックを記述する必要があります。

    @Resource
    private TenantProperties tenantProperties;

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        if (tenantProperties.getEnable()) {
            // マルチテナントプラグイン
            interceptor.addInnerInterceptor(tenantLineInnerInterceptor());
        }

        // データ権限処理
        interceptor.addInnerInterceptor(dataPermissionInterceptor());
        // ページネーションプラグイン
        interceptor.addInnerInterceptor(paginationInnerInterceptor());
        // オプティミスティックロックプラグイン
        interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
        return interceptor;
    }

まず、設定ファイルをインポートし、マルチテナントプラグインを登録します。注意すべきは、マルチテナントプラグインはページネーションプラグインの前に配置する必要があることです。

次に、対応するロジックを記述します。

public TenantLineInnerInterceptor tenantLineInnerInterceptor() {

        return new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                try {
                    LoginUser loginUser = LoginUserShareMap.getLoginUser();
                    if (loginUser == null) {
                        System.err.println("現在ログインしていないユーザー!");
                        //マルチテナントモード、テナントIDはnull
                        return new LongValue(0L);
                    } else {
                        //マルチテナントモード、テナントIDは現在のユーザーから取得
                        return new LongValue(loginUser.getUserId());
                    }
                } catch (Exception e) {
                    throw new RuntimeException("現在のログインユーザー情報の取得に失敗しました!", e);
                }

            }

            @Override
            public String getTenantIdColumn() {
                // データベースのテナントIDの列名に対応
                System.err.println("現在のテナント列名:" + tenantProperties.getColumn());
                return tenantProperties.getColumn();
            }

            // これはdefaultメソッドで、デフォルトではfalseを返し、すべてのテーブルがマルチテナント条件を結合する必要があることを示します
            @Override
            public boolean ignoreTable(String tableName) {
                LoginUser loginUser = LoginUserShareMap.getLoginUser();
                // ログインしているかどうかを判断し、ログインしている場合はフィルタリング
                if (loginUser != null) {
                    // プラットフォームのスーパーユーザーかどうかを判断し、プラットフォームのスーパーユーザーであればすべてのデータ権限を持つ
                    if (!LoginHelper.isAdmin()) {
                        // テナントをフィルタリングする必要があるテーブル
                        List<String> includes = tenantProperties.getIncludes();
                        return !includes.contains(tableName);
                    }
                }
                return true;
            }
        });
    }

ここで注意すべき点がいくつかあります。

  1. デフォルトでは ruoyi-vue-plus はプロジェクトの初期化時にデータベーステーブルの設定情報を読み込みます。また、フレームワークは sa-token を使用してログイン検証を行っているため、プロジェクトの初期化時にこのメソッドを実行すると、sa-token を使用してログインユーザーを取得すると web コンテキストを取得できない というエラーが発生します。
  2. ログイン状態を保存するためのコンポーネントを使用できます。通常、ThreadLocal を使用しますが、注意が必要です。このフレームワークはスレッドプールを使用しているため、ThreadLocal を使用すると予期しないエラーが発生する可能性があります。たとえば、高い同時実行性や高負荷の状況では、ユーザーがログインしたスレッドは A ですが、テナントを判断するスレッドは B であり、これら二つのスレッドのThreadLocalは異なります。

特定 SQL フィルタリング#

マルチテナントプラグインは テーブル ベースのフィルタリングを提供していますが、より細かい SQL ベースのフィルタリングは提供していません。
ただし、実現するためのアノテーションが提供されています。

アノテーションmybatis-plus バージョン
@InterceptorIgnore(tenantLine = "true")Mybatis-Plus3.4+
@SqlParser(filter=true)Mybatis-Plus3.4-

注意すべきは、MP バージョンが 3.1.1 以下の場合、@SqlParser(filter=true) を使用するには以下の設定が必要です。

# SQL解析キャッシュアノテーションを有効にします。MPバージョンが3.1.1以上の場合は設定は不要です。
mybatis-plus:
  global-config:
    sql-parser-cache: true

mapper で使用するだけです。

public interface TenantMapper extends BaseMapper<Tenant> {
    /**
     * 自定Wrapper, @SqlParser(filter = true)アノテーションはSQL解析を行わず、テナントの追加条件がないことを示します。
     *
     * @return
     */
    @SqlParser(filter = true)
    @Select("SELECT count(5) FROM t_tenant ")
    public Integer myCount();
}

マルチテナントプラグイン
マルチテナントフィールド隔離
マルチテナント解決策

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。