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