ホームページ  >  記事  >  Java  >  Springboot マルチテナント SaaS を構築する方法

Springboot マルチテナント SaaS を構築する方法

WBOY
WBOY転載
2023-05-12 16:49:061466ブラウズ

技術フレームワーク

スプリングブートのバージョンは 2.3.4.RELEASE

永続層は JPA を採用しています

テナント モデル設計

すべてのテナントが JPA を採用しています。 saas アプリケーション 同じサービスとデータベースを使用して、テナント データを分離するために、ここで BaseSaasEntity

public abstract class BaseSaasEntity {
    @JsonIgnore
    @Column(nullable = false, updatable = false)
    protected Long tenantId;
    }

が作成されます。テナント ID に対応するフィールド tenantId は 1 つだけです。すべてのテナント ビジネス エンティティはこれを継承します。親クラス。最後に、tenantId は、データがどのテナントに属しているかを区別するために使用されます。

SQL テナント データのフィルタリング

通常どおり、テーブルの作成後、対応するモジュールの CURD に従う必要があります。しかし、Saas アプリケーションの最も基本的な要件は、テナント データの分離です。つまり、B 社の人々は A 社のデータを見ることができません。どのようにフィルタリングしますか? 上で確立した BaseSaasEntity がここで機能します。現在のリクエストがどの企業から来ているかを区別することで、 from、すべてのテナント ビジネス SQL に where tenant=? を追加して、テナント データ フィルタリングを実装します。

Hibernate フィルター

テナント SQL フィルター コードをビジネスに追加すると、ワークロードが大きくなるだけでなく、エラーの可能性も高くなります。理想的には、フィルタリングされた SQL スプライシングをまとめて処理し、テナント ビジネス インターフェイスで SQL フィルタリングを有効にすることです。 JPA は Hibernate によって実装されているため、ここで Hibernate のいくつかの機能を使用できます

@MappedSuperclass
@Data
@FilterDef(name = "tenantFilter", parameters = {@ParamDef(name = "tenantId", type = "long")})
@Filter(condition = "tenant_id=:tenantId", name = "tenantFilter")
public abstract class BaseSaasEntity {
    @JsonIgnore
    @Column(nullable = false, updatable = false)
    protected Long tenantId;


    @PrePersist
    public void onPrePersist() {
        if (getTenantId() != null) {
            return;
        }
        Long tenantId = TenantContext.getTenantId();
        Check.notNull(tenantId, "租户不存在");
        setTenantId(tenantId);
    }
}

Hibernate3 は、「可視性」ルール、つまり Hibernate フィルターを使用してデータを処理する革新的な方法を提供します。 Hibernate フィルタは、パラメータを受け取ることができるグローバルに有効な名前付きフィルタです。特定の Hibernate セッションに対してフィルタを有効にする (または無効にする) かどうかを選択できます。

ここでは、@FilterDef と @Filter を使用して SQL フィルター条件を事前定義します。次に、 @TenantFilter アノテーションを使用して、インターフェイスにデータ フィルタリングが必要であることを識別します。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Transactional
public @interface TenantFilter {

    boolean readOnly() default true;

}

このインターフェイスは、コントローラー層に対応するメソッドに配置されていることがわかります。トランザクション アノテーション @Transactional を追加することの重要性は、休止状態フィルターをアクティブ化するにはトランザクションを有効にする必要があることです。ここでのデフォルトは読み取り専用トランザクションです。最後に、フィルターをアクティブにするアスペクトを定義します

@Aspect
@Slf4j
@RequiredArgsConstructor
public class TenantSQLAspect {
    private static final String FILTER_NAME = "tenantFilter";
    private final EntityManager entityManager;
    @SneakyThrows
    @Around("@annotation(com.lvjusoft.njcommon.annotation.TenantFilter)")
    public Object aspect(ProceedingJoinPoint joinPoint) {
        Session session = entityManager.unwrap(Session.class);
        try {
            Long tenantId = TenantContext.getTenantId();
            Check.notNull(tenantId, "租户不存在");
            session.enableFilter(FILTER_NAME).setParameter("tenantId", tenantId);
            return joinPoint.proceed();
        } finally {
            session.disableFilter(FILTER_NAME);
        }
    }
}

ここでのアスペクトのオブジェクトは、カスタマイズしたばかりの @TenantFilter アノテーションです。メソッドが実行される前に現在のテナント ID を取得し、フィルターを有効にします。このようにして、テナントデータの分離が完了しました。テナントのビジネス インターフェイスに @TenantFilter アノテーションを追加するだけで、開発はビジネス コードを考慮するだけで済みます。上の図の TenantContext は現在のスレッド テナント コンテキストです。フロントエンドと同意することで、テナント ID がインターフェイスのリクエスト ヘッダーに追加され、サーバーはインターセプターを使用して取得したテナント ID を ThreadLocal

public class IdentityInterceptor extends HandlerInterceptorAdapter {
    public IdentityInterceptor() {
        log.info("IdentityInterceptor init");
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader(AuthConstant.USER_TOKEN_HEADER_NAME);
        UserContext.setToken(token);
        String tenantId = request.getHeader(AuthConstant.TENANT_TOKEN_HEADER_NAME);
        TenantContext.setTenantUUId(tenantId);
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        UserContext.clear();
        TenantContext.clear();
    }
}
にキャッシュします。

サブライブラリ

テナント数が増えると、単一のMySQLデータベースと単一のテーブルのデータが必ずボトルネックになりますので、ここではサブデータベースという方法のみを使用します。複数のデータ ソースを利用して、テナントとデータ ソースの多対 1 マッピングを実行します。

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    private Map<Object, Object> targetDataSources;
    public DynamicRoutingDataSource() {
        targetDataSources =new HashMap<>();
        DruidDataSource druidDataSource1 = new DruidDataSource();
        druidDataSource1.setUsername("username");
        druidDataSource1.setPassword("password");
        druidDataSource1.setUrl("jdbc:mysql://localhost:3306/db?useSSL=false&useUnicode=true&characterEncoding=utf-8");
        targetDataSources.put("db1",druidDataSource1);
        
        DruidDataSource druidDataSource2 = new DruidDataSource();
        druidDataSource2.setUsername("username");
        druidDataSource2.setPassword("password");
        druidDataSource2.setUrl("jdbc:mysql://localhost:3306/db?useSSL=false&useUnicode=true&characterEncoding=utf-8");
        targetDataSources.put("db2",druidDataSource1);
        
        this.targetDataSources = targetDataSources;
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
    public void addDataSource(String key, DataSource dataSource) {
        if (targetDataSources.containsKey(key)) {
            throw new IllegalArgumentException("dataSource key exist");
        }
        targetDataSources.put(key, dataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContext.getSource();
    }
}

AbstractRoutingDataSource を実装して動的ルーティング データ ソースを宣言します。フレームワークが datesource を使用する前に、Spring はどのデータ ソースを使用するかを決定するために、terminateCurrentLookupKey() メソッドを呼び出します。ここでの DataSourceContext は上記の TenantContext と同様で、インターセプターで tenantInfo を取得した後、現在のテナントに対応するデータ ソース キーを見つけて ThreadLocal に設定します。

以上がSpringboot マルチテナント SaaS を構築する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。