在 Martin Fowler 的企业应用架构模式中介绍了四种关系数据库处理的模式。对于比较复杂的应用,比较常见的就是 active record 模式和 data mapper 模式。active record 正如 rails
的 activerecord
将面向业务的领域模型与数据实现绑定起来,Hibernate 和 JPA 就是采用的这种模式,通过标注可以将一个领域对象映射到数据库表中。而 data mapper 则强调领域模型和关系型数据库(当然,实际上也可以处理 noSQL 的)的数据结构是有差异的,需要一个 mapping 处理两者的差异,不能将两个东西融合成一个,这就是 MyBatis 所提供的能力。虽然如今的 Spring Data 已经非常的强大了,通过简单的接口声明就能够创建一个可以完成 CRUD
的 Repository
,通过在对象之间建立关联关系就能处理更复杂的联表查询。但是这样子依然不能解决一系列的问题:
convertor
,那么使用 JPA 的优势就不再明显了。@Query
注解和 sql
语句随处可见。activerecord
相比,它还是不够好用...说的挺让人伤心的,但是的确如此,努力了这么多年,就是做了一个 activerecord
的弱化版。那些快速的、用于忽悠的 CRUD
样例到目前为止,能和 rails 的脚手架比么...而且之前也提过,这种玩具代码毫无意义,我们需要的是可以处理复杂应用的情况,不然为啥不用 rails?另外,不论是 DDD 的书籍,还是 Applying UML and Patterns 或者是 Spring 的开山鼻祖 Rod Johnson 的 expert one-on-one J2EE Development without EJB 都在强调能够很好的实施面向对象的体系才是好的体系。MyBatis 做为一个 Data Mapper 的实现模式,完全的独立与业务对象,加上它 type handler
discriminator
的这些机制,可以很好的支持灵活的数据转换方式以及对象的多态机制。绝对是复杂业务系统的不二之选。
MyBatis 提供了一个 starter
用于和 Spring Boot 的集成。build.gradle
如下:
buildscript {
ext {
springBootVersion = '1.5.3.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.flywaydb:flyway-core')
compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.0')
compile('org.springframework.boot:spring-boot-starter-web')
runtime('com.h2database:h2')
compileOnly('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.mybatis.spring.boot:mybatis-spring-boot-starter-test:1.3.0')
}
可以看到,首先我引用了 flyway
做数据 migration。然后我只用了一个 h2 内存数据库,然后除了 mybatis-spring-boot-starter
之外还有一个 mybatis-spring-boo-starter-test
后面会讲到它。
这里我们举一个简单的例子,展示用 MyBatis 创建一个 Repository
的方式。有关 Repository 概念的内容可以在这里看到。
// User.java
@Data // [1]
public class User {
private final String id;
private final String username;
public User(String id, String username) {
this.id = id;
this.username = username;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id) &&
Objects.equals(username, user.username);
}
@Override
public int hashCode() {
return Objects.hash(id, username);
}
}
// UserRepository.java
@Repository
public interface UserRepository {
void save(User user);
Optional<User> findById(String userId); // [2]
}
// MyBatisUserRepository.java
@Repository
public class MyBatisUserRepository implements UserRepository {
@Autowired
private UserMapper mapper; // [3]
@Override
public void save(User user) {
mapper.insert(user);
}
@Override
public Optional<User> findById(String id) {
return Optional.ofNullable(mapper.findById(id));
}
}
// UserMapper.java
@Component
@Mapper
public interface UserMapper {
void insert(@Param("user") User user);
User findById(@Param("id") String id);
}
在业务领域,只有 User
UserRepository
而在具体的实现上,是采用了 MyBatisUserRepository
以及其所依赖的 UserMapper
具体的实现隐藏的很深,好处就是支持未来对其进行替换。
当然,很多时候、很多人都说尼玛这种替换怎么可能,很明显是想多了。但实际上我觉得未必如此,很多时候数据库的切换不一定是说你已经积攒了 2TB 数据了才去这么做,比如在开发的末期出现了一些严重影响架构的因素导致需要对数据库进行调整,你说这时候算早还是算晚呢?而且,通过技术手段尽量延迟决定本来就是一个很好的思路。再者,测试环境和生产环境采用不同的 Repository 也是很常见的情况呀,硬绑定了不就都变成集成测试了吗。
其中在代码中 [1]
的那个注解 @Data
源自 lombok 大大减少了 java 的模板代码。
前面提到的 mybatis-spring-boot-starter-test
这里要排上用场了。它提供了一个超超超好用了注解 MyBatisTest
,官方对其解释如下:
By default it will configure MyBatis(MyBatis-Spring) components(SqlSessionFactory and SqlSessionTemplate), configure MyBatis mapper interfaces and configure an in-memory embedded database. MyBatis tests are transactional and rollback at the end of each test by default.
也就是说,它会自动的帮助创建 embedded database 并且自动的采用 transactional 以及 rollback。有了它我们真是只需要关注业务逻辑就行了。下面是对 MyBatisUserRepository
的测试。
@RunWith(SpringRunner.class)
@MybatisTest
@Import(MyBatisUserRepository.class)
public class MyBatisRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
public void should_save_user_success() throws Exception {
User user = new User(UUID.randomUUID().toString(), "abc");
userRepository.save(user);
Optional<User> userOptional = userRepository.findById(user.getId());
assertThat(userOptional.get(), is(user));
}
}
详细内容见 mybatis-spring-boot-test-autoconfigure
最后还是要讲一些集成的额外内容。
src/main/resources
下有 db/migration
的目录,目录中的 migration 脚本以 V1__name
V2__name
V3__name
格式命名。更多内容见 flyway 官网。src/main/resources/application.properties
做一些配置。application.properties
或者 mybatis-config.xml
中指定 Mapper 的位置完整的项目见 Github