MyBatis 入门
配置
-
添加依赖:
- 添加 Spring Web 依赖
- 添加 MyBatis Framework 依赖
- 添加 MySQL Driver 依赖
-
配置数据源:
在工程的
application.properties
文件里,数据源配置 格式 为:spring.datasource.url=jdbc:mysql://mysql数据库地址:数据库端口/数据库名称?serverTimezone=GMT%2B8 spring.datasource.username=用户名 spring.datasource.password=密码
serverTimezone=GMT%2B8
这个参数的作用在于设置数据库的时区为中国区域
MyBatis 映射对象
-
所有的 ORM 框架都需要有一个 Java 对象来映射数据库的表,并且是一一对应的,一般我们把这类对象称为
DO
对象,对象名称的规范是表名+DO
-
一般情况下企业中都会把这个
DO
对象存放在xxx.xxx.dataobject
包下,所以DO
就是 dataobject 的缩写。 -
DO 对象和普通的 POJO 类并无不同,唯一要注意的点是属性类型要和数据库类型进行匹配
JDBC 类型(数据库字段类型) Java 数据类型 VARCHAR String TEXT String INTEGER int DOUBLE double BIGINT long DATETIME Date
MyBatis DAO
- 数据层的服务称为
DAO
层,DAO层包含对数据库操作的接口和实现类。 - MyBatis 只需要定义接口就可以完成数据的增删改查
MyBatis 查询
-
在DAO接口类中添加接口方法:
@Mapper public interface UserDAO { //默认参数就是 SQL 语句 @Select("SELECT id,user_name as userName,pwd,nick_name as nickName,avatar,gmt_created as gmtCreated,gmt_modified as gmtModified FROM user") List<UserDO> findAll(); }
-
MyBatis 转化对象的时候是按照名称一一对应的,表字段和DO字段名称不一致的时候,数据就会映射失败。可以使用 SQL 别名来解决这个问题。
MyBatis 插入
-
@Insert("INSERT INTO user (user_name, pwd, nick_name,avatar,gmt_created,gmt_modified) VALUES(#{userName}, #{pwd}, #{nickName}, #{avatar},now(),now())") @Options(useGeneratedKeys = true, keyColumn = "id", keyProperty = "id") int insert(UserDO userDO);
-
Options 注解有三个参数
-
useGeneratedKeys
设置为 true,代表允许数据库使用自增主键
-
keyColumn
设置表的主键字段名称,一般都是
id
-
keyProperty
设置
DO
模型的主键字段,一般都是id
-
-
一般数据插入我们都使用 POST 请求,同时使用 JSON 方式传递数据,为了接收 JSON 参数,需要在参数中添加
@RequestBody
注解
MyBatis 修改
-
@Update("update user set nick_name=#{nickName},gmt_modified=now() where id=#{id}") int update(UserDO userDO);
MyBatis 删除
-
@Delete("delete from user where id=#{id}") int delete(@Param("id") long id);
# 中的 id 要和 @Param("id") 中的 id 一样
MyBatis 简单查询
-
根据用户名查询
@Select("select id,user_name as userName,pwd,nick_name as nickName,avatar,gmt_created as gmtCreated,gmt_modified as gmtModified from user where user_name=#{userName} limit 1") UserDO findByUserName(@Param("userName") String name);
MyBatis XML语句
MyBatis XML 配置
-
在
application.properties
文件中,添加配置mybatis.mapper-locations
,这个配置用于指定 MyBatis Mapper XML 文件路径,一般来说这个路径和 DAO 的包路径一致mybatis.mapper-locations=classpath:com/youkeda/comment/dao/*.xml //对应的文件路径 src/main/resources/com/youkeda/comment/dao/
MyBatis XML Mapper
-
一个 DAO 类对应一个 DAO.xml 文件,如
UserDAO.java
会对应UserDAO.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.youkeda.comment.dao.UserDAO"> <resultMap id="userResultMap" type="com.youkeda.comment.dataobject.UserDO"> <id column="id" property="id"/> <result column="user_name" property="userName"/> </resultMap> </mapper>
-
头信息:固定格式,复制即可
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-
mapper 根节点:
namespace
命名空间,一般就是这个 mapper 所对应的 DAO 接口的全称
<mapper namespace="com.youkeda.comment.dao.UserDAO"> </mapper>
-
resultMap:
-
resultMap 用于处理表和 DO 对象的属性映射,确保表中的每个字段都有属性可以匹配
<resultMap id="userResultMap" type="com.youkeda.comment.dataobject.UserDO"> <id column="id" property="id"/> <result column="user_name" property="userName"/> </resultMap>
-
映射子对象:resultMap 有一个子节点
association
可以聚合其他模型<resultMap id="commentModelResultMap" type="com.youkeda.comment.model.Comment"> <id column="id" property="id"/> <result column="ref_id" property="refId"/> <result column="content" property="content"/> <result column="parent_id" property="parentId"/> <result column="gmt_created" property="gmtCreated"/> <result column="gmt_modified" property="gmtModified"/> <association property="author" javaType="com.youkeda.comment.model.User"> <id property="id" column="user_id"/> <result column="user_name" property="userName"/> <result column="nick_name" property="nickName"/> <result column="avatar" property="avatar"/> </association> </resultMap>
-
resultMap属性
-
id
唯一标示,一般我们的命名规则是
xxxResultMap
,比如这里的userResultMap
-
type
对应的 DO 类完整路径,比如这里的
com.youkeda.comment.dataobject.UserDO
-
-
resultMap 子节点
-
id
设置数据库主键字段信息,column 属性对应的是表的字段名称,property 对应的是 DO 属性名称
-
result
设置数据库其他字段信息,column 属性对应的是表的字段名称,property 对应的是 DO 属性名称
-
-
MyBatis XML Insert 语句
-
在 mapper 节点下添加
insert
节点<insert id="add" parameterType="com.youkeda.comment.dataobject.UserDO" useGeneratedKeys="true" keyProperty="id"> INSERT INTO user (user_name, pwd, nick_name,avatar,gmt_created,gmt_modified) VALUES(#{userName}, #{pwd}, #{nickName}, #{avatar},now(),now()) </insert>
-
insert节点属性:
-
id
同 DAO 类的方法名,同一个 xml 内是要唯一的,比如这里的
id="add"
是和UserDAO.add
一致的 -
parameterType
这个用于传递参数类型,一般是和
UserDAO.add
方法的参数类型一致的, add 方法int add(UserDO userDO)
-
MyBatis XML Update/Delete 语句
-
update:
<update id="update" parameterType="com.youkeda.comment.dataobject.UserDO"> update user set nick_name=#{nickName},gmt_modified=now() where id=#{id} </update>
-
delete:
<delete id="delete"> delete from user where id=#{id} </delete>
-
delete
方法的参数是由@Param
注解组成,默认情况下,MyBaits 会把这类数据当成Map
数据来传递,而 MyBatis 默认的parameterType
类型就是Map
,所以可以省略不写
MyBatis XML Select 语句
-
基于 XML 模式的开发顺序
- 创建 DO 对象
- 创建 DAO 接口,配置
@Mapper
注解 - 创建 XML 文件,并完成
resultMap
配置 - 创建 DAO 接口方法
- 创建对应的 XML 语句
-
select 语句:
- 属性
resultMap
的值我们一般配置为该 XML 文件下的resultMap
节点的 id 值
<select id="findByUserName" resultMap="userResultMap"> select * from user where user_name=#{userName} limit 1 </select>
- 属性
-
当用LIKE子句进行模糊查询时,要用
concat('%',#{keyWord},'%')
MyBatis 动态SQL
MyBatis XML 条件语句
-
if+set:
<update id="update" parameterType="com.youkeda.comment.dataobject.UserDO"> update user <set> <if test="nickName != null"> nick_name=#{nickName}, </if> <if test="avatar != null"> avatar=#{avatar}, </if> gmt_modified=now() </set> where id=#{id} </update>
-
if+select+where:
<select id="search" resultMap="userResultMap"> select * from user <where> <if test="keyWord != null"> user_name like CONCAT('%',#{keyWord},'%') or nick_name like CONCAT('%',#{keyWord},'%') </if> <if test="time != null"> and gmt_created <![CDATA[ >= ]]> #{time} </if> </where> </select>
>=、<、<=、>、&
这类的表达式会导致 MyBatis 解析失败,所以我们需要使用<![CDATA[ key ]]>
来包围住。 -
URL 传递过来的参数都是字符串类型的,所以API的
LocalDateTime
类型参数要加上@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
注解把字符串参数转化为日期类型
MyBatis XML 循环语句
-
foreach 语法:
<select id="findByIds" resultMap="userResultMap"> select * from user <where> id in <foreach item="item" index="index" collection="ids" open="(" separator="," close=")"> #{item} </foreach> </where> </select>
collection
指定集合的上下文参数名称,比如这里的 ids,对应的是@Param("ids")
item
指定遍历中的每一个数据的变量,一般我们用 item 命名,使用item.userName
获取具体的值index
集合的索引值,从0开始separator
遍历每条记录并添加分隔符open
表示的是节点开始时自定义的分隔符close
表示的是节点结束时自定义的分隔符
MyBatis 进阶
MyBatis 分页插件
-
PageHelper
分页插件<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.13</version> </dependency>
-
用法示例:
public Paging<UserDO> getAll() { // 设置当前页数为1,以及每页3条记录 Page<UserDO> page = PageHelper.startPage(1, 3).doSelectPage(() -> userDAO.findAll()); return new Paging<>(page.getPageNum(), page.getPageSize(), page.getPages(), page.getTotal(), page.getResult()); }
-
返回类型
Page
对象是 MyBatis 封装的分页模型,常用方法有:getResult()
获取当前页面数据getPages()
获取总页数getTotal()
获取总记录数getPageNum()
获取当前页数
-
在企业开发中,通常都会额外封装一个通用的分页模型
Paging
用于处理返回值public class Paging<R> implements Serializable { private static final long serialVersionUID = 522660448543880825L; /** * 页数 */ private int pageNum; /** * 每页数量 */ private int pageSize = 15; /** * 总页数 */ private int totalPage; /** * 总记录数 */ private long totalCount; /** * 集合数据 */ private List<R> data; public Paging() { } public Paging(int pageNum, int pageSize, int totalPage, long totalCount, List<R> data) { this.pageNum = pageNum; this.pageSize = pageSize; this.totalPage = totalPage; this.totalCount = totalCount; this.data = data; } // 省略 getter、setter }
Druid 连接池
-
依赖:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.23</version> </dependency>
-
开启监控:在
application.properties
文件里增加配置项spring.datasource.druid.stat-view-servlet.enabled=true spring.datasource.druid.stat-view-servlet.url-pattern=/druid/* spring.datasource.druid.stat-view-servlet.login-username=自定义 spring.datasource.druid.stat-view-servlet.login-password=自定义 spring.datasource.druid.stat-view-servlet.allow= spring.datasource.druid.stat-view-servlet.deny=
-
本地访问地址是
http://localhost:8080/druid/login.html
评论服务开发
开发技巧
-
一般User类中会有密码属性
pwd
,而密码是不能泄漏的,所以我们需要在服务端输出的时候过滤掉这个属性,我们可以使用 jackson 的注解来忽略@JsonSerialize(using = NullSerializer.class)
-
对于时间我们一般都希望输出的格式是
yyyy-MM-dd HH:mm:ss
,我们依然可以借助 jackson 的注解来完成格式化输出@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
-
为了能够统一描述返回数据,一般需要新增一个
Result
公共模型,如下:public class Result<D> implements Serializable { @JsonProperty("isSuccess") //该注解用于自定义 JSON 输出的时候的字段名称 private boolean success = false; private String code; private String message; private D data; // 省略 getter、setter }
-
优化JSON:
在application.properties 文件中,新增如下配置
spring.jackson.deserialization.fail-on-unknown-properties=false spring.jackson.default-property-inclusion=non_null
上面2个配置分别的作用是:
- 允许序列化未知的字段,可以兼容 Java 模型和 JSON 数据不一致的情况
- 忽略 null 字段
-
在企业中,我们一般会结合 commons-lang3 库来处理字符串,依赖如下:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.10</version> </dependency>
可以使用
StringUtils.isEmpty(str)
来判断是否为空,注意包路径为org.apache.commons.lang3.StringUtils
-
加密:
一般来说,我们会使用 md5 算法进行加密,建议使用 commons-codec 库进行加密处理
依赖如下:
<dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.14</version> </dependency>
加密的例子:
// 密码加自定义盐值,确保密码安全 String saltPwd = pwd + "_ykd2050"; // 生成md5值,并转为大写字母 String md5Pwd = DigestUtils.md5Hex(saltPwd).toUpperCase();
-
通用开发模式:
设计领域模型->开发基础的DAO =>开发Service =>开发API
-
优化DO和Model互转:
在企业中,我们一般会把DO和Model的转换代码抽象成公共方法,放在 DO 对象里,一般命名为
toModel
public User toModel() { User user = new User(); user.setId(getId()); user.setUserName(getUserName()); user.setNickName(getNickName()); user.setAvatar(getAvatar()); user.setGmtCreated(getGmtCreated()); user.setGmtModified(getGmtModified()); return user; }
-
安全处理:
基于安全的考虑,用户自定义输入的文本需要禁止 HTML 标签,这样可以避免很多恶意的代码,可以使用
commons-text
这个库进行 HTML 转义,从而达到禁用 HTML 标签的作用,需要添加依赖<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.8</version> </dependency>
执行下面的代码可以完成转义
String body = StringEscapeUtils.escapeHtml4(content);
-
将comment集合变成树状结构,也就是说如果评论有回复,那么回复数据应该在这条评论的 children 字段里
//查询所有的评论记录包含回复的 List<Comment> comments = commentDAO.findByRefId(refId); //构建 map 结构 Map<Long, Comment> commentMap = new HashMap<>(); //初始化一个虚拟根节点,0 可以对应的是所有一级评论的父亲 commentMap.put(0L, new Comment()); //把所有的评论转换为 map 数据 comments.forEach(comment -> commentMap.put(comment.getId(), comment)); // 再次遍历评论数据 comments.forEach(comment -> { //得到父评论 Comment parent = commentMap.get(comment.getParentId()); if (parent != null) { // 初始化 children 变量 if (parent.getChildren() == null) { parent.setChildren(new ArrayList<>()); } // 在父评论里添加回复数据 parent.getChildren().add(comment); } }); // 得到所有的一级评论 List<Comment> data = commentMap.get(0L).getChildren();
Spring 安全
CORS
-
CORS
全称"跨域资源共享",它允许浏览器向跨源服务器发出 XML HttpRequest 请求,从而克服了 AJAX 只能同源使用的限制(浏览器会禁止读取跨域名的资源)。 -
SpringBoot 支持 CORS 请求:
@Configuration public class CorsConfig { @Bean public FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); // 设置你要允许的网站域名,如果全允许则设为 * config.addAllowedOrigin("*"); // 如果要限制 HEADER 或 METHOD 请自行更改 config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setMaxAge(Duration.ofDays(5)); source.registerCorsConfiguration("/**", config); FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); // 这个顺序很重要哦,为避免麻烦请设置在最前 bean.setOrder(0); return bean; } }