原文连接:html
MyBatis 官网 是这么介绍它本身的:java
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎全部的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。mysql
这里仅展现和 MyBatis 相关的数据库依赖项,完整的示例,在文末会附上项目代码连接。git
<!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency>
spring-boot-start
系列的包,真是给 Spring Boot 开发带来了极大的便利,它的项目地址是:github
建立 users
表的 SQL:spring
SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for `users` -- ---------------------------- DROP TABLE IF EXISTS `users`; CREATE TABLE `users` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id', `userName` varchar(32) DEFAULT NULL COMMENT '用户名', `passWord` varchar(32) DEFAULT NULL COMMENT '密码', `user_sex` varchar(32) DEFAULT NULL, `nick_name` varchar(32) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of sys_user -- ---------------------------- INSERT INTO `users` VALUES (1,'michael翔', '123', 'MAN', 'zx'); INSERT INTO `users` VALUES (2,'张小敬', '123', 'MAN', 'zxj'); INSERT INTO `users` VALUES (3,'李司辰', '123', 'MAN', 'lsc'); INSERT INTO `users` VALUES (4,'崔器', '123', 'MAN', 'cq'); INSERT INTO `users` VALUES (5,'姚汝能', '123', 'MAN', 'yrn'); INSERT INTO `users` VALUES (null,'檀棋', '123', ' WOMAN', 'tq'); INSERT INTO `users` (`userName`,`passWord`,`user_sex`,`nick_name`) VALUES ('michael', '123', 'MAN', 'zx');
说明:sql
null
值,均可以;column
也能够不指定;application-dev.properties
:shell
swagger.enable=true server.port=8081 spring.datasource.url=jdbc:mysql://192.168.3.43:3306/beta?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver mybatis.type-aliases-package==com.michael.springbootmybatis.model mybatis.configuration.map-underscore-to-camel-case=true
userSex
,数据库属性为 user_sex
,MyBatis 默认是不能自动转换的。咱们能够配置 mybatis.configuration.map-underscore-to-camel-case
实现自动映射。若是不进行此配置,一般咱们要自定义如下结果集映射:@Results({ @Result(property = "userSex", column = "user_sex"), @Result(property = "nickName", column = "nick_name") }) @Select("SELECT * FROM users WHERE id = #{id}") UserEntity getUserById(Long id);
在不少 Select
语句须要作结果映射时,天然是至关麻烦。除了上面配置「驼峰属性自动映射」,也能够用在 @Results
中使用 id
来标识一个映射关系,而后能够用 @ResultMap
复用这个映射关系:数据库
@Select("SELECT * FROM users") @Results(id = "user", value = { @Result(property = "userSex", column = "user_sex"), @Result(property = "nickName", column = "nick_name") }) @Select("SELECT * FROM users WHERE id = #{id}") List<UserEntity> getAll(); @ResultMap("user") @Select("SELECT * FROM users WHERE id = #{id}") UserEntity getUserById(Integer id);
这里仅展现关键的部分的代码,完整可看下文的示例代码。apache
实体类:
@Data @ApiModel(description = "UserEntity 实体类") public class UserEntity implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty(value = "用户 id", dataType = "Long") private Long id; @ApiModelProperty(value = "用户名", required = true) private String userName; @ApiModelProperty(value = "密码") private String passWord; @ApiModelProperty(value = "性别") private UserSexEnum userSex; @ApiModelProperty(value = "昵称") private String nickName; @Override public String toString() { return "userName " + this.userName + ", password " + this.passWord + " , sex " + this.userSex; } }
dao/mapper
接口,数据库交互(Data Access Object
)层:
public interface UserMapper { // @Results({ // @Result(property = "userSex", column = "user_sex", javaType = UserSexEnum.class), // @Result(property = "nickName", column = "nick_name") // }) @Select("SELECT * FROM users") Page<UserEntity> getAll(); // @Results({ // @Result(property = "userSex", column = "user_sex"), // @Result(property = "nickName", column = "nick_name") // }) @Select("SELECT * FROM users WHERE id = #{id}") UserEntity getUserById(Long id); @Insert("INSERT INTO users(userName, passWord, user_sex, nick_name) " + "VALUES(#{userName}, #{passWord}, #{userSex}, #{nickName})") @Options(useGeneratedKeys = true, keyProperty = "id") // @SelectKey(statement = "select last_insert_id()", keyProperty = "id", before = false, resultType = Integer.class) void insert(UserEntity user); @Update("UPDATE users SET userName=#{userName},nick_name=#{nickName} WHERE id = #{id}") void update(UserEntity user); @Delete("DELETE FROM users WHERE id= #{id}") void deleteUserById(Long id); }
说明:
insert
这里用了一个 @Options
的注解,实现了「主键回填」的功能,也就是说,再建立好一个 user
以后,user
请求体中的 id
属性会自动赋值好;@SelectKey
注解被注释掉了,这个注解也一样能够实现「主键回填」的功能;service 接口:
public interface UserService { /** * 查询全部用户 * * @return */ Map<String,Object> getAll(int pageNum, int pageSize); /** * 根据用户 ID 查询用户 * * @param id 用户 ID * @return */ UserEntity getUserById(Long id); /** * 新增一个用户 * * @param user */ void insert(UserEntity user); /** * 更新用户信息,用户 ID 不传,会更新失败 * * @param user */ String update(UserEntity user); /** * 根据用户 ID 删除用户 * * @param id */ String deleteById(Long id); }
service 接口的实现类:
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public Map<String,Object> getAll(int pageNum, int pageSize) { //将参数传给这个方法就能够实现物理分页了,很是简单。 PageHelper.startPage(pageNum, pageSize); PageInfo<UserEntity> pageInfo = new PageInfo<>(userMapper.getAll()); Long total = pageInfo.getTotal(); List<UserEntity> users = pageInfo.getList(); Map<String,Object> map = new HashMap<>(); map.put("total", total); map.put("data", users); return map; } @Override public UserEntity getUserById(Long id) { return userMapper.getUserById(id); } @Override public void insert(UserEntity user) { userMapper.insert(user); } @Override public String update(UserEntity user) { userMapper.update(user); return "success"; } @Override public String deleteById(Long id) { userMapper.deleteUserById(id); return "success"; } }
controller 类:
@RestController @RequestMapping("/api/v1/") @Api(tags = {"用户相关接口"}, value = "用户模块") public class UserController { @Autowired private UserService userService; /** * 查询所有用户 * * @return */ @ApiOperation(value = "获取用户列表", notes = "获取所有用户信息") @RequestMapping(value = "/users", method = RequestMethod.GET) public Map<String,Object> getUsers( @RequestParam(name = "pageNum", defaultValue = "1", required = false) int pageNum, @RequestParam(name = "pateSize", defaultValue = "2", required = false) int pageSize) { return userService.getAll(pageNum, pageSize); } /** * 根据用户 ID 查询用户 * * @param id * @return */ @ApiOperation(value = "查询单用户", notes = "根据用户id 查询其信息") @ApiImplicitParam(name = "id", value = "用户id", paramType = "query", required = true) @GetMapping("/user/{id}") public UserEntity getUser(Long id) { UserEntity user = userService.getUserById(id); return user; } /** * 存储用户信息 * * @param user */ @ApiOperation(value = "存储用户信息", notes = "存储用户详细信息") @RequestMapping(value = "/user", method = RequestMethod.POST) public String save(UserEntity user) { userService.insert(user); // 用到了 主键回填 的配置 return "Create success, user id: " + user.getId(); } /** * 更新用户信息 * * @param user */ @ApiOperation(value = "更新用户信息", notes = "更新用户的我的信息") @PutMapping("/user/") public void update(@RequestBody UserEntity user) { userService.update(user); } /** * 根据用户 ID 删除用户 * * @param id */ @ApiOperation(value = "删除用户", notes = "根据用户id删除用户信息") @ApiImplicitParams({ @ApiImplicitParam(name = "id", value = "用户id", required = true, paramType = "path") }) @RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE) public void delete(@PathVariable("id") Long id) { userService.deleteById(id); } }
启动类:
@SpringBootApplication @MapperScan("com.michael.springbootmybatis.mapper") public class SpringBootMybatisApplication { public static void main(String[] args) { SpringApplication.run(SpringBootMybatisApplication.class, args); } }
@MapperScan("com.winter.mapper")
这个注解很是的关键,这个对应了项目中 mapper/dao
所对应的包路径。mapper/dao
类上使用 @Mapper
注解;一般,在进行查询时,咱们为了不一次性返回全部结果,一般会进行分页。好比查询全部用户的接口,实际应用中,用户数据可能会不少,若是所有一次返回,明显不合适。这时候,就须要进行分页查询。
本文咱们选用插键 pagehelper-spring-boot-starter
要进行分页。
<!-- 分页插件 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.5</version> </dependency>
须要添加相应的配置:
#pagehelper分页插件 pagehelper.helperDialect=mysql pagehelper.reasonable=true pagehelper.supportMethodsArguments=true pagehelper.params=count=countSql pagehelper.row-bounds-with-count=true pageSizeZero=true
分页插键参数介绍:
helperDialect
:分页插件会自动检测当前的数据库连接,自动选择合适的分页方式reasonable
:分页合理化参数,默认值为 false。当该参数设置为 true 时,pageNum<=0
时会查询第一页, pageNum>pages
(超过总数时),会查询最后一页。默认 false 时,直接根据参数进行查询params
:为了支持 startPage(Object params)
方法,增长了该参数来配置参数映射,用于从对象中根据属性名取值, 能够配置 pageNum,pageSize,count,pageSizeZero,reasonable
,不配置映射的用默认值, 默认值为 pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
。supportMethodsArguments
:支持经过 Mapper 接口参数来传递分页参数,默认值 false,分页插件会从查询方法的参数值中,自动根据上面 params
配置的字段中取值,查找到合适的值时就会自动分页pageSizeZero
:默认值为 false,当该参数设置为 true 时,若是 pageSize=0
或者 RowBounds.limit = 0
就会查询出所有的结果(至关于没有执行分页查询,可是返回结果仍然是 Page 类型)。我测试时,发现不设置,pageSize=0
也会返回所有;mapper
中查找所有用户的方法改为以下:
@Select("SELECT * FROM users") Page<UserEntity> getAll();
service 接口和其实现类的方法改为:
PageInfo<UserEntity> getAll(int pageNum, int pageSize);
service 接口实现类:
@Override public PageInfo<UserEntity> getAll(int pageNum, int pageSize) { //将参数传给这个方法就能够实现物理分页了,很是简单。 PageHelper.startPage(pageNum, pageSize); PageInfo<UserEntity> pageInfo = new PageInfo<>(userMapper.getAll()); return pageInfo; }
注意点:
PageHelper.startPage(pageNo,pageSize);
只对其后的第一个查询有效;controller 类:
@ApiOperation(value = "获取用户列表", notes = "获取所有用户信息") @RequestMapping(value = "/users", method = RequestMethod.GET) public PageInfo<UserEntity> getUsers( @RequestParam(name = "pageNum", defaultValue = "1", required = false) int pageNum, @RequestParam(name = "pateSize", defaultValue = "2", required = false) int pageSize) { return userService.getAll(pageNum, pageSize); }
除了上面的参数名,还习惯用下面的参数名:
offset
:和 pageNum
意思同样,指定返回记录的开始位置;limit
:和 pageSize
意思同样,指定返回记录的数量;上面的分页结果返回的内容有点多,一些属性并不想放在返回体中。能够进一步优化。编写工具类限定关心的属性。
分页查询结果封装类:
@Data public class PageResult { /** * 当前页码 */ private int pageNum; /** * 每页数量 */ private int pageSize; /** * 记录总数 */ private long totalSize; /** * 页码总数 */ private int totalPages; /** * 数据模型 */ private List<?> content; }
分页查询工具类:
public class PageUitls { /** * 将分页信息封装到统一的接口 * * @param pageInfo * @return */ public static PageResult getPageResult(PageInfo<?> pageInfo) { PageResult pageResult = new PageResult(); pageResult.setPageNum(pageInfo.getPageNum()); pageResult.setPageSize(pageInfo.getPageSize()); pageResult.setTotalSize(pageInfo.getTotal()); pageResult.setTotalPages(pageInfo.getPages()); pageResult.setContent(pageInfo.getList()); return pageResult; } }
接口方法:
/** * 查询全部用户 * * @return */ PageResult getAll(int pageNum, int pageSize);
接口实现类:
@Override public PageResult getAll(int pageNum, int pageSize) { //将参数传给这个方法就能够实现物理分页了,很是简单。 PageHelper.startPage(pageNum, pageSize); List<UserEntity> users = userMapper.getAll(); PageInfo<UserEntity> pageInfo = new PageInfo<>(users); return PageUitls.getPageResult(pageInfo); }
这样改写后,返回体就简洁许多了:
{ "pageNum": 1, "pageSize": 2, "totalSize": 3, "totalPages": 2, "content": [ { "id": 1, "userName": "Michael翔", "passWord": "123", "userSex": "MAN", "nickName": "ZX" }, { "id": 2, "userName": "HQH", "passWord": "123", "userSex": "WOMAN", "nickName": "QQ" } ] }
关于 MyBatis 的 IN 查询,也是试验了好久,才 OK 的。 StackOverflow 上就有相似的问题 How to use Annotations with iBatis (myBatis) for an IN query?。
为了测试 MyBatis IN 查询,咱们将以前的根据 ID 查询用户信息的接口进行修改,让它支持根据输入的 ID 列表查询多用户信息。
controller:
@ApiOperation(value = "查询指定 ID 的用户", notes = "根据用户 id 列表查询其信息") @ApiImplicitParam(name = "ids", value = "用户 id 列表", paramType = "path", required = true) @GetMapping(value = "/user/{ids}") public PageResult getUser(@RequestParam(name = "pageNum", defaultValue = "1", required = false) int pageNum, @RequestParam(name = "pageSize", defaultValue = "2", required = false) int pageSize, @PathVariable String ids) { List<String> idLst = Arrays.asList(ids.split(",")); PageResult user = userService.getUserById(pageNum, pageSize, idLst); return user; }
这里有个小注意点,@ApiImplicitParam
注解中的 paramType = "path"
记得修改成 path
,由于请求参数中包含路径变量了,不然渲染 URL 时,会出问题。
mapper 类:
@Select({ "<script>", "SELECT * ", "FROM users WHERE id IN", "<foreach item='id' index='index' collection='ids' open='(' separator=',' close=')'>", "#{id}", "</foreach>", "</script>" }) List<UserEntity> getUserById(@Param("ids") List<String> ids);
说明:
item
标识集合中每个元素进行迭代是的别名,不少教程中设置为 item
,我这里改成 id
也是 OK,并且也易于理解;index
指定一个名字,用于表示在迭代过程当中,每次迭代到的位置,从 0 开始;open
表示该语句以什么开始;separator
表示在每次进行迭代之间以什么符号做为分隔符;close
表示以什么结束;collection
属性,该属性是必须指定的,可是在不一样状况下,该属性的值是不同的;@Param
的设置比较关键,至关于给其修饰的参数指定一个别名:
@Param
,默认会和参数名同名,或者以注解传入的变量名为准。变量名将做为 @Select
中的可用参数,好比,我这里这样定义 @Param("ids2") List<String> ids
,那么,@Select
中可用参数名将是 ids2
,collection
也须定义为 ids2
,不然会报错:nested exception is org.apache.ibatis.binding.BindingException: Parameter 'list' not found. Available parameters are [ids2, param1]
;@Param
时,那么,此时 collection
须要定义为 list
,不然会报错:nested exception is org.apache.ibatis.binding.BindingException: Parameter 'list2' not found. Available parameters are [collection, list]
;上面的说明参考自 mybatis查询sql中in条件使用(foreach) ,没有找到官方文档支撑,待补充。
待后续补充
#{}
是通过预编译的,是安全的,而 ${}
是未通过预编译的,仅仅是取变量的值,是非安全的,存在 SQL 注入。${}
将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。${}
的状况,order by
、like
语句只能用 ${}
,用 #{}
会多个 ' '
致使 SQL 语句失效。此外动态拼接 SQL,模糊查询时也要用 ${}
。@ResultMap
比较方便;分页
PageResult
的优化,参考此文FAQ
欢迎关注我的公众号 「iPlayMichael」