MyBatis Generator 1.3.6版本开始支持新的一种runtime模式:MyBatis3DynamicSql。这是一种全新的模式,对java的版本要求最低是java 8。
该模式下不再生成 XML,不再生成 Example 类。文档里也推荐:join 操作要用到的 resultMap 应该是 XML 文件中的唯一元素。
MyBatis Generator (>= 1.3.6) 也已经提供支持,只需要将 context 的 targetRuntime 属性更改为 MyBatis3DynamicSQL 即可生成新的动态 SQL。
牛逼在哪里?它可构造任意 SQL:
SelectStatementProvider selectStatement = select(id, firstName, lastName, birthDate, employed, occupation)
    .from(employee)
    .where(firstName, isEqualTo("Bob"), or(firstName, isEqualTo("Alice")))
    .build()
    .render(RenderingStrategy.MYBATIS3);
List<Employee> employees = mapper.selectMany(selectStatement);MyBatis Generator 的配置文件里这样配置:
<generatorConfiguration> <context id="dsql" targetRuntime="MyBatis3DynamicSql"> <jdbcConnection driverClass="org.hsqldb.jdbcDriver" connectionURL="jdbc:hsqldb:mem:aname" /> <javaModelGenerator targetPackage="example.model" targetProject="src/main/java"/> <javaClientGenerator targetPackage="example.mapper" targetProject="src/main/java"/> <table tableName="FooTable" /> </context> </generatorConfiguration>
然后Run mybatis-generator:generate  就可以生成以下的三个类了:
model类、dao层的mapper类、*DynamicSqlSupport动态语句支持类。
本文的关注点是怎么在这种新模式下MyBatis3DynamicSql,使用关联查询, 就是对应老模式 MyBatis3 下的 association(一对一)、collection(一对多)。
依然拿下面的模型举例:
新闻表 news (其中字段cat_id对应分类表的id)
新闻分类表 news_categories
News.java 模型里添加:
    /* 自定义属性 */
    private NewsCategories cat;
    public NewsCategories getCat(){ return cat; }
    public void setCat(NewsCategories cat){ this.cat = cat; }
    /* 自定义属性 - 部分字段 */
    private Map catMap;
    public Map getCatMap(){ return catMap; }
    public void setCatMap(Map catMap){ this.catMap = catMap; }NewsCategories.java 模型里添加:
    // 自定义
    private List<News> newsList;
    public List<News> getNewsList(){ return newsList; }
    public void setNewsList(List<News> newsList){ this.newsList = newsList; }
    // 自定义 - Map 部分字段
    private List<Map> newsMapList;
    public List<Map> getNewsMapList(){ return newsMapList; }
    public void setNewsMapList(List<Map> newsMapList){ this.newsMapList = newsMapList; }模型里为什么定义2种形式呢?飘易是为了演示在某些场景下,只需要返回关联模型的一部分字段,比如有上百个字段但只需要几个字段,要去除部分敏感字段如密码等。定义为Map形式支持返回部分字段,而直接使用模型的话,即时对应字段没有传值,也会返回 null。
NewsMapper.java DAO层添加:
    // 自定义
    @Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: news")
    @SelectProvider(type=SqlProviderAdapter.class, method="select")
    @ResultMap("joinNewsResult")
    Optional<News> selectOneJoin(SelectStatementProvider selectStatement);
    // 自定义
    @Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: news")
    @SelectProvider(type=SqlProviderAdapter.class, method="select")
    @ResultMap("joinNewsResult")
    List<News> selectManyJoin(SelectStatementProvider selectStatement);NewsCategoriesMapper.java Dao添加:
    // 自定义 - join 连接查询
    @Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: news_categories")
    @SelectProvider(type=SqlProviderAdapter.class, method="select")
    @ResultMap("joinNewsCategoriesResult")
    Optional<NewsCategories> selectOneJoin(SelectStatementProvider selectStatement);
    // 自定义 - join 连接查询
    @Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: news_categories")
    @SelectProvider(type=SqlProviderAdapter.class, method="select")
    @ResultMap("joinNewsCategoriesResult")
    List<NewsCategories> selectManyJoin(SelectStatementProvider selectStatement);注意,使用join查询时依然需要配置xml文件,因为纯粹的注解方式并不能很好的定义关联关系 association(一对一)、collection(一对多);但是xml文件里只需要定义 resultMap 节点,不需要定义其他任何节点了。
NewsMapper.xml 里定义:
<mapper namespace="com.example.demo.mapper.NewsMapper"> <resultMap id="joinNewsResult" type="com.example.demo.model.News"> <id column="id" jdbcType="INTEGER" property="id" /> <result column="cat_id" jdbcType="INTEGER" property="catId" /> <result column="title" jdbcType="VARCHAR" property="title" /> <result column="keywords" jdbcType="VARCHAR" property="keywords" /> <result column="description" jdbcType="VARCHAR" property="description" /> <result column="filepaths" jdbcType="VARCHAR" property="filepaths" /> <result column="views" jdbcType="INTEGER" property="views" /> <result column="recommend" jdbcType="TINYINT" property="recommend" /> <result column="created_at" jdbcType="TIMESTAMP" property="createdAt" /> <result column="updated_at" jdbcType="TIMESTAMP" property="updatedAt" /> <result column="content" jdbcType="LONGVARCHAR" property="content" /> <!-- 一对一关联关系 --> <association property="cat" javaType="com.example.demo.model.NewsCategories"> <id column="c_id" jdbcType="INTEGER" property="id"/> <result column="c_title" jdbcType="VARCHAR" property="title"/> </association> <association property="catMap" javaType="map"> <id column="c_id" jdbcType="INTEGER" property="id"/> <result column="c_title" jdbcType="VARCHAR" property="title"/> </association> </resultMap> </mapper>
NewsCategoriesMapper.xml 文件里定义:
<mapper namespace="com.example.demo.mapper.NewsCategoriesMapper"> <resultMap id="joinNewsCategoriesResult" type="com.example.demo.model.NewsCategories"> <id column="id" jdbcType="INTEGER" property="id" /> <result column="title" jdbcType="VARCHAR" property="title" /> <result column="keywords" jdbcType="VARCHAR" property="keywords" /> <result column="description" jdbcType="VARCHAR" property="description" /> <result column="sort_id" jdbcType="INTEGER" property="sortId" /> <result column="created_at" jdbcType="TIMESTAMP" property="createdAt" /> <result column="updated_at" jdbcType="TIMESTAMP" property="updatedAt" /> <!-- 一对多关联关系 --> <collection property="newsList" ofType="com.example.demo.model.News"> <id column="n_id" jdbcType="INTEGER" property="id" /> <result column="cat_id" jdbcType="INTEGER" property="catId" /> <result column="n_title" jdbcType="VARCHAR" property="title" /> <result column="keywords" jdbcType="VARCHAR" property="keywords" /> <result column="description" jdbcType="VARCHAR" property="description" /> <result column="filepaths" jdbcType="VARCHAR" property="filepaths" /> <result column="views" jdbcType="INTEGER" property="views" /> <result column="recommend" jdbcType="TINYINT" property="recommend" /> <result column="n_created_at" jdbcType="TIMESTAMP" property="createdAt" /> <result column="updated_at" jdbcType="TIMESTAMP" property="updatedAt" /> <result column="content" jdbcType="LONGVARCHAR" property="content" /> </collection> <collection property="newsMapList" ofType="java.util.Map"> <id column="n_id" jdbcType="INTEGER" property="id" /> <result column="n_title" jdbcType="VARCHAR" property="title" /> <result column="n_created_at" jdbcType="TIMESTAMP" property="createdAt" /> </collection> </resultMap> </mapper>
注意:association(一对一)里使用的属性是 javaType;而 collection(一对多)里使用的属性是 ofType!
好了,准备工作做好,下面飘易就在控制器里写控制层的代码了。
NewsController.java 里编写代码:
@RestController
public class NewsController {
    @Resource
    private NewsMapper newsMapper;
    @Resource
    private NewsCategoriesMapper newsCategoriesMapper;
    /**
     * 单个资源
     * */
    @GetMapping("/news/{id}")
    public Result item(@PathVariable("id") Integer id) {
        // 筛选
        SelectStatementProvider selectStatement = select(news.allColumns(),
                (newsCategories.id).as("c_id"), (newsCategories.title).as("c_title"))
                .from(news)
                .leftJoin(newsCategories).on(news.catId, equalTo(newsCategories.id))
                .where(news.id, isEqualTo(id))
                .build()
                .render(RenderingStrategies.MYBATIS3);
        Optional<News> news = newsMapper.selectOneJoin(selectStatement);
        return Result.data(news);
    }
    /**
     * 资源列表
     * */
    @GetMapping("/news")
    public Result itemList(@RequestParam Map<String,String> req) {
        // 分页页码参数
        int pageNum = req.containsKey("pageNum") ? Integer.parseInt(req.get("pageNum")) : Result.pageNum;
        int pageSize = req.containsKey("pageSize") ? Integer.parseInt(req.get("pageSize")) : Result.pageSize;
        // 开始分页
        PageHelper.startPage(pageNum, pageSize);// 紧跟着的第一个select方法会被分页
        // 筛选
        SelectStatementProvider selectStatement = select(news.allColumns(),
                (newsCategories.id).as("c_id"), (newsCategories.title).as("c_title"))
                .from(news)
                .leftJoin(newsCategories).on(news.catId, equalTo(newsCategories.id))
                .orderBy(sortColumn("news.id").descending())
                .build()
                .render(RenderingStrategies.MYBATIS3);
        List<News> list = newsMapper.selectManyJoin(selectStatement);
        PageInfo pageInfo = new PageInfo(list);// 分页包装
        return Result.list(pageInfo);
    }
    /**
     * 增加
     * @param news: 接受 Content-Type: application/x-www-form-urlencoded
     *            body里为 title=test&cat_id=1
     * */
    @PostMapping("/news")
    public Result insert(News news) {
        Date date = new Date();
        news.setCreatedAt(date);
        news.setUpdatedAt(date);
        Integer res = newsMapper.insertSelective(news);
        return Result.data(res);
    }
    /**
     * 编辑
     * */
    @PutMapping("/news/{id}")
    public Result update(@PathVariable("id") Integer id, News news) {
        Date date = new Date();
        news.setUpdatedAt(date);
        news.setId(id);
        Integer res = newsMapper.updateByPrimaryKeySelective(news);
        return Result.data(res);
    }
    /**
     * 删除
     * */
    @DeleteMapping("/news/{id}")
    public Result delete(@PathVariable("id") Integer id) {
        Integer res = newsMapper.deleteByPrimaryKey(id);
        return Result.data(res);
    }
}NewsCategoriesController.java 里编写代码:
@RestController
public class NewsCategoryController {
    @Resource
    private NewsMapper newsMapper;
    @Resource
    private NewsCategoriesMapper newsCategoriesMapper;
    /**
     * 单个资源
     * */
    @GetMapping("/news_category/{id}")
    public Result item(@PathVariable("id") Integer id) {
        SelectStatementProvider selectStatement = select(newsCategories.allColumns(),
                (news.id).as("n_id"),(news.title).as("n_title"),(news.createdAt).as("n_created_at"))
                .from(newsCategories)
                .leftJoin(news)
                .on(newsCategories.id, equalTo(news.catId))
                .where(newsCategories.id, isEqualTo(id))
                .orderBy(sortColumn("news_categories.id").descending())
                .build()
                .render(RenderingStrategies.MYBATIS3);
        // 单个
        Optional<NewsCategories> newsCategories = newsCategoriesMapper.selectOneJoin(selectStatement);
        return Result.data(newsCategories);
    }
    /**
     * 资源列表
     * */
    @GetMapping("/news_category")
    public Result itemList(@RequestParam Map<String,String> req) {
        // 分页页码参数
        int pageNum = req.containsKey("pageNum") ? Integer.parseInt(req.get("pageNum")) : Result.pageNum;
        int pageSize = req.containsKey("pageSize") ? Integer.parseInt(req.get("pageSize")) : Result.pageSize;
        // 开始分页
        PageHelper.startPage(pageNum, pageSize);// 紧跟着的第一个select方法会被分页
        // 筛选
        SelectStatementProvider selectStatement = select(newsCategories.allColumns(),
                (news.id).as("n_id"),(news.title).as("n_title"),(news.createdAt).as("n_created_at"))
                .from(newsCategories)
                .leftJoin(news)
                .on(newsCategories.id, equalTo(news.catId))
                .orderBy(sortColumn("news_categories.id").descending())
                .build()
                .render(RenderingStrategies.MYBATIS3);
        List<NewsCategories> list = newsCategoriesMapper.selectManyJoin(selectStatement);
        PageInfo pageInfo = new PageInfo(list);// 分页包装
        return Result.list(pageInfo);
    }
    /**
     * 增加
     * @param newsCategories: 接受 Content-Type: application/x-www-form-urlencoded
     *            body里为 title=test&cat_id=1
     * */
    @PostMapping("/news_category")
    public Result insert(NewsCategories newsCategories) {
        Date date = new Date();
        newsCategories.setCreatedAt(date);
        newsCategories.setUpdatedAt(date);
        Integer res = newsCategoriesMapper.insertSelective(newsCategories);
        return Result.data(res);
    }
    /**
     * 编辑
     * */
    @PutMapping("/news_category/{id}")
    public Result update(@PathVariable("id") Integer id, NewsCategories newsCategories) {
        Date date = new Date();
        newsCategories.setUpdatedAt(date);
        newsCategories.setId(id);
        Integer res = newsCategoriesMapper.updateByPrimaryKeySelective(newsCategories);
        return Result.data(res);
    }
    /**
     * 删除
     * */
    @DeleteMapping("/news_category/{id}")
    public Result delete(@PathVariable("id") Integer id) {
        Integer res = newsCategoriesMapper.deleteByPrimaryKey(id);
        return Result.data(res);
    }
}关联查询里,association(一对一)、collection(一对多)里 如果使用 select 属性的话,就会导致 N+1 的问题。我们使用 join 连接查询,这样可以避免产生 N+1 问题。
使用postman,查询 news 单个资源:

查询 news 列表:

查询 news_categories 单个资源:

查询 news_categories 列表:

【参考】
1、MyBatis Dynamic SQL Usage Notes:https://mybatis.org/generator/generatedobjects/dynamicSqlV2.html
2、MyBatis Dynamic SQL Quick Start:https://mybatis.org/mybatis-dynamic-sql/docs/quickStart.html