MyBatisインターセプターによるパラメータオブジェクトのプロパティ設定

MyBatisのmapper.xmlファイルでは、#{}や${}を使用してパラメータを取得します。これらのパラメータは事前にmapper.javaインターフェースファイルで指定する必要があります。

パラメータを拡張するためには、MyBatisがどのようにmapper.javaからパラメータを受け取っているか理解することが重要です。

Executor.javaインターフェースのqueryメソッドを覗くと、最初の引数であるMappedStatementオブジェクトにparameterMapというフィールドがあります。このフィールドはMap型で、パラメータを保持しています。したがって、インターセプター内でMappedStatementオブジェクトのparameterMapに必要なパラメータを追加することで目的を達成できます。

@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class OrganizationScopeInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] arguments = invocation.getArgs();

        if (ArrayUtils.isNotEmpty(arguments)) {
            MappedStatement mappedStatement = (MappedStatement) arguments[0];
            OrganizationScope scope = getOrganizationScope(mappedStatement);

            StringBuilder condition = new StringBuilder();
            if (scope != null) {
                Long organizationId = SecurityUtils.getOrganizationId();

                if (organizationId != null) {
                    condition.append(StringUtils.format(" {}.{} = {} ", scope.alias(), scope.columnName(), organizationId));
                } else {
                    condition.append(" 1=0 ");
                }
            }

            if (arguments[1] == null) {
                arguments[1] = new MapperMethod.ParamMap<>();
            }
            Map parameters = (Map) arguments[1];
            // ここが重要な部分
            parameters.put("organizationScope", condition.length() > 0 ? " (" + condition.toString() + ")" : "");
        }
        return invocation.proceed();
    }

    private OrganizationScope getOrganizationScope(MappedStatement mappedStatement) {
        String identifier = mappedStatement.getId();
        // クラスとメソッド名を取得
        String className = identifier.substring(0, identifier.lastIndexOf('.'));
        String mapperMethod = identifier.substring(identifier.lastIndexOf('.') + 1);

        Class clazz;
        try {
            clazz = Class.forName(className);
        } catch (ClassNotFoundException e) {
            return null;
        }
        Method[] methods = clazz.getMethods();

        OrganizationScope scope = null;
        for (Method method : methods) {
            if (method.getName().equals(mapperMethod)) {
                scope = method.getAnnotation(OrganizationScope.class);
                break;
            }
        }
        return scope;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

parametersにorganizationScopeを追加すると、対応するmapper.xmlで#{organizationScope}や${organizationScope}を使用してそのパラメータを取得できます。

プロジェクトでPageHelperプラグインを使用している場合、起動後に対応するmapperのクエリを実行すると、PageHelperのインターセプターが先に動作し、カスタムインターセプターがまだ実行されていないためパラメータが見つからないというエラーが発生します。

カスタムインターセプターをPageHelperのインターセプターよりも前に実行させるには、SqlSessionFactoryに追加したインターセプターの順序を利用します。PageHelperの自動構成クラスを確認すると、PageHelperのインターセプターはMyBatisの自動構成後に設定されます。

@Configuration
@ConditionalOnBean(SqlSessionFactory.class)
@EnableConfigurationProperties(PageHelperProperties.class)
@AutoConfigureAfter(MybatisAutoConfiguration.class)
@Lazy(false)
public class PageHelperAutoConfig {

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactorys;

    @Autowired
    private PageHelperProperties properties;

    @Bean
    @ConfigurationProperties(prefix = PageHelperProperties.PAGEHELPER_PREFIX)
    public Properties pageHelperProperties() {
        return new Properties();
    }

    @PostConstruct
    public void addPageInterceptor() {
        PageInterceptor interceptor = new PageInterceptor();
        Properties props = new Properties();
        props.putAll(pageHelperProperties());
        props.putAll(this.properties.getProperties());
        interceptor.setProperties(props);
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactorys) {
            Configuration config = sqlSessionFactory.getConfiguration();
            if (!containsInterceptor(config, interceptor)) {
                config.addInterceptor(interceptor);
            }
        }
    }

    private boolean containsInterceptor(Configuration config, Interceptor interceptor) {
        try {
            return config.getInterceptors().contains(interceptor);
        } catch (Exception e) {
            return false;
        }
    }
}

カスタムインターセプターをPageHelperのインターセプターよりも後に追加するため、以下の設定を行います。

@Configuration
@ConditionalOnBean(SqlSessionFactory.class)
@EnableConfigurationProperties(PageHelperProperties.class)
@Lazy(false)
@AutoConfigureAfter(PageHelperAutoConfig.class)
public class OrganizationScopeInterceptorConfig {

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactorys;

    @PostConstruct
    public void addPageInterceptor() {
        OrganizationScopeInterceptor interceptor = new OrganizationScopeInterceptor();
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactorys) {
            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
        }
    }

}

ここで注意すべき点は、OrganizationScopeInterceptorConfigがSpring Bootによって自動的にスキャンされてしまうと、設定の順序に関わらず先に読み込まれる可能性があることです。これを防ぐためには、以下のような方法があります。

  • @SpringBootApplication(exclude = OrganizationScopeInterceptorConfig.class)
  • @ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = OrganizationScopeInterceptorConfig.class))
  • resources/META-INF/spring.factoriesに以下の内容を追加する
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  top.sclf.common.datascope.config.OrganizationScopeInterceptorConfig

タグ: MyBatis インターセプター パラメータ PageHelper SQL

6月28日 20:15 投稿