把 Spring Boot 1.5.3 與 MyBatis 集成

為什么選擇 MyBatis

在 Martin Fowler 的企業(yè)應用架構模式中介紹了四種關系數據庫處理的模式蕉鸳。對于比較復雜的應用驶臊,比較常見的就是 active record 模式和 data mapper 模式。active record 正如 railsactiverecord 將面向業(yè)務的領域模型與數據實現綁定起來,JPA 就是采用的這種模式,通過標注可以將一個領域對象映射到數據庫表中。而 data mapper 則強調領域模型和關系型數據庫(當然读第,實際上也可以處理 noSQL 的)的數據結構是有差異的,需要一個 mapping 處理兩者的差異拥刻,不能將兩個東西融合成一個,這就是 MyBatis 所提供的能力父泳。雖然如今的 Spring Data 已經非常的強大了般哼,通過簡單的接口聲明就能夠創(chuàng)建一個可以完成 CRUDRepository,通過在對象之間建立關聯(lián)關系就能處理更復雜的聯(lián)表查詢惠窄。但是這樣子依然不能解決一系列的問題:

  1. 數據模型與領域模型的綁定:我還是需要把一個領域對象通過注解直接映射到數據對象蒸眠,但是有的時候我的領域對象是一個聚合根(Aggregate Root),它包含一系列實體(Entity)和值對象(Value Object)杆融,這簡單的注解做不到呀楞卡,我還是需要耗費很多的力氣去做 convertor,那么使用 JPA 的優(yōu)勢就不再明顯了脾歇。
  2. 實現讀寫分離難度大蒋腮,我在 some tips for ddd 中有做解釋,DDD 關注的是一個寫模型藕各,關注領域的構建以及模型內數據的一致性池摧。然而 JPA 實際上并沒有考慮到這一點,它默認的實現是希望有一個統(tǒng)一的模型激况,不考慮讀寫模型的區(qū)別作彤,而在這個基礎上對其做讀寫的分離其難度是大于靈活性更強的 MyBatis 的膘魄。
  3. 通常在采用 rest api 進行數據展示的 GET 方法中所提供的數據是讀模型中的數據會使用大量的多表 join 以及參數的直接或間接映射,其實采用 jpa 的注解進行包裹反而顯得不方便了竭讳。我不認為 spring data 提供的那種查詢可以很好的處理创葡,至少在我參與的稍微復雜的項目中,內嵌在 JpaRepository 中的 @Query 注解和 sql 語句隨處可見绢慢,相比這個蹈丸,直接用 MyBatis 的 xml mapping 以及其動態(tài) sql 的支持不是更好嗎?
  4. 和 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

其他

最后還是要講一些集成的額外內容赃梧。

  1. flyway 要求在項目的 src/main/resources 下有 db/migration 的目錄,目錄中的 migration 腳本以 V1__name V2__name V3__name 格式命名豌熄。更多內容見 flyway 官網
  2. Mybatis 需要配置一個 mybatis-config.xml 文件物咳,并在 src/main/resources/application.properties 做一些配置锣险。
  3. 如果使用 XML 定義 Mapper 還需要在 application.properties 或者 mybatis-config.xml 中指定 Mapper 的位置

完整的項目見 Github

更多內容請見 aisensiy.github.io

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市览闰,隨后出現的幾起案子芯肤,更是在濱河造成了極大的恐慌,老刑警劉巖压鉴,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件崖咨,死亡現場離奇詭異,居然都是意外死亡油吭,警方通過查閱死者的電腦和手機击蹲,發(fā)現死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門署拟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人歌豺,你說我怎么就攤上這事推穷。” “怎么了类咧?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵馒铃,是天一觀的道長。 經常有香客問我痕惋,道長区宇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任值戳,我火速辦了婚禮议谷,結果婚禮上,老公的妹妹穿的比我還像新娘述寡。我一直安慰自己柿隙,他們只是感情好,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布鲫凶。 她就那樣靜靜地躺著禀崖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪螟炫。 梳的紋絲不亂的頭發(fā)上波附,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音昼钻,去河邊找鬼掸屡。 笑死,一個胖子當著我的面吹牛然评,可吹牛的內容都是我干的仅财。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼碗淌,長吁一口氣:“原來是場噩夢啊……” “哼盏求!你這毒婦竟也來了?” 一聲冷哼從身側響起亿眠,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤碎罚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后纳像,有當地人在樹林里發(fā)現了一具尸體荆烈,經...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年竟趾,在試婚紗的時候發(fā)現自己被綠了憔购。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宫峦。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖倦始,靈堂內的尸體忽然破棺而出斗遏,到底是詐尸還是另有隱情,我是刑警寧澤鞋邑,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布诵次,位于F島的核電站,受9級特大地震影響枚碗,放射性物質發(fā)生泄漏逾一。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一肮雨、第九天 我趴在偏房一處隱蔽的房頂上張望遵堵。 院中可真熱鬧,春花似錦怨规、人聲如沸陌宿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽壳坪。三九已至,卻和暖如春掰烟,著一層夾襖步出監(jiān)牢的瞬間爽蝴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工纫骑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蝎亚,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓先馆,卻偏偏與公主長得像发框,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子煤墙,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內容