把 Spring Boot 1.5.3 与 MyBatis 集成

为什么选择 MyBatis

在 Martin Fowler 的企业应用架构模式中介绍了四种关系数据库处理的模式。对于比较复杂的应用,比较常见的就是 active record 模式和 data mapper 模式。active record 正如 railsactiverecord 将面向业务的领域模型与数据实现绑定起来,JPA 就是采用的这种模式,通过标注可以将一个领域对象映射到数据库表中。而 data mapper 则强调领域模型和关系型数据库(当然,实际上也可以处理 noSQL 的)的数据结构是有差异的,需要一个 mapping 处理两者的差异,不能将两个东西融合成一个,这就是 MyBatis 所提供的能力。虽然如今的 Spring Data 已经非常的强大了,通过简单的接口声明就能够创建一个可以完成 CRUDRepository,通过在对象之间建立关联关系就能处理更复杂的联表查询。但是这样子依然不能解决一系列的问题:

  1. 数据模型与领域模型的绑定:我还是需要把一个领域对象通过注解直接映射到数据对象,但是有的时候我的领域对象是一个聚合根(Aggregate Root),它包含一系列实体(Entity)和值对象(Value Object),这简单的注解做不到呀,我还是需要耗费很多的力气去做 convertor,那么使用 JPA 的优势就不再明显了。
  2. 实现读写分离难度大,我在 some tips for ddd 中有做解释,DDD 关注的是一个写模型,关注领域的构建以及模型内数据的一致性。然而 JPA 实际上并没有考虑到这一点,它默认的实现是希望有一个统一的模型,不考虑读写模型的区别,而在这个基础上对其做读写的分离其难度是大于灵活性更强的 MyBatis 的。
  3. 通常在采用 rest api 进行数据展示的 GET 方法中所提供的数据是读模型中的数据会使用大量的多表 join 以及参数的直接或间接映射,其实采用 jpa 的注解进行包裹反而显得不方便了。我不认为 spring data 提供的那种查询可以很好的处理,至少在我参与的稍微复杂的项目中,内嵌在 JpaRepository 中的 @Query 注解和 sql 语句随处可见,相比这个,直接用 MyBatis 的 xml mapping 以及其动态 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 的实现模式,完全的独立于业务对象,它甚至都不需要在领域对象上提供任何的注解。加上它 type handler discriminator 的这些机制,可以很好的支持灵活的数据转换方式以及对象的多态机制。在面向比较简单的应用开发时,它很显然比 jpa 这样的要繁琐许多,显得开发效率有点低,但在应对各种复杂的场景的时候保持比较线性的开发速度而不需要大量高深的奇技淫巧,是复杂业务系统开发的不二之选。

集成 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 创建一个 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);
}

在业务领域,只有 User UserRepository 而在具体的实现上,是采用了 MyBatisUserRepository 以及其所依赖的 UserMapper 具体的实现隐藏的很深,好处就是支持未来对其进行替换。

当然,很多时候、很多人都说尼玛这种替换怎么可能,很明显是想多了。但实际上我觉得未必如此,很多时候数据库的切换不一定是说你已经积攒了 2TB 数据了才去这么做,比如在开发的末期出现了一些严重影响架构的因素导致需要对数据库进行调整,你说这时候算早还是算晚呢?而且,通过技术手段尽量延迟决定本来就是一个很好的思路。再者,测试环境和生产环境采用不同的 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.

也就是说,它会自动的帮助创建 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

其他

最后还是要讲一些集成的额外内容。

  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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容