初识 spring springboot mybatis
手把手带你做一个简单的 mvc 应用。
一、代码结构
代码结果如上图,主要包括:
- controller: 控制层,作用是对外接收用户请求,对内调度对应 service。也就是常说的 api url。
- service: 服务层,作用是实现业务逻辑。比如用户商品购买需要生成订单然后扣库存,也就是至少两个数据层操作,先调订单 dao 增加订单,再调库存 dao 减库存。
- entity: 实体,作用是对内映射数据库对象,对外映射接口请求对象和接口返回对象。例如:一个用户信息他在数据库中的基本信息只有姓名、密码两个字段。另外用户还对应一个爱好表,用户跟爱好的关系是一个1对多关系。这样我们可以定义一个User的实体,有两个成员参数 name、password ,用这个实体可以映射 user 表的数据。再定一个 UserRequest 实体对象,除了 name、password 外还有一个 List 保存爱好,这样可以用 UserRequest 接收接口传过来的数据。接口返回对象雷同,当需要一个新的接口返回数据时,可以定义一个接口返回对象。
- dao: 数据层 ,作用是操作数据库,也就是常说的 CRUD。
二、controller
当完成需求分析后,一般设计都是从低往上的(entity -> dao -> service -> controller )。但这么写你可能感觉没啥意思,写了半天也看不到任何效果。所以这次咱们反过来先从 controller 下手。
当然如果公司没有接口平台,不能定义接口和 mock 接口的时候,也会先实现 controller,这样可以快速完成 api 文档和前后端分离开发。
业务背景:
做了一个服务器检查的功能,其中每一台服务器需要检查多个内容,并且每台服务器要检查的内容不同。这次要增加的功能就是添加要检查的条目。
1、创建 StepController ,如下图
package com.bjy.qa.controller.servercheck; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller @RequestMapping("/serverCheck/step") public class StepController { private final Log logger = LogFactory.getLog(getClass()); @RequestMapping("/add") @ResponseBody public Object add(@RequestBody String step) throws Exception { logger.info("add step:" + step); return "response"; } }
2、点击 运行
3、在 postman 中测试,配置如下图
三、service
下面开始实现业务层,service 分为两部分 接口、实现。
1、创建接口,添加类选择 Interface 接口名称是 IStepService
注意:大写 I 开头,说明这是一个接口
添加后如下图,由于没有定义实体对象,所以暂时当 String 传入
package com.bjy.qa.service.servercheck; public interface IStepService { /** * 新增一条数据 * @param step * @return */ Object add(String step); }
2、此时只有接口还没有实现,所以需要添加一个实现
package com.bjy.qa.service.servercheck.impl; import com.bjy.qa.service.servercheck.IStepService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Service; @Service public class StepService implements IStepService { private final Log logger = LogFactory.getLog(getClass()); @Override public Object add(String step) { logger.info("add step:" + step); return "成功执行 service"; } }
3、controller 中调用此 service
package com.bjy.qa.controller.servercheck; import com.bjy.qa.service.servercheck.IStepService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.annotation.Resource; @Controller @RequestMapping("/serverCheck/step") public class StepController { private final Log logger = LogFactory.getLog(getClass()); @Resource IStepService iStepService; @RequestMapping("/add") @ResponseBody public Object add(@RequestBody String step) throws Exception { logger.info("add step:" + step); return iStepService.add(step); } }
4、重新运行后再次调用,你能看到这次返回的是 service 中的内容。
四、entity
1、下面就要定义实体了,在 entity 中增加 step 实体,实体继承自 BaseEntity,BaseEntity 中包括了基本字段(id:自增id、createdAt:创建时间、updatedAt:更新时间、isDelete:逻辑删除标记)
package com.bjy.qa.entity.servercheck; import com.bjy.qa.entity.BaseEntity; public class Step extends BaseEntity<Step> { @Override public String toString() { return "Step{} " + super.toString(); } }
2、Controller、Service 中参数传递改为实体
3、再次运行,你能看到 id 和 createdAt 两个参数被 BaseEntity 收到了
4、定义业务字段,数据库中 step 表如下图
除了 id、createdAt、updatedAt、isDelete 外还有 server_check_id(bigint)、desc(varchar)、key(varchar)、expected(text)、runTag(varchr)。其中 bigint 对应 java 的 Long、varchar 和 text 对应 java 的 String。
在 实体(step.java)中定义字段并 生成 set、get、toString 方法,如下图
package com.bjy.qa.entity.servercheck; import com.bjy.qa.entity.BaseEntity; public class Step extends BaseEntity<Step> { private Long serverCheckId; // 外健,对应 serverCheck id private String desc; // 描述 private String key; // 需要检查的 key private String expected; // 预期结果 private String runTag; // 运行 tag public Long getServerCheckId() { return serverCheckId; } public void setServerCheckId(Long serverCheckId) { this.serverCheckId = serverCheckId; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getExpected() { return expected; } public void setExpected(String expected) { this.expected = expected; } public String getRunTag() { return runTag; } public void setRunTag(String runTag) { this.runTag = runTag; } @Override public String toString() { return "Step{" + "serverCheckId=" + serverCheckId + ", desc='" + desc + '\'' + ", key='" + key + '\'' + ", expected='" + expected + '\'' + ", runTag='" + runTag + '\'' + "} " + super.toString(); } }
5、修改提交 json 后重新运行
{ "desc": "描述", "key": "key" }
五、dao
最后就剩下操作数据库了。
1、添加 StepDao,如下图。因为集成了 mybatis、mybatisplus 添加完 dao 后就完成了基本的 CURD
package com.bjy.qa.dao.servercheck; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.bjy.qa.entity.servercheck.Step; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; @Mapper @Repository public interface StepDao extends BaseMapper<Step> { }
2、修改 StepService。例子中写的是 add,那就完成数据库插入。如下图
package com.bjy.qa.service.servercheck.impl; import com.bjy.qa.dao.servercheck.StepDao; import com.bjy.qa.entity.servercheck.Step; import com.bjy.qa.service.servercheck.IStepService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class StepService implements IStepService { private final Log logger = LogFactory.getLog(getClass()); @Resource StepDao stepDao; @Override public Object add(Step step) { logger.info("add step:" + step); stepDao.insert(step); return "成功执行 service"; } }
3、重新运行,报错了,原因是 desc、key 这两个字段是 mysql 关键字,错误如下图
4、修改 Setp Entity,加入 TableField 指定字段名
@TableField("`desc`") private String desc; // 描述 @TableField("`key`") private String key; // 需要检查的 key
5、重新运行,成功
六、返回从字符串改成包装对象
上边 add 接口只是返回一个字符串,每次成功和失败都要包装 code、msg 等信息。所以可以改成直接返回对象
1、修改 StepService 将返回字符串改为返回 dao 结果对象。
2、修改 StepController 将 service 返回的内容用 Response.success 包装一下
3、再次运行,会自动添加 code、msg
4、除了成功还有失败等,参看
:)-- 以上就是基本的 mvc 模型 -- (:
下面还会讲一些其他技巧
七、字段校验
1、字段校验
一般情况下,接口请求参数需要有一些必填、长度、类型等校验,该如何实现呢?
1、修改 Setp Entity,加入 NotEmpty 非空校验
@NotEmpty(message = "desc: 描述字段必填")
2、修改 StepController
3、去掉 desc 参数后,重新运行
4、除了 NotEmpty 还有如下,参见 https://blog.csdn.net/leaf__yang/article/details/126037361
- @Null 元素必须为null
- @NotNull 元素不能null
- @AssertTrue 元素必须为true
- @AssertFalse 元素必须是false
- @Min(value) 元素必须是一个数字,其值必须大于等于指定的最小值
- @Max(value) 元素必须是一个数字,其值必须小于等于指定的最大值
- @DecimalMin(value) 元素必须是一个数字,其值必须大于等于指定的最小值
- @DecimalMax(value) 元素必须是一个数字,其值必须小于等于指定的最大值
- @Size(max,min) 元素的大小必须在指定的范围内
- @Digits(integer,fraction) 元素必须是一个数字,其值必须在可接受的范围内
- @Past 元素必须是一个过去的日期
- @Future 元素必须是一个将来的日期
- @Pattern(value) 元素必须符合指定的正则表达式
- @Email 元素必须是电子邮箱地址
- @Length 字符串的大小必须在指定的范围内
- @NotEmpty 字符串必须非空
- @Range 元素必须在合理的范围内
2、分组校验 groups
当两个接口,一个接口要校验 desc 不能为空,另一个接口要校验 key 不能为空,那怎么办呢?可以按照分组校验
1、增加后如下图
package com.bjy.qa.entity.servercheck; import com.baomidou.mybatisplus.annotation.TableField; import com.bjy.qa.entity.BaseEntity; import javax.validation.constraints.NotEmpty; public class Step extends BaseEntity<Step> { private Long serverCheckId; // 外健,对应 serverCheck id @NotEmpty(groups = Step.Step1Group.class, message = "desc: 描述字段必填") @TableField("`desc`") private String desc; // 描述 @NotEmpty(groups = Step.Step2Group.class, message = "key: key字段必填") @TableField("`key`") private String key; // 需要检查的 key private String expected; // 预期结果 private String runTag; // 运行 tag public Long getServerCheckId() { return serverCheckId; } public void setServerCheckId(Long serverCheckId) { this.serverCheckId = serverCheckId; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getExpected() { return expected; } public void setExpected(String expected) { this.expected = expected; } public String getRunTag() { return runTag; } public void setRunTag(String runTag) { this.runTag = runTag; } @Override public String toString() { return "Step{" + "serverCheckId=" + serverCheckId + ", desc='" + desc + '\'' + ", key='" + key + '\'' + ", expected='" + expected + '\'' + ", runTag='" + runTag + '\'' + "} " + super.toString(); } public static interface Step1Group { } public static interface Step2Group { } }
2、修改 StepController
package com.bjy.qa.controller.servercheck; import com.bjy.qa.entity.servercheck.Step; import com.bjy.qa.entity.user.User; import com.bjy.qa.service.servercheck.IStepService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Controller; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.annotation.Resource; import javax.validation.Valid; @Controller @RequestMapping("/serverCheck/step") @Validated public class StepController { private final Log logger = LogFactory.getLog(getClass()); @Resource IStepService iStepService; @RequestMapping("/add") @ResponseBody @Validated(Step.Step1Group.class) public Object add(@RequestBody @Valid Step step) throws Exception { logger.info("add step:" + step); return iStepService.add(step); } @RequestMapping("/add1") @ResponseBody @Validated(Step.Step2Group.class) public Object add1(@RequestBody @Valid Step step) throws Exception { logger.info("add step:" + step); return iStepService.add(step); } }
3、重新运行
调用接口 add1 接口提示 desc 必填
调用接口 add1 接口时提示 key 必填
八、高阶sql
上例完成的是基础的 CRUD 操作,那遇到 关联查询、批量插入 该如何处理呢。
1、关联查询
1、在对应 dao 中增加如下代码,此例有 3 个表(user、company、project),其中 user 是主表,company、project 表左连接 user 表
@Select("SELECT " + " `user`.*, " + " company.NAME AS company_name, " + " project.NAME AS project_name " + "FROM " + " `user` " + " LEFT JOIN company ON `user`.company_id = company.id " + " LEFT JOIN project ON `user`.project_id = project.id " + "WHERE " + " `user`.id = #{id}") public User selectById(Long id);
2、批量插入
下例中传入的是一个 list warehouseDetails,使用 foreach 语句批量插入数据到 server_check_step_log 表中
@Insert("<script>" + "INSERT INTO server_check_step_log (project_id, server_check_task_id, server_check_step_id, status, actual, error_msg, is_delete, updated_at, created_at) VALUES" + "<foreach collection='list' item='detail' separator=','> " + "(#{detail.projectId}, #{detail.serverCheckTaskId}, #{detail.serverCheckStepId}, #{detail.status}, #{detail.actual}, #{detail.errorMsg}, #{detail.isDelete}, #{detail.updatedAt}, #{detail.createdAt})" + "</foreach> " + "</script>") boolean insertBatch(@Param("list") List<ServerCheckStepLog> warehouseDetails);
3、当然也支持 mybatis XML 文件的方式
在对应 dao 中增加一个接口,同目录下创建 xml 文件。
注意:dao 中的方法名,要跟 xml 文件中的 id 相同。
/** * 查询用户完整信息,包括公司及项目列表(需要关联多个表一般不要使用) * @param id 用户id * @return 用户数据 */ public User selectWholeInfoById(@Param("userId") Long id);
<?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.bjy.qa.dao.user.UserDao"> <resultMap id="AllColumnMap" type="com.bjy.qa.entity.user.User"> <result column="id" property="id"/> <result column="updated_at" property="updatedAt"/> <result column="created_at" property="createdAt"/> <result column="is_delete" property="delete"/> <result column="name" property="name"/> <result column="avatar" property="avatar"/> <result column="account" property="account"/> <result column="company_id" property="companyId"/> <result column="project_id" property="projectId"/> <collection property="companyList" ofType="com.bjy.qa.entity.manage.Company"> <result column="c.id" property="id" /> <result column="c.name" property="name" /> <collection property="projectList" ofType="com.bjy.qa.entity.manage.Project"> <result column="p.id" property="id" /> <result column="p.name" property="name" /> </collection> </collection> </resultMap> <select id="selectWholeInfoById" resultMap="AllColumnMap"> SELECT u.*, c.id AS `c.id`, c.`name` AS `c.name`, p.id AS `p.id`, p.`name` AS `p.name` FROM `user` AS u LEFT JOIN ( SELECT * FROM user_company_project WHERE is_delete = FALSE ) AS ucp ON ucp.user_id = u.id LEFT JOIN ( SELECT * FROM company WHERE is_delete = FALSE ) AS c ON c.id = ucp.company_id LEFT JOIN ( SELECT * FROM project WHERE is_delete = FALSE ) AS p ON p.id = ucp.project_id WHERE u.id = #{userId,jdbcType=NUMERIC} AND u.is_delete = FALSE </select> </mapper>