SpringBoot2.x入門:使用MyBatis
阿新 • • 發佈:2020-07-19
> 這是公眾號《Throwable文摘》釋出的第**25**篇原創文章,收錄於專輯《SpringBoot2.x入門》。
## 前提
這篇文章是《SpringBoot2.x入門》專輯的**第8篇**文章,使用的`SpringBoot`版本為`2.3.1.RELEASE`,`JDK`版本為`1.8`。
`SpringBoot`專案引入`MyBatis`一般的套路是直接引入`mybatis-spring-boot-starter`或者使用基於`MyBatis`進行二次封裝的框架例如`MyBatis-Plus`或者`tk.mapper`等,但是本文會使用一種更加原始的方式,單純依賴`org.mybatis:mybatis`和`org.mybatis:mybatis-spring`把`MyBatis`的功能整合到`SpringBoot`中,`Spring(Boot)`使用的是**微核心架構**,任何第三方框架或者外掛都可以按照本文的思路融合到該微核心中。
## 引入MyBatis依賴
編寫本文的時候(`2020-07-18`)`org.mybatis:mybatis`的最新版本是`3.5.5`,而`org.mybatis:mybatis-spring`的最新版本是`2.0.5`,在使用`BOM`管理`SpringBoot`版本的前提下,引入下面的依賴:
```xml
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-jdbc
org.mybatis
mybatis
3.5.5
org.mybatis
mybatis-spring
2.0.5
```
> 注意的是低版本的MyBatis如果需要使用JDK8的日期時間API,需要額外引入mybatis-typehandlers-jsr310依賴,但是某個版本之後mybatis-typehandlers-jsr310中的類已經移植到org.mybatis:mybatis中作為內建類,可以放心使用JDK8的日期時間API。
## 新增MyBatis配置
`MyBatis`的核心模組是`SqlSessionFactory`與`MapperScannerConfigurer`。前者可以使用`SqlSessionFactoryBean`,功能是為每個`SQL`的執行提供`SqlSession`和載入全域性配置或者`SQL`實現的`XML`檔案,後者是一個`BeanDefinitionRegistryPostProcessor`實現,主要功能是主動通過配置的基礎包(`Base Package`)中遞迴搜尋`Mapper`介面(這個算是`MyBatis`獨有的掃描階段,**務必指定明確的掃描包,否則會因為效率太低導致啟動階段耗時增加**),並且把它們註冊成`MapperFactoryBean`(簡單理解為介面動態代理實現新增到方法快取中,並且委託到`IOC`容器,此後可以直接注入`Mapper`介面),注意這個`BeanFactoryPostProcessor`的回撥優先順序極高,在自動裝配`@Autowired`族註解或者`@ConfigurationProperties`屬性繫結處理之前已經回撥,**因此在處理`MapperScannerConfigurer`的屬性配置時候絕對不能使用`@Value`或者自定義字首屬性`Bean`進行自動裝配**,但是可以從`Environment`中直接獲取。
這裡新增一個自定義屬性字首`mybatis`,用於繫結配置檔案中的屬性到`MyBatisProperties`類中:
```java
@ConfigurationProperties(prefix = "mybatis")
@Data
public class MyBatisProperties {
private String configLocation;
private String mapperLocations;
private String mapperPackages;
private static final ResourcePatternResolver RESOLVER = new PathMatchingResourcePatternResolver();
/**
* 轉化Mapper對映檔案為Resource
*/
public Resource[] getMapperResourceArray() {
if (!StringUtils.hasLength(mapperLocations)) {
return new Resource[0];
}
List resources = new ArrayList<>();
String[] locations = StringUtils.commaDelimitedListToStringArray(mapperLocations);
for (String location : locations) {
try {
resources.addAll(Arrays.asList(RESOLVER.getResources(location)));
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
return resources.toArray(new Resource[0]);
}
}
```
接著新增一個`MybatisAutoConfiguration`用於配置`SqlSessionFactory`:
```java
@Configuration
@EnableConfigurationProperties(value = {MyBatisProperties.class})
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@RequiredArgsConstructor
public class MybatisAutoConfiguration {
private final MyBatisProperties myBatisProperties;
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// 其實核心配置就是這兩項,其他TypeHandlersPackage、TypeAliasesPackage等等自行斟酌是否需要新增
bean.setConfigLocation(new ClassPathResource(myBatisProperties.getConfigLocation()));
bean.setMapperLocations(myBatisProperties.getMapperResourceArray());
return bean;
}
/**
* 事務模板,用於程式設計式事務 - 可選配置
*/
@Bean
@ConditionalOnMissingBean
public TransactionTemplate transactionTemplate(PlatformTransactionManager platformTransactionManager) {
return new TransactionTemplate(platformTransactionManager);
}
/**
* 資料來源事務管理器 - 可選配置
*/
@Bean
@ConditionalOnMissingBean
public PlatformTransactionManager platformTransactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
```
一般情況下,啟用事務需要定義`PlatformTransactionManager`的實現,而`TransactionTemplate`適用於程式設計式事務(和宣告式事務`@Transactional`區別,程式設計式更加靈活)。上面的配置類中只使用了兩個屬性,而`mybatis.mapperPackages`將用於`MapperScannerConfigurer`的載入上。新增`MapperScannerRegistrarConfiguration`如下:
```java
@Configuration
public class MapperScannerRegistrarConfiguration {
public static class AutoConfiguredMapperScannerRegistrar implements
BeanFactoryAware, EnvironmentAware, ImportBeanDefinitionRegistrar {
private Environment environment;
private BeanFactory beanFactory;
@Override
public void setBeanFactory(@NonNull BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public void setEnvironment(@NonNull Environment environment) {
this.environment = environment;
}
@Override
public void registerBeanDefinitions(@NonNull AnnotationMetadata importingClassMetadata,
@NonNull BeanDefinitionRegistry registry) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
StringJoiner joiner = new StringJoiner(ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
// 這裡使用了${mybatis.mapperPackages},否則會使用AutoConfigurationPackages.get(this.beanFactory)獲取專案中自定義配置的包
String mapperPackages = environment.getProperty("mybatis.mapperPackages");
if (null != mapperPackages) {
String[] stringArray = StringUtils.commaDelimitedListToStringArray(mapperPackages);
for (String pkg : stringArray) {
joiner.add(pkg);
}
} else {
List packages = AutoConfigurationPackages.get(this.beanFactory);
for (String pkg : packages) {
joiner.add(pkg);
}
}
builder.addPropertyValue("basePackage", joiner.toString());
BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
Stream.of(beanWrapper.getPropertyDescriptors())
.filter(x -> "lazyInitialization".equals(x.getName())).findAny()
.ifPresent(x -> builder.addPropertyValue("lazyInitialization",
"${mybatis.lazyInitialization:false}"));
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
}
}
@Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
public static class MapperScannerRegistrarNotFoundConfiguration {
}
}
```
到此基本的配置`Bean`已經定義完畢,接著需要新增配置項。一般一個專案的`MyBatis`配置是相對固定的,可以直接新增在主配置檔案`application.properties`中:
```properties
server.port=9098
spring.application.name=ch8-mybatis
mybatis.configLocation=mybatis-config.xml
mybatis.mapperLocations=classpath:mappings/base,classpath:mappings/ext
mybatis.mapperPackages=club.throwable.ch8.repository.mapper,club.throwable.ch8.repository
```
個人喜歡在`resource/mappings`目錄下定義`base`和`ext`兩個目錄,`base`目錄用於存在`MyBatis`生成器生成的`XML`檔案,這樣就能在後續添加了表字段之後直接重新生成和覆蓋`base`目錄下對應的`XML`檔案即可。同理,在專案的原始碼包下建`repository/mapper`,然後`Mapper`類直接存放在`repository/mapper`目錄,`DAO`類存放在`repository`目錄,`MyBatis`生成器生成的`Mapper`類可以直接覆蓋`repository/mapper`目錄中對應的類。
`resources`目錄下新增一個`MyBatis`的全域性配置檔案`mybatis-config.xml`:
```xml
```
專案目前的基本結構如下:
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202007/sp-g-ch8-1.png)
## 使用Mybatis
為了簡單起見,這裡使用`h2`記憶體資料庫進行演示。新增`h2`的依賴:
```xml
com.h2database
h2
1.4.200
```
`resources`目錄下新增一個`schema.sql`和`data.sql`:
```sql
// resources/schema.sql
drop table if exists customer;
create table customer
(
id bigint generated by default as identity,
customer_name varchar(32),
age int,
create_time timestamp default current_timestamp,
edit_time timestamp default current_timestamp,
primary key (id)
);
// resources/data.sql
INSERT INTO customer(customer_name,age) VALUES ('doge', 22);
INSERT INTO customer(customer_name,age) VALUES ('throwable', 23);
```
新增對應的實體類`club.throwable.ch8.entity.Customer`:
```java
@Data
public class Customer {
private Long id;
private String customerName;
private Integer age;
private LocalDateTime createTime;
private LocalDateTime editTime;
}
```
新增`Mapper`和`DAO`類:
```java
// club.throwable.ch8.repository.mapper.CustomerMapper
public interface CustomerMapper {
}
// club.throwable.ch8.repository.CustomerDao
public interface CustomerDao extends CustomerMapper {
Customer queryByName(@Param("customerName") String customerName);
}
```
新增`XML`檔案`resource/mappings/base/BaseCustomerMapper.xml`和`resource/mappings/base/ExtCustomerMapper.xml`:
```xml
// BaseCustomerMapper.xml
// ExtCustomerMapper.xml
```
> 細心的夥伴會發現,DAO和Mapper類是繼承關係,而ext和base下對應的Mapper檔案中的BaseResultMap也是繼承關係
配置檔案中增加`h2`資料來源的配置:
```properties
// application.properties
spring.datasource.url=jdbc:h2:mem:db_customer;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=FALSE
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=org.h2.Driver
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.h2.console.settings.web-allow-others=true
spring.datasource.schema=classpath:schema.sql
spring.datasource.data=classpath:data.sql
```
新增一個啟動類進行驗證:
```java
public class Ch8Application implements CommandLineRunner {
@Autowired
private CustomerDao customerDao;
@Autowired
private ObjectMapper objectMapper;
public static void main(String[] args) {
SpringApplication.run(Ch8Application.class, args);
}
@Override
public void run(String... args) throws Exception {
Customer customer = customerDao.queryByName("doge");
log.info("Query [name=doge],result:{}", objectMapper.writeValueAsString(customer));
customer = customerDao.queryByName("throwable");
log.info("Query [name=throwable],result:{}", objectMapper.writeValueAsString(customer));
}
}
```
執行結果如下:
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202007/sp-g-ch8-2.png)
## 使用Mybatis生成器生成Mapper檔案
有些時候為了提高開發效率,更傾向於使用生成器去預生成一些已經具備簡單`CRUD`方法的`Mapper`檔案,這個時候可以使用`mybatis-generator-core`。編寫本文的時候(`2020-07-18`)`mybatis-generator-core`的最新版本為`1.4.0`,`mybatis-generator-core`可以通過程式設計式使用或者`Maven`外掛形式使用。
這裡僅僅簡單演示一下`Maven`外掛形式下使用`mybatis-generator-core`的方式,關於`mybatis-generator-core`後面會有一篇數萬字的文章詳細介紹此生成器的使用方式和配置項的細節。在專案的`resources`目錄下新增一個`generatorConfig.xml`:
```xml
```
然後再專案的`POM`檔案新增一個`Maven`外掛:
```xml
org.mybatis.generator
mybatis-generator-maven-plugin
1.4.0
Generate MyBatis Artifacts
generate
jdbc:h2:mem:db_customer;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=FALSE
org.h2.Driver
root
123456
${basedir}/src/main/resources/schema.sql
com.h2database
h2
1.4.200
```
> 筆者發現這裡必須要在外掛的配置中重新定義資料庫連線屬性和schema.sql,因為外掛跑的時候無法使用專案中已經啟動的h2例項,具體原因未知。
配置完畢之後,執行`Maven`命令:
```shell
mvn -Dmybatis.generator.overwrite=true mybatis-generator:generate -X
```
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202007/sp-g-ch8-3.png)
然後`resource/mappings/base`目錄下新增了一個帶有基本`CRUD`方法實現的`CustomerMapper.xml`,同時`CustoemrMapper`介面和`Customer`實體也被重新覆蓋生成了。
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202007/sp-g-ch8-4.png)
> 這裡把前面手動編寫的BaseCustomerMapper.xml註釋掉,預防衝突。另外,CustomerMapper.xml的insertSelective標籤需要加上keyColumn="id" keyProperty="id" useGeneratedKeys="true"屬性,用於實體insert後的主鍵回寫。
最後,修改並重啟啟動一下`Ch8Application`驗證結果:
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202007/sp-g-ch8-5.png)
## 小結
這篇文章相對詳細地介紹了`SpringBoot`專案如何使用`MyBatis`,如果需要連線`MySQL`或者其他資料庫,只需要修改資料來源配置和`MyBatis`生成器的配置檔案即可,其他配置類和專案骨架可以直接複用。
本文`demo`倉庫:
- `Github`:https://github.com/zjcscut/spring-boot-guide/tree/master/ch8-mybatis
(本文完 c-2-d e-a-20200719 封面來自《秒速五釐米》)
公眾號《Throwable文摘》(id:throwable-doge),不定期推送架構設計、併發、原始碼探究相關的原創文章:
![](https://public-1256189093.cos.ap-guangzhou.myqcloud.com/static/wechat-account-l