為什么選擇 MyBatis
在 Martin Fowler 的企業(yè)應用架構模式中介紹了四種關系數據庫處理的模式蕉鸳。對于比較復雜的應用驶臊,比較常見的就是 active record 模式和 data mapper 模式。active record 正如 rails
的 activerecord
將面向業(yè)務的領域模型與數據實現綁定起來,JPA 就是采用的這種模式,通過標注可以將一個領域對象映射到數據庫表中。而 data mapper 則強調領域模型和關系型數據庫(當然读第,實際上也可以處理 noSQL 的)的數據結構是有差異的,需要一個 mapping 處理兩者的差異拥刻,不能將兩個東西融合成一個,這就是 MyBatis 所提供的能力父泳。雖然如今的 Spring Data 已經非常的強大了般哼,通過簡單的接口聲明就能夠創(chuàng)建一個可以完成 CRUD
的 Repository
,通過在對象之間建立關聯(lián)關系就能處理更復雜的聯(lián)表查詢惠窄。但是這樣子依然不能解決一系列的問題:
- 數據模型與領域模型的綁定:我還是需要把一個領域對象通過注解直接映射到數據對象蒸眠,但是有的時候我的領域對象是一個聚合根(Aggregate Root),它包含一系列實體(Entity)和值對象(Value Object)杆融,這簡單的注解做不到呀楞卡,我還是需要耗費很多的力氣去做
convertor
,那么使用 JPA 的優(yōu)勢就不再明顯了脾歇。 - 實現讀寫分離難度大蒋腮,我在 some tips for ddd 中有做解釋,DDD 關注的是一個寫模型藕各,關注領域的構建以及模型內數據的一致性池摧。然而 JPA 實際上并沒有考慮到這一點,它默認的實現是希望有一個統(tǒng)一的模型激况,不考慮讀寫模型的區(qū)別作彤,而在這個基礎上對其做讀寫的分離其難度是大于靈活性更強的 MyBatis 的膘魄。
- 通常在采用 rest api 進行數據展示的 GET 方法中所提供的數據是讀模型中的數據會使用大量的多表 join 以及參數的直接或間接映射,其實采用 jpa 的注解進行包裹反而顯得不方便了竭讳。我不認為 spring data 提供的那種查詢可以很好的處理创葡,至少在我參與的稍微復雜的項目中,內嵌在 JpaRepository 中的
@Query
注解和sql
語句隨處可見绢慢,相比這個蹈丸,直接用 MyBatis 的 xml mapping 以及其動態(tài) sql 的支持不是更好嗎? - 和 rails 的
activerecord
相比呐芥,它還是不夠好用...說的挺讓人傷心的逻杖,但是的確如此,努力了這么多年思瘟,就是做了一個activerecord
的弱化版荸百。那些快速的、用于忽悠的CRUD
樣例到目前為止滨攻,能和 rails 的腳手架比么...而且之前也提過够话,這種玩具代碼毫無意義,我們需要的是可以處理復雜應用的情況光绕,不然為啥不用 rails女嘲?
另外,不論是 DDD 的書籍诞帐,還是 Applying UML and Patterns 或者是 Spring 的開山鼻祖 Rod Johnson 的 expert one-on-one J2EE Development without EJB 都在強調能夠很好的實施面向對象的體系才是好的體系欣尼。MyBatis 做為一個 Data Mapper 的實現模式,完全的獨立于業(yè)務對象停蕉,它甚至都不需要在領域對象上提供任何的注解愕鼓。加上它 type handler
discriminator
的這些機制,可以很好的支持靈活的數據轉換方式以及對象的多態(tài)機制慧起。在面向比較簡單的應用開發(fā)時菇晃,它很顯然比 jpa 這樣的要繁瑣許多,顯得開發(fā)效率有點低蚓挤,但在應對各種復雜的場景的時候保持比較線性的開發(fā)速度而不需要大量高深的奇技淫巧磺送,是復雜業(yè)務系統(tǒng)開發(fā)的不二之選。
集成 Spring Boot 與 MyBatis
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 創(chuàng)建一個 Repository
的方式鞭执。有關 Repository 概念的內容可以在[這里]({% post_url 2016-05-17-ddd-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);
}
在業(yè)務領域芒粹,只有 User
UserRepository
而在具體的實現上,是采用了 MyBatisUserRepository
以及其所依賴的 UserMapper
具體的實現隱藏的很深大溜,好處就是支持未來對其進行替換化漆。
當然,很多時候钦奋、很多人都說尼瑪這種替換怎么可能座云,很明顯是想多了。但實際上我覺得未必如此付材,很多時候數據庫的切換不一定是說你已經積攢了 2TB 數據了才去這么做朦拖,比如在開發(fā)的末期出現了一些嚴重影響架構的因素導致需要對數據庫進行調整,你說這時候算早還是算晚呢厌衔?而且璧帝,通過技術手段盡量延遲決定本來就是一個很好的思路。再者富寿,測試環(huán)境和生產環(huán)境采用不同的 Repository 也是很常見的情況呀睬隶,硬綁定了不就都變成集成測試了嗎。
其中在代碼中 [1]
的那個注解 @Data
源自 lombok 大大減少了 java 的模板代碼页徐。
測試 MyBatis
前面提到的 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.
也就是說变勇,它會自動的幫助創(chuàng)建 embedded database 并且自動的采用 transactional 以及 rollback恤左。有了它我們真是只需要關注業(yè)務邏輯就行了。下面是對 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
其他
最后還是要講一些集成的額外內容赃梧。
- flyway 要求在項目的
src/main/resources
下有db/migration
的目錄,目錄中的 migration 腳本以V1__name
V2__name
V3__name
格式命名豌熄。更多內容見 flyway 官網。 - Mybatis 需要配置一個 mybatis-config.xml 文件物咳,并在
src/main/resources/application.properties
做一些配置锣险。 - 如果使用 XML 定義 Mapper 還需要在
application.properties
或者mybatis-config.xml
中指定 Mapper 的位置
完整的項目見 Github
更多內容請見 aisensiy.github.io