MyBatis-Spring 1.1.0 发布以后,提供了三个 bean 以供构建 Spring Batch 应用程序:MyBatisPagingItemReader、MyBatisCursorItemReader 和 MyBatisBatchItemWriter。而在 2.0.0 中,还提供了三个建造器(builder)类来对 Java 配置提供支持:MyBatisPagingItemReaderBuilder、MyBatisCursorItemReaderBuilder 和 MyBatisBatchItemWriterBuilder。
提示 本章是关于 Spring Batch 的,而不是关于 MyBatis 的批量 SqlSession。要查找关于批量 session 的更多信息,请参考 使用 SqlSession 一章。
这个 bean 是一个 ItemReader,能够从数据库中分页地读取记录。
它执行 setQueryId 属性指定的查询来获取请求的数据。这个查询使用 setPageSize 属性指定了分页请求的大小,并被执行。其它的页面将在必要时被请求(例如调用 read() 方法时),返回对应位置上的对象。
reader 还提供了一些标准的请求参数。在命名查询的 SQL 中,必须使用部分或全部的参数(视乎 SQL 方言而定)来构造指定大小的结果集。这些参数是:
它们可以被映射成如下的 select 语句:
<select id="getEmployee" resultMap="employeeBatchResult">
SELECT id, name, job FROM employees ORDER BY id ASC LIMIT #{_skiprows}, #{_pagesize}
</select>配合如下的配置样例:
<bean id="reader" class="org.mybatis.spring.batch.MyBatisPagingItemReader"> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> <property name="queryId" value="com.my.name.space.batch.EmployeeMapper.getEmployee" /> </bean>
@Bean
public MyBatisPagingItemReader<Employee> reader() {
return new MyBatisPagingItemReaderBuilder<Employee>()
.sqlSessionFactory(sqlSessionFactory())
.queryId("com.my.name.space.batch.EmployeeMapper.getEmployee")
.build();
}让我们通过一个更复杂一点的例子来阐明一切:
<bean id="dateBasedCriteriaReader"
class="org.mybatis.spring.batch.MyBatisPagingItemReader"
p:sqlSessionFactory-ref="batchReadingSessionFactory"
p:parameterValues-ref="datesParameters"
p:queryId="com.my.name.space.batch.ExampleMapper.queryUserInteractionsOnSpecificTimeSlot"
p:pageSize="200"
scope="step"/>
<util:map id="datesParameters" scope="step">
<entry key="yesterday" value="#{jobExecutionContext['EXTRACTION_START_DATE']}"/>
<entry key="today" value="#{jobExecutionContext['TODAY_DATE']}"/>
<entry key="first_day_of_the_month" value="#{jobExecutionContext['FIRST_DAY_OF_THE_MONTH_DATE']}"/>
<entry key="first_day_of_the_previous_month" value="#{jobExecutionContext['FIRST_DAY_OF_THE_PREVIOUS_MONTH_DATE']}"/>
</util:map>
@StepScope
@Bean
public MyBatisPagingItemReader<User> dateBasedCriteriaReader(
@Value("#{@datesParameters}") Map<String, Object> datesParameters) throws Exception {
return new MyBatisPagingItemReaderBuilder<User>()
.sqlSessionFactory(batchReadingSessionFactory())
.queryId("com.my.name.space.batch.ExampleMapper.queryUserInteractionsOnSpecificTimeSlot")
.parameterValues(datesParameters)
.pageSize(200)
.build();
}
@StepScope
@Bean
public Map<String, Object> datesParameters(
@Value("#{jobExecutionContext['EXTRACTION_START_DATE']}") LocalDate yesterday,
@Value("#{jobExecutionContext['TODAY_DATE']}") LocalDate today,
@Value("#{jobExecutionContext['FIRST_DAY_OF_THE_MONTH_DATE']}") LocalDate firstDayOfTheMonth,
@Value("#{jobExecutionContext['FIRST_DAY_OF_THE_PREVIOUS_MONTH_DATE']}") LocalDate firstDayOfThePreviousMonth) {
Map<String, Object> map = new HashMap<>();
map.put("yesterday", yesterday);
map.put("today", today);
map.put("first_day_of_the_month", firstDayOfTheMonth);
map.put("first_day_of_the_previous_month", firstDayOfThePreviousMonth);
return map;
}上面的样例使用了几个东西:
这个 bean 是一个 ItemReader ,能够通过游标从数据库中读取记录。
提示 为了使用这个 bean,你需要使用 MyBatis 3.4.0 或更新的版本。
它执行 setQueryId 属性指定的查询来获取请求的数据(通过 selectCursor()方法)。每次调用 read() 方法时,将会返回游标指向的下个元素,直到没有剩余的元素了。
这个 reader 将会使用一个单独的数据库连接,因此 select 语句将不会参与到 step 处理中创建的任何事务。
当使用游标时,只需要执行普通的查询:
<select id="getEmployee" resultMap="employeeBatchResult"> SELECT id, name, job FROM employees ORDER BY id ASC </select>
搭配以下的配置样例:
<bean id="reader" class="org.mybatis.spring.batch.MyBatisCursorItemReader"> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> <property name="queryId" value="com.my.name.space.batch.EmployeeMapper.getEmployee" /> </bean>
@Bean
public MyBatisCursorItemReader<Employee> reader() {
return new MyBatisCursorItemReaderBuilder<Employee>()
.sqlSessionFactory(sqlSessionFactory())
.queryId("com.my.name.space.batch.EmployeeMapper.getEmployee")
.build();
}这是一个 ItemWriter,通过利用 SqlSessionTemplate 中的批量处理功能来对给定的所有记录执行多个语句。SqlSessionFactory 需要被配置为 BATCH 执行类型。
当调用 write() 时,将会执行 statementId
属性中指定的映射语句。一般情况下,write()
应该在一个事务中进行调用。
下面是一个配置样例:
<bean id="writer" class="org.mybatis.spring.batch.MyBatisBatchItemWriter"> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> <property name="statementId" value="com.my.name.space.batch.EmployeeMapper.updateEmployee" /> </bean>
@Bean
public MyBatisBatchItemWriter<User> writer() {
return new MyBatisBatchItemWriterBuilder<User>()
.sqlSessionFactory(sqlSessionFactory())
.statementId("com.my.name.space.batch.EmployeeMapper.updateEmployee")
.build();
}将 ItemReader 读取的记录转换为任意的参数对象:
默认情况下,MyBatisBatchItemWriter 会将 ItemReader 读取的对象(或 ItemProcessor 转换过的对象) 以参数对象的形式传递给 MyBatis(SqlSession#update())。 如果你想自定义传递给 MyBatis 的参数对象,可以使用 itemToParameterConverter 选项。 使用该选项后,可以传递任意对象给 MyBatis。 举个例子:
首先,创建一个自定义的转换器类(或工厂方法)。以下例子使用了工厂方法。
public static <T> Converter<T, Map<String, Object>> createItemToParameterMapConverter(String operationBy, LocalDateTime operationAt) {
return item -> {
Map<String, Object> parameter = new HashMap<>();
parameter.put("item", item);
parameter.put("operationBy", operationBy);
parameter.put("operationAt", operationAt);
return parameter;
};
}接下来,编写 SQL 映射。
<select id="createPerson" resultType="org.mybatis.spring.sample.domain.Person">
insert into persons (first_name, last_name, operation_by, operation_at)
values(#{item.firstName}, #{item.lastName}, #{operationBy}, #{operationAt})
</select>最后,配置 MyBatisBatchItemWriter。
// Java 配置样例
@Bean
public MyBatisBatchItemWriter<Person> writer() throws Exception {
return new MyBatisBatchItemWriterBuilder<Person>()
.sqlSessionFactory(sqlSessionFactory())
.statementId("org.mybatis.spring.sample.mapper.PersonMapper.createPerson")
.itemToParameterConverter(createItemToParameterMapConverter("batch_java_config_user", LocalDateTime.now()))
.build();
}
<!-- XML 配置样例 -->
<bean id="writer" class="org.mybatis.spring.batch.MyBatisBatchItemWriter">
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
<property name="statementId" value="org.mybatis.spring.sample.mapper.PersonMapper.createPerson"/>
<property name="itemToParameterConverter">
<bean class="org.mybatis.spring.sample.config.SampleJobConfig" factory-method="createItemToParameterMapConverter">
<constructor-arg type="java.lang.String" value="batch_xml_config_user"/>
<constructor-arg type="java.time.LocalDateTime" value="#{T(java.time.LocalDateTime).now()}"/>
</bean>
</property>
</bean>
使用复合 writer 对多个表进行写入(但带有问题):
这个小技巧只能在 MyBatis 3.2+ 以上的版本中使用,因为之前的版本中含有导致 writer 行为不正确的问题。
如果批量处理时需要写入复杂的数据,例如含有关联的记录,甚至是向多个数据库写入数据,你可能就需要一种办法来绕开 insert 语句只能插入到一个表中的限制。为了绕过此限制,批处理必须准备好要通过 writer 写入的项。然而,基于对被处理的数据的观察,可以尝试使用下面的方法来解决此问题。下面的方法能够工作于具有简单关联或不相关的多个表的项。
在这种方法中,处理 Spring Batch 项的处理器中将会持有各种不同的记录。假设每个项都有一个与 InteractionMetadata 相关联的 Interaction,并且还有两个不相关的行 VisitorInteraction 和 CustomerInteraction,这时候持有器(holder)看起来像这样:
public class InteractionRecordToWriteInMultipleTables {
private final VisitorInteraction visitorInteraction;
private final CustomerInteraction customerInteraction;
private final Interaction interaction;
// ...
}
public class Interaction {
private final InteractionMetadata interactionMetadata;
}在 Spring 配置中要配置一个 CompositeItemWriter,它将会将写入操作委托到特定种类的 writer 上面去。注意 InteractionMetadata 在例子里面是一个关联,它需要首先被写入,这样 Interaction 才能获得更新之后的键。
<bean id="interactionsItemWriter" class="org.springframework.batch.item.support.CompositeItemWriter">
<property name="delegates">
<list>
<ref bean="visitorInteractionsWriter"/>
<ref bean="customerInteractionsWriter"/>
<!-- 顺序很重要 -->
<ref bean="interactionMetadataWriter"/>
<ref bean="interactionWriter"/>
</list>
</property>
</bean>
@Bean
public CompositeItemWriter<?> interactionsItemWriter() {
CompositeItemWriter compositeItemWriter = new CompositeItemWriter();
List<ItemWriter<?>> writers = new ArrayList<>(4);
writers.add(visitorInteractionsWriter());
writers.add(customerInteractionsWriter());
writers.add(interactionMetadataWriter());
writers.add(interactionWriter());
compositeItemWriter.setDelegates(writers);
return compositeItemWriter;
}接下来需要配置每一个被委托的 writer;例如 Interaction 和 InteractionMetadata 对应的 writer。
<bean id="interactionMetadataWriter" class="org.mybatis.spring.batch.MyBatisBatchItemWriter" p:sqlSessionTemplate-ref="batchSessionTemplate" p:statementId="com.my.name.space.batch.InteractionRecordToWriteInMultipleTablesMapper.insertInteractionMetadata"/> <bean id="interactionWriter" class="org.mybatis.spring.batch.MyBatisBatchItemWriter" p:sqlSessionTemplate-ref="batchSessionTemplate" p:statementId="com.my.name.space.batch.InteractionRecordToWriteInMultipleTablesMapper.insertInteraction"/>
和 reader 中的一样,通过 statementId 属性指定对应命名空间前缀的查询。
而在映射器配置文件中,应该根据每种特定的记录编写特定的语句,如下所示:
<insert id="insertInteractionMetadata"
parameterType="com.my.batch.interactions.item.InteractionRecordToWriteInMultipleTables"
useGeneratedKeys="true"
keyProperty="interaction.interactionMetadata.id"
keyColumn="id">
<!-- 此 insert 语句使用了 #{interaction.interactionMetadata.property,jdbcType=...} -->
</insert>
<insert id="insertInteraction"
parameterType="com.my.batch.interactions.item.InteractionRecordToWriteInMultipleTables"
useGeneratedKeys="true"
keyProperty="interaction.id"
keyColumn="id">
<!--
此 insert 语句对普通的属性使用的是 #{interaction.property,jdbcType=...}
而对于 InteractionMetadata 属性使用的是 #{interaction.interactionMetadata.property,jdbcType=...}
-->
</insert>执行的时候会怎么样呢?首先,insertInteractionMetadata 将会被调用,update 语句被设置为返回由 JDBC 驱动返回的主键(参考 keyProperty 和 keyColumn)。由于 InteractionMetadata 的对象被此查询更新了,下一个查询将可以通过 insertInteraction 开展父对象 Interaction 的写入工作。
然而要注意,JDBC 驱动在这方面的行为并不总是与此相一致。在编写文档时,H2 的数据库驱动 1.3.168 甚至只在 BATCH 模式下返回最后的索引值(参考 org.h2.jdbc.JdbcStatement#getGeneratedKeys),而 MySQL 的 JDBC 驱动则工作良好并返回所有 ID。