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