目录
动态SQL
动态 SQL 是 MyBatis 的一个强大特性,它允许在 SQL 语句中根据条件动态拼接不同的 SQL 片段,从而实现灵活的 SQL 查询和操作。通过动态 SQL,我们可以根据不同的条件来构建不同的 SQL 查询语句,这在实际开发中非常常见和有用。
动态 SQL 的一些常见应用场景包括:
- 条件查询:根据不同的条件组合动态生成 WHERE 子句,从而实现灵活的条件查询。
- 动态排序:根据用户的选择动态生成 ORDER BY 子句,实现按不同字段排序的功能。
- 动态更新:根据不同的条件动态生成 SET 子句,从而实现只更新某些字段的功能。
- 动态插入:根据不同的条件动态生成插入语句,实现只插入部分字段的功能。
- 动态删除:根据不同的条件动态生成删除语句,实现灵活的删除操作。
MyBatis 提供了丰富的动态 SQL 功能,包括 <if>、<choose>、<when>、<otherwise>、<trim>、<where>、<set>、<foreach> 等标签,可以根据具体的需求灵活组合使用,实现各种复杂的动态 SQL 拼接。
可以参考官方文档:mybatis – MyBatis 3 | Introduction
标签<`if`>
注册分为两种字段:必填字段和非必填字段,如果在添加用户的时候有不确定的字段传入,程序应该如何实现呢?这个时候就需要使用动态标签来判断了。
<insert id="insert2" useGeneratedKeys="true" keyProperty="id">
insert into
userinfo
(username,password,age,
<if test="gender!=null">
gender,
</if>
phone)
values
(#{username},#{password},#{age},
<if test="gender!=null">
#{gender},
</if>
#{phone})
</insert>
首先是都有的情况下:
接下来:
如果传入的参数对象中的 gender 属性不为 null,那么生成的 SQL 语句就会包含 gender 字段名。
- <if test="gender!=null">:这是一个条件判断,用于检查传入的参数对象中的 gender 属性是否为 null。如果 gender 不为 null,就会执行 <if> 标签内的代码块。
- gender,:这是 SQL 语句的一部分,表示在 SQL 插入语句中插入 gender 字段名。注意这里的逗号是为了保证 SQL 语法的正确性,在最终生成的 SQL 语句中,如果条件满足,就会插入这个字段名。
- </if>:表示条件判断的结束标签,如果 <if> 标签内的条件成立,就会执行其中的内容直到遇到 </if> 结束。
现在把我们的代码完善一下:
<insert id="insert2" useGeneratedKeys="true" keyProperty="id">
insert into
userinfo
(
<if test="username!=null">
username,
</if>
<if test="password!=null">
password,
</if>
<if test="age!=null">
age,
</if>
<if test="gender!=null">
gender,
</if>
<if test="phone!=null">
phone,
</if>
)
values
(
<if test="username!=null">
#{username},
</if>
<if test="password!=null">
#{password},
</if>
<if test="age!=null">
#{age},
</if>
<if test="gender!=null">
#{gender},
</if>
<if test="phone!=null">
#{phone},
</if>
)
</insert>
调整格式之后:
<insert id="insert2" useGeneratedKeys="true" keyProperty="id">
insert into
userinfo
(
<if test="username!=null">
username
</if>
<if test="password!=null">
,password
</if>
<if test="age!=null">
,age
</if>
<if test="gender!=null">
,gender
</if>
<if test="phone!=null">
,phone
</if>
)
values
(
<if test="username!=null"> #{username}</if>
<if test="password!=null"> ,#{password}</if>
<if test="age!=null"> ,#{age}</if>
<if test="gender!=null"> ,#{gender}</if>
<if test="phone!=null"> ,#{phone}</if>
)
</insert>
那么我们现在再把别的字段置为null:
哎呀我的天!又出错了!又要去手动调整逗号……好麻烦!
这个时候就要考虑别的方法了。
使用 <trim> 标签可以确保在生成的 SQL 语句中正确处理字段和值之间的逗号,并且不会在 SQL 语句末尾添加多余的逗号,从而避免了 SQL 语法错误。
<`trim`> 标签
在先前的用户插入功能中,尽管只有一个 gender 字段是可选填的,但如果存在多个字段,通常可以考虑使用标签与标签结合的方式,以动态生成的方式处理多个字段。
这些标签具有以下属性:
- prefix:用于表示整个语句块,其值将作为前缀。
- suffix:用于表示整个语句块,其值将作为后缀。
- prefixOverrides:表示整个语句块需要去除的前缀。
- suffixOverrides:表示整个语句块需要去除的后缀。
<insert id="insert2" useGeneratedKeys="true" keyProperty="id">
insert into
userinfo
<trim prefixOverrides="," prefix="(" suffix=")" suffixOverrides=",">
<if test="username!=null">
username,
</if>
<if test="password!=null">
password,
</if>
<if test="age!=null">
age,
</if>
<if test="gender!=null">
gender,
</if>
<if test="phone!=null">
phone,
</if>
</trim>
values
<trim prefixOverrides="," prefix="(" suffix=")" suffixOverrides=",">
<if test="username!=null"> #{username},</if>
<if test="password!=null"> #{password},</if>
<if test="age!=null">#{age},</if>
<if test="gender!=null">#{gender},</if>
<if test="phone!=null">#{phone},</if>
</trim>
</insert>
根据错误信息,可以看出问题出在数据库表 userinfo 的字段 username 上,它没有设置默认值,因此在执行插入操作时,即使该字段在 Java 对象中为 null,数据库仍然要求提供一个值。
解决这个问题的方法有两种:
- 给 userinfo 表的 username 字段设置一个默认值,可以是空字符串或者其他适当的默认值。
- 修改插入语句,在 username 字段的 if 条件判断中添加一个判断条件,即使 username 为 null 也能插入成功。具体来说,可以修改插入语句中的 <trim> 标签,使得在 username 为 null 时,插入一个默认的值或者 NULL。
<trim>使用注解式(不推荐)
@Insert("<script>" +
"INSERT INTO userinfo " +
"<trim prefix='(' suffix=')' suffixOverrides=','>" +
"<if test='username!=null'>username,</if>" +
"<if test='password!=null'>password,</if>" +
"<if test='age!=null'>age,</if>" +
"<if test='gender!=null'>gender,</if>" +
"<if test='phone!=null'>phone,</if>" +
"</trim>" +
"VALUES " +
"<trim prefix='(' suffix=')' suffixOverrides=','>" +
"<if test='username!=null'>#{username},</if>" +
"<if test='password!=null'>#{password},</if>" +
"<if test='age!=null'>#{age},</if>" +
"<if test='gender!=null'>#{gender},</if>" +
"<if test='phone!=null'>#{phone}</if>" +
"</trim>"+
"</script>")
Integer insertUserByCondition(UserInfo userInfo);
在以上的 SQL 动态解析过程中,对第一个部分的处理如下:
- 根据 prefix 配置,将开始部分添加上 '('。
- 根据 suffix 配置,将结束部分添加上 ')'。
- 多个组织的语句都以 ',' 结尾,在最后拼接好的字符串也会以 ',' 结尾,然后根据 suffixOverrides 配置去掉最后一个 ','。
注意 <if test="username !=null"> 中的 username 是传入对象的属性。
<`where`>标签
看如下场景, 系统会根据我们的筛选条件,动态组装where 条件:
这种如何实现呢? 接下来我们看代码:
需求: 传入的用户对象,根据属性做where条件查询,用户对象中属性不为 null 的,都为查询条件.。如 username 为 "a",则查询条件为 where username="a"。
<select id="queryUserByWhere" resultType="com.example.mybatisstudy.model.UserInfo">
select * from userinfo
where
<if test="userName!=null">
username= #{userName}
</if>
<if test="age!=null">
and age=#{age}
</if>
</select>
<select id="queryUserByWhere" resultType="com.example.mybatisstudy.model.UserInfo">
select * from userinfo
where
<if test="param1 != null">
username = #{param1}
</if>
<if test="param2 != null">
and age = #{param2}
</if>
</select>
现在不给userName传值:
报错了:
那么我们如果把SQL里面age前面的and调到userName后面,一会儿不给age传值的时候也会报错。所以我们考虑别的方法:
<select id="queryUserByWhere" resultType="com.example.mybatisstudy.model.UserInfo">
select * from userinfo
<where>
<if test="userName!=null">
username= #{userName}
</if>
<if test="age!=null">
and age=#{age}
</if>
</where>
</select>
<select id="queryUserByWhere" resultType="com.example.mybatisstudy.model.UserInfo">
select * from userinfo
<where>
<if test="param1 != null">
username = #{param1}
</if>
<if test="param2 != null">
and age = #{param2}
</if>
</where>
</select>
<where> 标签在 MyBatis 中用于动态生成 SQL 查询语句的 WHERE 子句。它的主要作用是根据条件动态添加 WHERE 子句的开头和条件之间的连接词(如 AND 或 OR),以避免不必要的逻辑错误。
通常情况下,当使用多个条件进行动态查询时,我们可能需要根据不同条件的存在与否来拼接 WHERE 子句。<where> 标签的作用就是为你处理这种情况。
当使用 <where> 标签时,MyBatis 会自动去掉生成的 SQL 语句中不必要的 AND 或 OR 连接词,并确保 WHERE 子句的正确性。
如果查询条件不存在,则 <where> 标签会自动删除不必要的 WHERE 子句,以保持生成的 SQL 语句的正确性。
或者使用注解方式
@Select("<script>" +
"select id, username, age, gender, phone, delete_flag, " +
"create_time, update_time" +
" from userinfo" +
" <where>" +
" <if test='age != null'> and age = #{age} </if>" +
" <if test='gender != null'> and gender = #{gender} </if>" +
" <if test='deleteFlag != null'> and delete_flag = #{deleteFlag} </if>" +
" </where>" +
"</script>")
List<UserInfo> queryByCondition(UserInfo userInfo);
<`set`>标签
需求: 根据传入的用户对象属性来更新用户数据,可以使用标签来指定动态内容。
<set> 标签是 MyBatis 中用于动态生成 SQL 更新语句的标签之一。它主要用于在动态更新语句中构建 SET 子句。
在更新数据时,通常需要根据传入的参数来动态确定需要更新的字段和对应的值。<set> 标签的作用就是帮助我们动态构建 SET 子句,根据传入参数的存在与否来确定需要更新的字段。
具体来说,<set> 标签会自动去掉 SET 子句中不必要的逗号,并确保只有在至少一个字段需要更新时才会生成正确的 SET 子句。
我们一旦用到<if>标签就需要考虑标点符号,此时虽然也可以使用<trim>标签,但是也可以使用<set>标签:
<update id="update2">
update userinfo
<set>
<if test="username!=null">
username = #{username},
</if>
<if test="password!=null">
password = #{password},
</if>
<if test="age!=null">
age = #{age}
</if>
</set>
where id = #{id}
</update>
<`foreach`>标签
<foreach> 标签是 MyBatis 中用于遍历集合并生成相应 SQL 语句的标签之一。它主要用于在 SQL 查询中动态生成 IN 子句,将集合中的元素作为查询条件之一。
该标签具有如下属性:
- collection:绑定方法参数中的集合,如 List、Set、Map 或数组对象。
- item:遍历时的每一个对象。
- open:语句块开头的字符串。
- close:语句块结束的字符串。
- separator:每次遍历之间间隔的字符串。
<foreach> 标签的作用是将集合中的元素遍历,并根据遍历的结果动态生成相应的 SQL 语句。它可以将集合中的元素作为查询条件,或者作为插入、更新等操作的值。
我们更详细地描述 <foreach> 标签用途和工作原理:
- 遍历集合:<foreach> 标签用于遍历传入的集合对象,如 List、Set、Map 或数组。
- 动态生成 SQL 语句:通过遍历集合中的元素,<foreach> 标签可以根据遍历的结果动态生成相应的 SQL 语句。
- 作为查询条件:在 SQL 查询中,<foreach> 标签通常用于生成 IN 子句,其中集合中的元素被作为查询条件的一部分。例如,在 WHERE 子句中使用 IN 子句来查询符合条件的数据。
- 作为操作的值:除了作为查询条件外,<foreach> 标签也可以用于插入、更新等操作中,将集合中的元素作为要插入、更新的值。
- 语句块的开头和结束:<foreach> 标签还可以指定开头和结尾的字符串,以及每次遍历之间的分隔符。这使得在 SQL 语句中动态生成的部分更具灵活性。
总的来说,<foreach> 标签允许在 SQL 查询和操作中使用集合数据,并根据集合的内容动态生成相应的 SQL 语句,从而实现更加灵活和动态的数据库操作。
<delete id="batchDelete">
delete from userinfo where id in
<foreach collection="ids" separator="," open="(" close=")" item="id">
#{id}
</foreach>
</delete>
这里如果你没有改掉之前阿里云的那个依赖,此处运行也不会成功,Mybatis会自动创建一个参数。
你需要这样写:
<delete id="batchDelete">
delete from userinfo where id in
<foreach collection="list" separator="," open="(" close=")" item="id">
#{id}
</foreach>
</delete>
或者使用注解方式:
@Delete("<script>"+
"delete from userinfo where id in"+
"<foreach collection='list' separator=','open='(' close=')' item='id'>"+
"#{id}"+
"</foreach>"+
"</script>")
Integer batchDelete(List<Integer> ids);
报错了,这也是注解不好的地方,报错没有报错提示,需要你一点点自查,所以初学者真的不建议使用注解的方式实现动态SQL。
最后的异常 org.apache.ibatis.builder.BuilderException 表示 MyBatis 在解析 XML 映射文件时出现了问题,具体是由于 <foreach> 元素没有正确闭合导致的解析错误。
异常信息中提到了 元素类型 "foreach" 必须后跟属性规范 ">" 或 "/>",这意味着 MyBatis 在解析 XML 时遇到了 <foreach> 元素,并期望在该元素中有正确的属性规范。在 XML 中,每个元素必须以 ">" 或 "/>" 结束,但是在这个异常中,似乎 <foreach> 元素缺少了正确的属性规范。
这个异常通常是由于在 XML 中写错了语法导致的,可能是由于 <foreach> 元素的属性没有正确指定,或者是由于 XML 标签未正确闭合。
<`include`>标签与<sql>标签
在 XML 映射文件中,经常会出现重复的 SQL 片段,这样会导致代码冗余。为了解决这个问题,我们可以使用 <sql> 标签将重复的 SQL 片段提取出来,然后通过 <include> 标签进行引用。
- <sql> 标签:用于定义可重用的 SQL 片段。你可以在其中定义任何 SQL 语句片段,然后通过 id 属性为其命名以供引用。
- <include> 标签:用于引用已经定义的 SQL 片段。通过 refid 属性指定要包含的 SQL 片段的 id。
<sql id="cols">
id, username,password,gender,age,phone,
</sql>
<select id="queryUserList" resultType="com.example.mybatisstudy.model.UserInfo">
select
<include refid = "cols"></include>
delete_flag as deleteFlag,
create_time as createTime,
update_time as updateTime
from userinfo
</select>
<select id="queryUserList2" resultMap="BaseMap">
select
<include refid = "cols"></include>
delete_flag,
create_time,
update_time
from userinfo
</select>
案例练习
基于以上知识的学习,我们就可以做⼀些简单的项目了!
我们之前已经写过“表白墙”和“图书管理系统”的程序了,现在的工作就是根据新学到的知识把它们更新完善~~
表白墙
在前面的案例中,我们创建了一个简单的表白墙应用程序,但是存在一个问题:一旦服务器重启,内存中的数据就会丢失。为了确保数据不丢失,我们需要将数据存储到数据库中。接下来,我们将使用 MyBatis 来实现对数据的操作。
首先,我们需要创建一个数据库表来存储我们的数据。然后,我们将使用 MyBatis 来定义 SQL 语句,并通过 MyBatis 提供的接口来执行这些 SQL 语句。这样,我们就可以将数据存储在数据库中,从而确保数据在服务器重启后不会丢失。
在实现过程中,我们需要使用 MyBatis 的注解或 XML 配置来定义 SQL 语句,并创建相应的 Java 接口来执行这些 SQL 语句。通过这种方式,我们可以方便地与数据库交互,实现数据的持久化存储,确保数据的安全性和可靠性。
借助 MyBatis,我们可以很容易地将应用程序中的数据存储到数据库中,从而避免数据丢失的问题,提高数据的持久性和可靠性。
数据准备
我回忆了一下,以前已经创建了一个表白墙的数据库了,我们直接用它就好:
DROP TABLE IF EXISTS message_info;
CREATE TABLE `message_info` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`from` VARCHAR(127) NOT NULL,
`to` VARCHAR(127) NOT NULL,
`message` VARCHAR(256) NOT NULL,
`delete_flag` TINYINT(4) DEFAULT 0 COMMENT '0-正常, 1-删除',
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now() ON UPDATE now(),
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;
我们对这个表的 update_time 列使用了 ON UPDATE now(),这意味着每当该行发生更新操作时,MySQL 将自动更新该列的值为当前时间。
now() 可以替换成其他获取时间的标识符,比如:CURRENT_TIMESTAMP()、LOCALTIME()等。
针对 MySQL 的版本不同,对于自动更新的行为有一些限制和规则:
对于 MySQL 版本小于 5.6.5:
- 只有 TIMESTAMP 类型支持自动更新,DATETIME 类型不支持。
- 一张表只能有一列设置了自动更新。
- 不允许同时存在两个列——其中一个设置了 DEFAULT CURRENT_TIMESTAMP 默认值,另一个设置了 ON UPDATE CURRENT_TIMESTAMP 自动更新。这是因为 DEFAULT CURRENT_TIMESTAMP 和 ON UPDATE CURRENT_TIMESTAMP 是两种对时间戳列进行操作的方式,它们都会对列的值进行自动处理,但是在同一个表中,MySQL 不支持两种不同的自动处理方式同时存在于不同的列上。
DEFAULT CURRENT_TIMESTAMP:当一行数据被插入时,如果该列没有指定值,则将使用当前时间作为默认值。
ON UPDATE CURRENT_TIMESTAMP:当一行数据被更新时,该列的值会自动更新为当前时间。
因此,在创建表时,我们只能选择其中一种方式来处理时间戳列,而不能同时在同一个表中使用这两种方式。
对于 MySQL 版本大于等于 5.6.5:
- TIMESTAMP 和 DATETIME 类型都支持自动更新,并且可以有多列设置自动更新。
在这个表中,我们使用了 DATETIME 类型,并为其设置了 ON UPDATE now(),这意味着我们必须确保 MySQL 的版本在 5.6.5 或更高,否则自动更新的功能将不会起作用。
引入MyBatis 和 MySQL驱动依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
或者使用插件——EditStarters来引入依赖 :
配置MySQL账号密码
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/message_wall?characterEncoding=utf8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration: # 配置打印 MyBatis⽇志
map-underscore-to-camel-case: true #配置驼峰⾃动转换
编写后端代码
先稍微调整一下三层架构:
MessageInfo:
package com.example.UserControl.model;
import lombok.Data;
import javax.annotation.sql.DataSourceDefinition;
import lombok.Data;
import java.util.Date;
@Data
public class MessageInfo {
private Integer id;
private String from;
private String to;
private String message;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
}
再添加两个包,mapper和service:
MassageMapper:
package com.example.UserControl.mapper;
import com.example.UserControl.model.MessageInfo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface MessageMapper {
/**
* 插入留言信息
* @return
*/
@Insert("insert into message_info (`from`, `to`, `message`) values (#{from}, #{to}, #{message})")
Integer insertMessage(MessageInfo messageInfo);
/**
* 查询留言信息
*/
@Select("select * from message_info where delete_flag <> 1")
List<MessageInfo> queryMessageList();
}
MessageService:
package com.example.UserControl.service;
import com.example.UserControl.mapper.MessageMapper;
import com.example.UserControl.model.MessageInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MessageService {
@Autowired
private MessageMapper messageMapper;
public Integer insertMessage(MessageInfo messageInfo) {
return messageMapper.insertMessage(messageInfo);
}
public List<MessageInfo> queryList() {
return messageMapper.queryMessageList();
}
}
MessageController:
package com.example.demo.controller;
import com.example.demo.model.MessageInfo;
import com.example.demo.service.MessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@RequestMapping("/message")
@RestController
public class MessageController {
@Autowired
private MessageService messageService;
private List<MessageInfo> messageInfos = new ArrayList<>();
@RequestMapping("/publish")
public boolean publishMessage(MessageInfo messageInfo){
if (!StringUtils.hasLength(messageInfo.getFrom())
|| !StringUtils.hasLength(messageInfo.getTo())
|| !StringUtils.hasLength(messageInfo.getMessage())){
return false;
}
//暂时存放在内存中
// messageInfos.add(messageInfo);
//把数据放在mysql当中
messageService.insertMessage(messageInfo);
return true;
}
@RequestMapping("/getList")
public List<MessageInfo> getList(){
for (MessageInfo messageInfo: messageInfos){
}
//return messageInfos;
return messageService.queryList();
}
}
后端测试
成功!
但是现在数据库里面没有数据,我们先去插入几条:
INSERT INTO message_info (`from`, `to`, `message`, `delete_flag`) VALUES
('user1', 'user2', 'Hello, how are you?', 0),
('user2', 'user1', 'Hi, I am good. Thanks!', 0),
('user1', 'user3', 'Hey there!', 0);
select * from message_info;
成功拿到数据!
现在测试发布的接口。
因为参数比较多,我们选择postman来做测试:
接口测试完成,我们现在可以在浏览器运行了。
前端测试
成功!
图书管理系统
前面我们在图书管理系统中已经完成了用户登录和图书列表功能,不过我们的数据都是模拟的。现在,我们将继续完善系统,添加更多功能,使其更加实用和完整。
我们先来回顾一下:
所以我们大致要修改的地方有:
1、应该从数据库拿到用户名和密码,验证用户输入是否正确,然后进入图书列表页。所以我们得有一张用户信息表。
2、图书列表页需要有一张图书列表的信息表,从而后续可以完成“增删查改”的操作。
数据库表设计
构思设计
数据库表是应用程序开发中的一个重要环节,数据库表的设计往往会决定我们的应用需求是否能顺利实现,甚至决定我们的实现方式。如何设计表以及这些表有哪些字段,这些表存在哪些关系,也是非常重要的。
数据库表设计是依据业务需求来设计的。如何设计出优秀的数据库表,与经验有很大关系。数据库表通常分两种:实体表和关系表。
分析我们的需求,图书管理系统相对来说比较简单,只有两个实体:用户和图书,并且用户和图书之间没有关联关系。
实体表和关系表是数据库中常见的两种表类型,它们在数据库设计中扮演着不同的角色。
-
实体表(Entity Table): 实体表也称为基本表或主表,用于存储系统中的基本实体或主要业务对象的信息。每个实体表代表一个实体,表的每一行对应一个具体的实体实例。实体表通常具有简单直观的结构,用于存储数据的基本属性。例如,在一个图书管理系统中,用户表和图书表就是典型的实体表。用户表存储用户的基本信息,如用户ID、用户名、密码等;而图书表存储图书的基本信息,如图书ID、图书名称、作者等。实体表之间通常是独立的,没有直接的关联关系。
-
关系表(Relationship Table): 关系表也称为连接表或中间表,用于表示实体之间的关系或连接。关系表存储了不同实体之间的关联信息,通过多对多关系将多个实体连接起来。关系表通常由两个或多个外键组成,这些外键分别指向连接的实体表的主键。例如,在一个图书管理系统中,如果用户可以借阅多本图书,就需要一个关系表来表示用户和图书之间的借阅关系。该关系表可能包含用户ID和图书ID作为外键,表示哪些用户借阅了哪些图书。通过关系表,可以实现复杂的多对多关系。
表的具体字段设计,也与需求有关。对于用户表,常见的字段可能包括用户ID、用户名、密码、邮箱等;而对于图书表,则可能包括图书ID、图书名称、作者、出版日期、ISBN号等。根据具体业务需求,还可以添加其他字段,如用户权限、图书类别等。
在设计数据库表时,需要考虑到数据的完整性、一致性和性能等方面,合理设计表结构能够更好地支撑应用程序的功能和性能要求。
在用户表中,我们只需要包含用户名和密码这两个基本字段即可,但在复杂的业务情况下,可能还涉及到昵称、年龄等其他资料。
对于图书表,需要确定哪些字段是必要的,这通常需要参考需求页面,但需要注意的是,图书表的设计不应该仅仅依赖于单个页面,而应该是对整个系统进行全面分析和观察后才能决定的。
创建数据库 book_test
-- 创建数据库
DROP DATABASE IF EXISTS book_test;
CREATE DATABASE book_test DEFAULT CHARACTER SET utf8mb4;
USE book_test;
-- 用户表
DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info (
`id` INT NOT NULL AUTO_INCREMENT,
`user_name` VARCHAR(128) NOT NULL,
`password` VARCHAR(128) NOT NULL,
`delete_flag` TINYINT(4) NULL DEFAULT 0,
`create_time` DATETIME DEFAULT NOW(),
`update_time` DATETIME DEFAULT NOW() ON UPDATE NOW(),
PRIMARY KEY (`id`),
UNIQUE INDEX `user_name_UNIQUE` (`user_name` ASC)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 图书表
DROP TABLE IF EXISTS book_info;
CREATE TABLE `book_info` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`book_name` VARCHAR(127) NOT NULL,
`author` VARCHAR(127) NOT NULL,
`count` INT(11) NOT NULL,
`price` DECIMAL(7,2) NOT NULL,
`publish` VARCHAR(256) NOT NULL,
`status` TINYINT(4) DEFAULT 1 COMMENT '0-无效, 1-正常, 2-不允许借阅',
`create_time` DATETIME DEFAULT NOW(),
`update_time` DATETIME DEFAULT NOW() ON UPDATE NOW(),
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;
-- 初始化数据
INSERT INTO user_info (user_name, password) VALUES ('admin', 'admin');
INSERT INTO user_info (user_name, password) VALUES ('zhangsan', '123456');
-- 初始化图书数据
INSERT INTO `book_info` (book_name, author, count, price, publish) VALUES ('活着', '余华', 29, 22.00, '北京文艺出版社');
INSERT INTO `book_info` (book_name, author, count, price, publish) VALUES ('平凡的世界', '路遥', 5, 98.56, '北京十月文艺出版社');
INSERT INTO `book_info` (book_name, author, count, price, publish) VALUES ('三体', '刘慈欣', 9, 102.67, '重庆出版社');
INSERT INTO `book_info` (book_name, author, count, price, publish) VALUES ('金字塔原理', '麦肯锡', 16, 178.00, '企主与建设出版社');
引入 MyBatis 和 MySQL 驱动依赖
修改pom文件,记得刷新,修改和前面一样的,此处不再赘述。
配置数据库&日志
同理,修改配置文件,记得改成自己的数据库名称、MySQL用户名和密码:
# 应用服务 WEB 访问端口
server.port: 8080
# 下面这些内容是为了让 MyBatis 映射
# 指定 MyBatis 的 Mapper 文件
mybatis.mapper-locations: classpath:mappers/*xml
# 指定 MyBatis 的实体目录
mybatis.type-aliases-package: com.example.mybatisstudy.mybatis.entity
# 数据库连接配置
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/message_wall?characterEncoding=utf8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration: # 配置打印 MyBatis⽇志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true #配置驼峰⾃动转换
mapper-locations: classpath:mapper/**Mapper.xml
Model创建
package com.example.librarysystem.book.model;
import lombok.Data;
import java.util.Date;
@Data
public class UserInfo {
private Integer id;
private String userName;
private String password;
private Integer delete_flag;
private Date createTime;
private Date updateTime;
}
package com.example.librarysystem.book.model;
import java.math.BigDecimal;
import java.util.Date;
import lombok.Data;
@Data
public class BookInfo {
private Integer ID;
private String bookName;
private String author;
private Integer count;
private BigDecimal price;
private String publish;
//尽可能避免存储字符串:
private Integer state;//1-可借阅 2-不可借阅
private Integer status;//1-可借阅 2-不可借阅
private String stateCN;
private Date createTime;
private Date updateTime;
}
用户登录
约定前后端交互接口
请求:
POST /user/login
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
参数:
name=zhangsan&password=123456
响应:
true
说明:
- 发送 POST 请求到 /user/login 接口,请求内容为表单格式。
- 参数包括用户的用户名和密码。
- 服务器返回一个布尔类型的数据。如果验证成功,则返回 true,否则返回 false。
实现服务器代码
数据层:
创建UserInfoMapper:
package com.example.librarysystem.book.mapper;
import com.example.librarysystem.book.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserInfoMapper {
@Select("select * from user_info where delete_flag=0 and user_name=#{userName}")
UserInfo queryByName(String userName);
}
业务层:
创建UserService:
package com.example.librarysystem.book.service;
import com.example.librarysystem.book.mapper.UserInfoMapper;
import com.example.librarysystem.book.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
/**
* 从数据中查询用户信息
* @return
*/
public UserInfo queryByName(String userName){
return userInfoMapper.queryByName(userName);
}
}
控制层:
从数据库中,根据名称查询用户,如果可以查到,并且密码⼀致,就认为登录成功。
创建UserController:
package com.example.librarysystem.book.controller;
import com.example.librarysystem.book.model.UserInfo;
import com.example.librarysystem.book.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public boolean login(String userName, String password, HttpSession session) {
//校验参数
if (!StringUtils.hasLength(userName) || (!StringUtils.hasLength(password))) {
return false;
}
//校验密码是否正确
//存Session
session.setAttribute("userName", userName);
//判断数据库的密码和用户输入的密码是否一致
//查询数据库, 得到数据库的密码
UserInfo userInfo = userService.queryByName(userName);
if (userInfo == null) {
return false;
}
if (password.equals(userInfo.getPassword())) {
userInfo.setPassword("");
//密码正确
session.setAttribute("user_session", userInfo);
return true;
}
return false;
}
}
这里你可能会有疑问:
在我们的代码中,存储的 session 名字是相同的 "user_session",每次存的用户信息的session名字都一样,后面的信息会覆盖前面的session吗?
对于同一个客户端(用户),后面存储的信息会覆盖前面的信息。因为 session 是基于会话的,每个客户端(浏览器)都有自己的 session,它们之间是相互隔离的。
在我们的代码中,存储的 session 名字是相同的 "user_session",因此后面存储的用户信息会覆盖之前存储的信息。这意味着如果同一个用户登录了多次,后一次登录的信息会覆盖前一次登录的信息。
但是!对于不同的用户(不同的客户端),它们的 session 是不同的,因此存储的信息不会相互覆盖。每个用户都有自己独立的 session,用于存储与该用户相关的信息。
测试
部署程序,验证服务器是否能正确返回数据。
先测试后端接口:
测试前端代码:
成功进入:
添加图书
约定前后端交互接口
请求方式: POST
请求地址: /book/addBook
请求头:
- Content-Type: application/x-www-form-urlencoded; charset=UTF-8
请求参数:
- bookName:图书名称(String)
- author:作者(String)
- count:数量(Integer)
- price:价格(Double)
- publish:出版社(String)
- status:状态(Integer)
响应数据:
- 成功时返回空字符串 "",表示添加图书成功。
- 失败时返回相应的错误信息字符串,例如 "参数不完整" 或 "添加图书失败"。
约定:
- 客户端通过发送 POST 请求到 /book/addBook 接口来提交图书信息。
- 请求参数以表单的形式提交。
- 服务器根据请求参数的完整性和有效性进行处理。
- 如果添加图书成功,则返回空字符串 ""。
- 如果添加图书失败,则返回相应的错误信息字符串。
实现服务器代码
数据层:创建BookInfoMapper文件。
package com.example.librarysystem.book.mapper;
import com.example.librarysystem.book.model.BookInfo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BookInfoMapper {
@Insert("insert into book_info (book_name, author, count, price, publish) " +
"values (#{bookName}, #{author}, #{count}, #{price}, #{publish} )")
Integer insertBook(BookInfo bookInfo);
}
业务层: 在BookService中补充代码:
public Integer insertBook(BookInfo bookInfo){
return bookInfoMapper.insertBook(bookInfo);
}
package com.example.librarysystem.book.service;
import com.example.librarysystem.book.dao.BookDao;
import com.example.librarysystem.book.mapper.BookInfoMapper;
import com.example.librarysystem.book.model.BookInfo;
import com.sun.org.apache.bcel.internal.generic.NEW;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class BookService {
/*
* 根据数据层返回的结果, 对数据进行处理
* */
@Autowired
private BookDao bookDao;
@Autowired
private BookInfoMapper bookInfoMapper;
public List<BookInfo> bookInfoList(){
List<BookInfo> bookInfos= bookDao.mockBookData();
//2.对数据进行处理——对状态进行处理
for(BookInfo bookInfo : bookInfos){
if(bookInfo.getState()==1){
bookInfo.setStateCN("可借阅");
}else if(bookInfo.getState()==2){
bookInfo.setStateCN("不可借阅");
}
}
return bookInfos;
}
public Integer insertBook(BookInfo bookInfo){
return bookInfoMapper.insertBook(bookInfo);
}
}
控制层:在BookController补充代码:
我们应该要先进行参数校验,校验通过再进行图书添加。
在实际开发中,首先进行参数校验,只有校验通过了才进行图书添加操作。即使前端进行了参数校验,后端开发人员也应该进行再次校验。这是因为后端接口可能会遭受到黑客攻击,黑客可能会绕过前端直接访问后端接口。如果后端不进行校验,就有可能产生脏数据或者执行恶意操作。在学习阶段,我们暂时不涉及安全领域模块的开发,防止攻击一般是由企业统一来实施和管理的。
@RequestMapping("/addBook")
public String addBook(BookInfo bookInfo){
log.info("添加图书, bookInfo:{}",bookInfo);
//参数校验
if (!StringUtils.hasLength(bookInfo.getBookName())
|| !StringUtils.hasLength(bookInfo.getAuthor())
|| bookInfo.getCount()<=0
|| bookInfo.getPrice()==null
|| !StringUtils.hasLength(bookInfo.getPublish())){
return "参数错误";
}
//添加图书
try {
bookService.insertBook(bookInfo);
}catch (Exception e){
return "内部发生错误, 请联系管理员";
}
return "";
}
package com.example.librarysystem.book.controller;
import com.example.librarysystem.book.model.BookInfo;
import com.example.librarysystem.book.service.BookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import static jdk.nashorn.internal.runtime.regexp.joni.Config.log;
@RequestMapping("/book")
@RestController
@Slf4j
public class BookController {
@Autowired
private BookService bookService;
@RequestMapping("/getList")
public List<BookInfo> getList(){
//1.从数据库中获取数据
//数据采用Mock的方式
List<BookInfo> bookInfos = bookService.bookInfoList();
//3.返回数据
return bookInfos;
}
@RequestMapping("/addBook")
public String addBook(BookInfo bookInfo){
log.info("添加图书, bookInfo:{}",bookInfo);
//参数校验
if (!StringUtils.hasLength(bookInfo.getBookName())
|| !StringUtils.hasLength(bookInfo.getAuthor())
|| bookInfo.getCount()<=0
|| bookInfo.getPrice()==null
|| !StringUtils.hasLength(bookInfo.getPublish())){
return "参数错误";
}
//添加图书
try {
bookService.insertBook(bookInfo);
}catch (Exception e){
return "内部发生错误, 请联系管理员";
}
return "";
}
}
实现客户端代码:
提供的前面的页面中,JS已经提前留了空位。
<script type="text/javascript" src="js/jquery.min.js"></script>
<script>
function add() {
alert("添加成功");
location.href = "book_list.html";
//添加图书
$.ajax({
type:"post",
url: "/book/addBook",
data: $("#addBook").serialize(),
success:function(result){
if(result==""){
//图书添加成功
location.href = "book_list.html";
}else{
//图书添加失败
alert(result);
}
}
});
// alert("添加成功");
// location.href = "book_list.html";
}
</script>
测试
还是一样的,先进行后端的测试:
添加成功:
数据插入成功!
图书列表
刚刚其实可以看到,在添加图书之后跳转到了我们的图书列表页面,但是并没有显示刚才添加的图书信息。
接下来就我们来实现图书列表,这可是一个大工程……
需求分析
我们之前设计的留言墙查询功能是简单地将数据库中的所有数据一次性查询出来并展示在页面上。然而,假设数据库中存在大量数据(比如几万条),直接将所有数据一次性展示在页面上显然不太现实。为了解决这个问题,我们需要引入分页功能。
分页是一种常见的数据展示方式,它通过将数据分割成若干页,每页显示一定数量的数据来减轻页面加载压力和提升用户体验。例如,我们可以设置每页展示10条数据。这样,用户在访问页面时只会看到当前页的数据,如果需要查看更多数据,可以通过点击页面下方的页码进行查询。
通过引入分页功能,我们可以更高效地展示数据库中的大量数据,同时让用户可以方便地浏览不同页码的数据,提升了系统的性能和用户体验。
在分页过程中,数据的展示方式如下:
- 第1页:展示第1条到第10条的数据
- 第2页:展示第11条到第20条的数据
- 第3页:展示第21条到第30条的数据
- 依此类推...
要实现分页功能,我们需要利用数据库的功能进行分页查询。在SQL语句中,我们使用LIMIT关键字来指定查询结果的数量和偏移量。LIMIT的格式为 LIMIT 开始索引, 每页显示的条数,其中开始索引表示从查询结果中的第几条数据开始,索引从0开始计数,每页显示的条数则确定了每页展示的数据量。通过设置不同的开始索引和每页显示的条数,我们可以实现分页功能,逐页展示查询结果。
我们先给数据库添加更多的数据,便于我们进行分页:
INSERT INTO book_info (book_name, author, count, price, publish)
VALUES
('了不起的盖茨比', 'F·斯科特·菲茨杰拉德', 50, 12.99, '斯克里布纳'),
('杀死一只知更鸟', '哈珀·李', 45, 10.99, '哈珀·珍珠现代经典'),
('1984', '乔治·奥威尔', 55, 9.99, '西格内特经典'),
('傲慢与偏见', '简·奥斯汀', 40, 8.99, '现代图书馆'),
('麦田里的守望者', 'J·D·塞林格', 30, 11.50, '小布朗出版公司'),
('动物农场', '乔治·奥威尔', 25, 7.99, '企鹅图书'),
('美丽新世界', '阿道斯·赫胥黎', 35, 10.25, '哈珀·珍珠现代经典'),
('霍比特人', 'J.R.R.托尔金', 60, 15.75, '水手图书'),
('哈利·波特与魔法石', 'J.K.罗琳', 65, 18.99, 'Scholastic'),
('指环王', 'J.R.R.托尔金', 70, 20.50, '水手图书'),
('简·爱', '夏洛蒂·勃朗特', 50, 11.99, '企鹅经典'),
('呼啸山庄', '艾米莉·勃朗特', 45, 10.75, '企鹅经典'),
('白鲸记', '赫尔曼·梅尔维尔', 40, 12.25, '华兹华斯出版社'),
('道林·格雷的画像', '奥斯卡·王尔德', 55, 14.99, '多佛出版社'),
('弗兰肯斯坦', '玛丽·雪莱', 30, 9.50, '西格内特经典'),
('哈克贝利·费恩历险记', '马克·吐温', 35, 8.75, '多佛出版社'),
('德古拉', '布拉姆·斯托克', 20, 7.99, '企鹅经典'),
('奥德赛', '荷马', 65, 13.25, '企鹅经典'),
('远大前程', '查尔斯·狄更斯', 70, 12.99, '企鹅经典'),
('坎特伯雷故事集', '杰弗里·乔叟', 55, 11.50, '企鹅经典');
这条SQL语句的作用是从名为book_info的表中选择所有列(*表示所有列),其中status列的值不等于0(即排除了状态为0的图书),并且从结果集中获取第1到第10行的数据。LIMIT关键字用于限制结果集的返回数量和偏移量。在这里,0 是偏移量,表示从结果集中的第 1 行开始获取数据,而 10 表示返回的最大行数,即每页显示的数据量。
-- 查询第1页的SQL语句:
SELECT * FROM book_info WHERE `status` <> 0 LIMIT 0,10;
-- 查询第2页的SQL语句:
SELECT * FROM book_info WHERE `status` <> 0 LIMIT 10,10;
-- 查询第3页的SQL语句:
SELECT * FROM book_info WHERE `status` <> 0 LIMIT 20,10;
-- 查询第4页的SQL语句:
SELECT * FROM book_info WHERE `status` <> 0 LIMIT 30,10;
-- 查询第5页的SQL语句:
SELECT * FROM book_info WHERE `status` <> 0 LIMIT 40,10;
-- 查询第6页的SQL语句:
SELECT * FROM book_info WHERE `status` <> 0 LIMIT 50,10;
以此类推...
观察以上SQL语句,可以看到开始索引在不断变化,每页显示的条数是固定的。开始索引的计算公式为:开始索引 = (当前页码 - 1) * 每页显示条数。
根据前端页面的需求和后端响应的数据,我们可以得出以下结论:
-
前端查询请求需要传递的参数包括:
- currentPage:当前页码,默认值为1。
- pageSize:每页显示条数,默认值为10。 为了提高项目的扩展性,通常不会将固定值硬编码在代码中,而是以参数的形式进行传递。扩展性指的是软件系统能够适应未来需求变化而进行扩展的能力。例如,如果当前需求是每页显示10条记录,后期需求变更为每页显示20条记录,后端代码无需任何修改。
-
后端响应给前端的数据包括:
- records:查询到的数据列表,存储在List集合中。
- total:总记录数,用于告知前端应显示多少页。显示页数可通过以下公式计算:totalPage = (total + pageSize - 1) / pageSize。 具体计算方法是先计算出总页数的最大值((total + pageSize - 1)),然后除以每页显示的条数(pageSize),即可得到总页数。
翻页请求和响应部分,我们通常封装在两个对象中。
翻页请求对象
我们需要根据currentPage 和 pageSize,计算出来开始索引。
PageRequest修改为:
package com.example.librarysystem.book.model;
import lombok.Data;
@Data
public class PageRequest {
//当前页
private Integer currentPage =1;
//每页显示个数
private Integer pageSize =10;
/**
* 从多少条记录开始查询
*/
private Integer offset;
public Integer getOffset() {
return (currentPage-1) * pageSize;
}
}
翻页列表结果类:
package com.example.librarysystem.book.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class PageResult<T> {
private List<T> records;
private Integer count;
}
基于以上分析,我们来约定前后端交互接口:
约定前后端交互接口
-
请求信息:
- 请求路径:/book/getListByPage
- 请求参数:
- currentPage:当前页码,用于告诉服务器请求的是第几页的数据。如果不传递参数,默认为第一页。
- pageSize:每页显示的条数,用于告诉服务器每页需要展示多少条数据。
-
请求头:
- Content-Type: application/x-www-form-urlencoded; charset=UTF-8
-
响应信息:
- 响应数据格式:JSON
- 响应数据包括:
- total:总记录数,告知前端共有多少条记录。
- records:查询到的数据列表,存储着每条记录的详细信息,包括书籍的ID、书名、作者、数量、价格、出版社、状态等。
根据约定,浏览器向服务器发送请求时,通过currentPage参数告知服务器请求的是第几页的数据。后端根据请求参数返回对应页的数据,第一页可以不传参数,因为currentPage默认值为1。
实现服务器代码
控制层:
完善 BookController:
@RequestMapping("/getListByPage")
public PageResult<BookInfo> getListByPage(PageRequest pageRequest){
log.info("查询列表信息, pageRequest:{}", pageRequest);
if (pageRequest.getCurrentPage()<1){
return null;
}
return bookService.getListByPage(pageRequest);
}
数据层:
翻页查询SQL。
BookInfoMapper :
图书列表按id降序排列:
/**
* 查询总数
*/
@Select("select count(1) from book_info where `status` <>0")
Integer count();
/**
* 查询当前页的数据
*/
@Select("select * from book_info where `status` <>0 order by id desc limit #{offset}, #{pageSize}")
List<BookInfo> queryListBypage(PageRequest pageRequest);
其中offset 在PageRequest类中已经给赋值。
业务层
BookService:
public PageResult<BookInfo> getListByPage(PageRequest pageRequest) {
//1. 查询记录的总数
Integer count = bookInfoMapper.count();
//2. 查询当前页的数据
List<BookInfo> bookInfos = bookInfoMapper.queryListBypage(pageRequest);
PageResult<BookInfo> result = new PageResult<>();
//result.setCount(count);
//result.setRecords(bookInfos);
//return result;
return new PageResult<>(bookInfos,count);
}
-
翻页信息查询:在实现翻页功能时,需要返回两类信息:数据的总数和当前页的列表信息。为了获取这两类信息,通常需要执行两次SQL查询操作。第一次查询获取总记录数,第二次查询获取当前页的数据列表。
-
图书状态映射:图书状态与数据库中存储的status字段存在对应关系。如果未来图书状态码发生变动,可能需要修改项目中涉及到状态码的相关代码。为了避免频繁的代码修改,通常可以采用枚举类来处理状态码的映射关系。通过枚举类,可以将状态码与对应的状态描述进行静态映射,从而提高代码的可维护性和可扩展性。
现在我们可以现在后台测试一下了:
没有问题。
实现客户端代码
我们定义:
- 当访问第一页图书时,前端URL为:http://127.0.0.1:8080/book_list.html?currentPage=1
- 当访问第二页列表时,前端URL为:http://127.0.0.1:8080/book_list.html?currentPage=2
要求:
- 当浏览器访问book_list.html页面时,会向后端发送请求,后端返回数据后将其显示在页面上。
- 后端请求的URL应为:/book/getListByPage?currentPage=1
- 在前端的JavaScript中,需要将原来的后端请求方法从/book/getList修改为/book/getListByPage?currentPage=1。
我们先修改了这个部分:
运行之后发现能够正确显示10条数据了,但是状态都是null:
我们回过头去完善一下BookService:
public PageResult<BookInfo> getListByPage(PageRequest pageRequest) {
//1. 查询记录的总数
Integer count = bookInfoMapper.count();
//2. 查询当前页的数据
List<BookInfo> bookInfos = bookInfoMapper.queryListBypage(pageRequest);
for (BookInfo bookInfo:bookInfos){
//根据状态, 设置描述
if(bookInfo.getStatus()==1){
bookInfo.setStateCN("可借阅");
}
else if(bookInfo.getStatus()==2){
bookInfo.setStateCN("不可借阅");
}else {
bookInfo.setStateCN("无效");
}
}
return new PageResult<>(bookInfos,count);
}
再次运行:
但是仔细想想,以后可能还有很多地方要添加上一些固定内容,一点一点到处修改很麻烦,不如把它们进行统一:
创建enmus目录, 创建BookStatusEnums类:
package com.example.librarysystem.book.model;
import lombok.Data;
public enum BookStatusEnums {
DELETE(0,"无效"),
NORMAL(1,"可借阅"),
FORBIDDEN(2,"不可借阅"),
;
private int code;
private String desc;
BookStatusEnums(int code, String desc) {
this.code = code;
this.desc = desc;
}
//根据code, 获取描述
public static BookStatusEnums getDescByCode(int code){
switch (code){
case 0: return BookStatusEnums.DELETE;
case 1: return BookStatusEnums.NORMAL;
case 2: return BookStatusEnums.FORBIDDEN;
}
return BookStatusEnums.DELETE;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
此时, BookService的代码, 可以修改如下:
public PageResult<BookInfo> getListByPage(PageRequest pageRequest) {
//1. 查询记录的总数
Integer count = bookInfoMapper.count();
//2. 查询当前页的数据
List<BookInfo> bookInfos = bookInfoMapper.queryListBypage(pageRequest);
for (BookInfo bookInfo:bookInfos){
//根据状态, 设置描述
bookInfo.setStateCN(BookStatusEnums.getDescByCode(bookInfo.getStatus()).getDesc());
}
return new PageResult<>(bookInfos,count,pageRequest);
}
运行一下,一切正常:
现在的前端代码中,我们尚未设置currentPage参数的URL。
我们可以直接使用location.search从URL中获取参数信息。location.search用于获取URL的查询字符串,其中包含问号(?)和参数信息。
例如,对于URL http://127.0.0.1:8080/book_list.html?currentPage=1,location.search返回的是?currentPage=1。因此,我们可以将URL修改为"/book/getListByPage" + location.search,以便将查询字符串直接拼接到请求URL的末尾。
关于页面下方的页码条部分,我们使用的是一个插件:jqPaginator分页组件
很多风格随你选择:
现在有个问题,动态的当前页面如何获取?
在获取动态当前页面的过程中,有两种常见的方法:
- 从URL中获取: 在前端,我们可以从URL中获取当前页面的动态参数。例如,对于URL 127.0.0.1:9090/book list.htm?currentPage=5,我们可以先使用问号(?)分割URL,然后获取问号后面的部分,即查询字符串。接着,我们可以使用等号(=)分割查询字符串,根据键(key)匹配,从而获取到currentPage参数的值。这样,我们就可以动态地获取当前页面的页码信息。
- 后端传递: 另一种方法是在后端将当前页面的页码信息一同传递给前端。当前端请求数据时,后端可以将当前页码作为响应的一部分返回给前端。前端收到响应后,就可以直接使用后端传递的当前页面信息,而不需要从URL中解析。
我们采用第二种思路:
package com.example.librarysystem.book.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class PageResult<T> {
private List<T> records;
private Integer count;
private PageRequest pageRequest;
}
补充上BookService:
package com.example.librarysystem.book.service;
//
//import com.example.librarysystem.book.dao.BookDao;
import com.example.librarysystem.book.mapper.BookInfoMapper;
import com.example.librarysystem.book.model.BookInfo;
import com.example.librarysystem.book.model.BookStatusEnums;
import com.example.librarysystem.book.model.PageRequest;
import com.example.librarysystem.book.model.PageResult;
import com.sun.org.apache.bcel.internal.generic.NEW;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class BookService {
/*
* 根据数据层返回的结果, 对数据进行处理
* */
// @Autowired
// private BookDao bookDao;
@Autowired
private BookInfoMapper bookInfoMapper;
// public List<BookInfo> bookInfoList(){
//
// List<BookInfo> bookInfos= bookDao.mockBookData();
// //2.对数据进行处理——对状态进行处理
// for(BookInfo bookInfo : bookInfos){
// if(bookInfo.getState()==1){
// bookInfo.setStateCN("可借阅");
// }else if(bookInfo.getState()==2){
// bookInfo.setStateCN("不可借阅");
// }
// }
// return bookInfos;
// }
public Integer insertBook(BookInfo bookInfo){
return bookInfoMapper.insertBook(bookInfo);
}
// public PageResult<BookInfo> getListByPage(PageRequest pageRequest) {
// //1. 查询记录的总数
// Integer count = bookInfoMapper.count();
// //2. 查询当前页的数据
// List<BookInfo> bookInfos = bookInfoMapper.queryListBypage(pageRequest);
// PageResult<BookInfo> result = new PageResult<>();
result.setCount(count);
result.setRecords(bookInfos);
return result;
// return new PageResult<>(bookInfos,count);
// }
public PageResult<BookInfo> getListByPage(PageRequest pageRequest) {
//1. 查询记录的总数
Integer count = bookInfoMapper.count();
//2. 查询当前页的数据
List<BookInfo> bookInfos = bookInfoMapper.queryListBypage(pageRequest);
for (BookInfo bookInfo:bookInfos){
//根据状态, 设置描述
bookInfo.setStateCN(BookStatusEnums.getDescByCode(bookInfo.getStatus()).getDesc());
}
return new PageResult<>(bookInfos,count,pageRequest);
}
}
然后又遇到一个问题:
页面初始化的同时也会进行页面跳转,这样就会进入死循环,你一初始化就跳转,一初始化就跳转。
我们多加一个条件判断:
function getBookList() {
$.ajax({
type: "get",
url: "/book/getListByPage"+location.search,
success: function (result) {
var books = result.records;
console.log(books);
var finalHtml = "";
for(var book of books){
//拼接html
finalHtml +='<tr>';
finalHtml +='<td><input type="checkbox" name="selectBook" value="'+book.id+'" id="selectBook" class="book-select"></td>';
finalHtml +='<td>'+book.id+'</td>';
finalHtml +='<td>'+book.bookName+'</td>';
finalHtml +='<td>'+book.author+'</td>';
finalHtml +='<td>'+book.count+'</td>';
finalHtml +='<td>'+book.price+'</td>';
finalHtml +='<td>'+book.publish+'</td>';
finalHtml +='<td>'+book.stateCN+'</td>';
finalHtml +='<td>';
finalHtml +='<div class="op">';
finalHtml +='<a href="book_update.html?bookId='+book.id+'">修改</a>';
finalHtml +='<a href="javascript:void(0)" onclick="deleteBook('+book.id+')">删除</a>';
finalHtml +='</div>';
finalHtml +='</td>';
finalHtml +='</tr>';
}
$("tbody").html(finalHtml);
//翻页信息
$("#pageContainer").jqPaginator({
totalCounts: result.count, //总记录数
pageSize: 10, //每页的个数
visiblePages: 7, //可视页数
currentPage: result.pageRequest.currentPage, //当前页码
first: '<li class="page-item"><a class="page-link">首页</a></li>',
prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',
next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',
last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',
page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',
//页码点击时都会执行
onPageChange: function (page, type) {
console.log("第" + page + "页, 类型:" + type);
if(type=="change"){
location.href = "book_list.html"+location.search;
}
}
});
}
});
}
运行:
点击第二页没有反应……
再次修改:
成功跳转:
翻页的前端代码部分就完成了!
function getBookList() {
$.ajax({
type: "get",
url: "/book/getListByPage"+location.search,
success: function (result) {
var books = result.records;
console.log(books);
var finalHtml = "";
for(var book of books){
//拼接html
finalHtml +='<tr>';
finalHtml +='<td><input type="checkbox" name="selectBook" value="'+book.id+'" id="selectBook" class="book-select"></td>';
finalHtml +='<td>'+book.id+'</td>';
finalHtml +='<td>'+book.bookName+'</td>';
finalHtml +='<td>'+book.author+'</td>';
finalHtml +='<td>'+book.count+'</td>';
finalHtml +='<td>'+book.price+'</td>';
finalHtml +='<td>'+book.publish+'</td>';
finalHtml +='<td>'+book.stateCN+'</td>';
finalHtml +='<td>';
finalHtml +='<div class="op">';
finalHtml +='<a href="book_update.html?bookId='+book.id+'">修改</a>';
finalHtml +='<a href="javascript:void(0)" onclick="deleteBook('+book.id+')">删除</a>';
finalHtml +='</div>';
finalHtml +='</td>';
finalHtml +='</tr>';
}
$("tbody").html(finalHtml);
//翻页信息
$("#pageContainer").jqPaginator({
totalCounts: result.count, //总记录数
pageSize: 10, //每页的个数
visiblePages: 7, //可视页数
currentPage: result.pageRequest.currentPage, //当前页码
first: '<li class="page-item"><a class="page-link">首页</a></li>',
prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',
next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',
last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',
page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',
//页码点击时都会执行
onPageChange: function (page, type) {
console.log("第" + page + "页, 类型:" + type);
if(type=="change"){
location.href = "book_list.html?currentPage="+page;
}
}
});
}
});
}
修改图书
约定前后端交互接口
进入修改页面,需要显示当前图书的信息;
此接口用于根据图书ID查询图书信息。请求时,根据图书ID,获取当前图书的信息。响应返回了图书的详细信息,包括图书ID、书名、作者、数量、价格、出版社、状态、创建时间和更新时间等。
请求:/book/queryBookById?bookId=25
参数:bookId
响应:
{
"id": 25,
"bookName": "图书21",
"author": "作者2",
"count": 999,
"price": 222.00,
"publish": "出版社1",
"status": 2,
"statusCN": null,
"createTime": "2023-09-04T04:01:27.000+00:00",
"updateTime": "2023-09-05T03:37:03.000+00:00"
}
点击修改按钮,修改图书信息。
请求:
/book/updateBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
参数:
- id: 图书ID
- bookName: 图书名称
- author: 作者
- count: 数量
- price: 价格
- publish: 出版社
- status: 状态
响应:"" // 失败信息,成功时返回空字符串
根据约定,浏览器向服务器发送 /book/updateBook 的HTTP请求,使用form表单形式提交数据。服务器对提交的图书信息进行处理,成功更新图书信息时返回空字符串,表示更新成功;否则返回相应的失败信息。
实现服务端代码
数据层
根据图书ID,查询图书信息
/**
* 根据ID查询图书信息
*/
@Select("select * from book_info where `status` <>0 and id= #{id}")
BookInfo queryBookById(Integer id);
/**
* 根据ID, 修改图书信息
*/
Integer updateBook(BookInfo bookInfo);
更新逻辑相对较为复杂,传递了哪些值?我们需要更新哪些值?这里都需要使用到动态SQL。
对于我们这种初学者而言,注解的方式拼接动态SQL不太友好,我们还是采用xml的方式来实现。
配置xml路径:
mybatis:
mapper-locations: classpath:mapper/**Mapper.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.example.librarysystem.book.mapper.BookInfoMapper">
<update id="updateBook">
update book_info
<set>
<if test="bookName!=null">
book_name = #{bookName},
</if>
<if test="author!=null">
author =#{author},
</if>
<if test="count!=null">
count = #{count},
</if>
<if test="price!=null">
price =#{price},
</if>
<if test="publish">
publish = #{publish},
</if>
<if test="status!=null">
status =#{status},
</if>
</set>
where id = #{id}
</update>
</mapper>
控制层
BookController:
package com.example.librarysystem.book.controller;
import com.example.librarysystem.book.model.BookInfo;
import com.example.librarysystem.book.model.PageRequest;
import com.example.librarysystem.book.model.PageResult;
import com.example.librarysystem.book.service.BookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import static jdk.nashorn.internal.runtime.regexp.joni.Config.log;
@RequestMapping("/book")
@RestController
@Slf4j
public class BookController {
@Autowired
private BookService bookService;
// @RequestMapping("/getList")
// public List<BookInfo> getList(){
// //1.从数据库中获取数据
// //数据采用Mock的方式
//
// List<BookInfo> bookInfos = bookService.bookInfoList();
//
// //3.返回数据
// return bookInfos;
// }
@RequestMapping("/addBook")
public String addBook(BookInfo bookInfo){
log.info("添加图书, bookInfo:{}",bookInfo);
//参数校验
if (!StringUtils.hasLength(bookInfo.getBookName())
|| !StringUtils.hasLength(bookInfo.getAuthor())
|| bookInfo.getCount()<=0
|| bookInfo.getPrice()==null
|| !StringUtils.hasLength(bookInfo.getPublish())){
return "参数错误";
}
//添加图书
try {
bookService.insertBook(bookInfo);
}catch (Exception e){
return "内部发生错误, 请联系管理员";
}
return "";
}
@RequestMapping("/getListByPage")
public PageResult<BookInfo> getListByPage(PageRequest pageRequest){
log.info("查询列表信息, pageRequest:{}", pageRequest);
if (pageRequest.getCurrentPage()<1){
return null;
}
return bookService.getListByPage(pageRequest);
}
@RequestMapping("/queryBookById")
public BookInfo queryBookById(Integer bookId){
log.info("查询图书信息, bookId:"+bookId);
//参数校验
if (bookId<1){
log.error("非法图书ID, bookId:"+bookId);
return null;
}
return bookService.queryBookById(bookId);
}
@RequestMapping("/updateBook")
public boolean updateBook(BookInfo bookInfo){
log.info("更新图书, updateBook:{}",bookInfo);
if (!StringUtils.hasLength(bookInfo.getBookName())
|| !StringUtils.hasLength(bookInfo.getAuthor())
|| bookInfo.getCount()<=0
|| bookInfo.getPrice()==null
|| !StringUtils.hasLength(bookInfo.getPublish())){
return false;
}
try {
Integer result = bookService.updateBook(bookInfo);
if (result<=0){
return false;
}
return true;
}catch (Exception e){
log.error("更新图书失败, e:{}", e);
return false;
}
}
}
业务层
BookService:
public BookInfo queryBookById(Integer bookId) {
return bookInfoMapper.queryBookById(bookId);
}
public Integer updateBook(BookInfo bookInfo) {
return bookInfoMapper.updateBook(bookInfo);
}
现在我们可以测试一下后端代码:
实现客户端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>修改图书</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/add.css">
</head>
<body>
<div class="container">
<div class="form-inline">
<h2 style="text-align: left; margin-left: 10px;"><svg xmlns="http://www.w3.org/2000/svg" width="40"
fill="#17a2b8" class="bi bi-book-half" viewBox="0 0 16 16">
<path
d="M8.5 2.687c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z" />
</svg>
<span>修改图书</span>
</h2>
</div>
<form id="updateBook">
<input type="hidden" class="form-control" id="bookId" name="id">
<div class="form-group">
<label for="bookName">图书名称:</label>
<input type="text" class="form-control" id="bookName" name="bookName">
</div>
<div class="form-group">
<label for="bookAuthor">图书作者</label>
<input type="text" class="form-control" id="bookAuthor" name="author"/>
</div>
<div class="form-group">
<label for="bookStock">图书库存</label>
<input type="text" class="form-control" id="bookStock" name="count"/>
</div>
<div class="form-group">
<label for="bookPrice">图书定价:</label>
<input type="number" class="form-control" id="bookPrice" name="price">
</div>
<div class="form-group">
<label for="bookPublisher">出版社</label>
<input type="text" id="bookPublisher" class="form-control" name="publish"/>
</div>
<div class="form-group">
<label for="bookStatus">图书状态</label>
<select class="custom-select" id="bookStatus" name="status">
<option value="1" selected>可借阅</option>
<option value="2">不可借阅</option>
</select>
</div>
<div class="form-group" style="text-align: right">
<button type="button" class="btn btn-info btn-lg" onclick="update()">确定</button>
<button type="button" class="btn btn-secondary btn-lg" onclick="javascript:history.back()">返回</button>
</div>
</form>
</div>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script>
$.ajax({
type: "get",
url: "/book/queryBookById"+location.search,
success:function(book){
console.log(book);
if(book!=null){
$("#bookId").val(book.id);
$("#bookName").val(book.bookName);
$("#bookAuthor").val(book.author);
$("#bookStock").val(book.count);
$("#bookPrice").val(book.price);
$("#bookPublisher").val(book.publish);
$("#bookStatus").val(book.status);
}
}
});
function update() {
alert("更新成功");
location.href = "book_list.html"
// alert("更新成功");
// location.href = "book_list.html"
$.ajax({
type:"post",
url: "/book/updateBook",
data: $("#updateBook").serialize(),
success:function(result){
if(result==true){
location.href = "book_list.html"
}else{
alert("更新失败");
}
}
});
}
</script>
</body>
</html>
现在我们来进行前端代码的测试:
但是现在出现了一个问题,我们的数据并没有进行更新……
这个时候你会发现前后端完全没有任何报错信息……
我们此时只能从手动打印的日志着手:
原来是这里写错了,不应该写成大写的ID,应该是id:
修改后重新运行:
更新成功!
数据库也更新成功。
删除图书
约定前后端交互接口
删除操作在数据库管理中通常分为两种方式:逻辑删除和物理删除。
逻辑删除,又称为软删除、假删除或Soft Delete,是指在删除数据时,并不真正地从数据库中移除数据,而是通过在数据表的某一列(通常命名为is_deleted或类似的)上设置一个删除标识来表示该行数据已被标记为删除状态。在逻辑删除中,常见的做法是使用UPDATE语句来更新该标识,如下所示:
UPDATE book_info SET is_deleted = 1 WHERE id = 1;
这样的操作将会把id为1的书籍信息标记为已删除状态,但实际上数据仍然存在于数据库中。逻辑删除的优势在于能够保留被删除数据的历史记录,并且可以方便地恢复或审计被删除的数据。
另一种删除方式是物理删除,也称为硬删除。物理删除直接从数据库表中删除某一行或某一组数据,使得这些数据在数据库中完全消失,不再占用存储空间。通常,物理删除操作使用DELETE语句来执行,如下所示:
DELETE FROM book_info WHERE id = 25;
以上操作将会永久地从数据库中移除id为25的书籍信息,而不再保留任何关于这些数据的记录。物理删除的主要优势在于能够释放数据库存储空间,但缺点是一旦删除后就无法恢复被删除的数据,且可能导致数据丢失或无法追溯。
在实际应用中,选择逻辑删除还是物理删除取决于具体业务需求和数据管理策略。需要权衡考虑数据保留和空间利用之间的平衡,以及对数据安全和恢复性的要求。
数据作为公司的重要财产,通常情况下,我们采用逻辑删除的方式来处理数据的删除操作。逻辑删除通过在数据行中增加一个标识来表示该行数据已被删除,保留了数据的历史记录,方便进行恢复或审计。
当然,在一些特殊情况下,也可以考虑采用物理删除+归档的方式来处理数据。
物理删除并归档的方式通常包括以下步骤:
创建一个与原表结构相似的新表,该新表用于记录被删除数据及其删除时间等信息。可以使用INSERT INTO SELECT语句来将原表中的数据插入到新表中,并记录删除时间等信息。示例SQL语句如下:
INSERT INTO archived_table
SELECT *, NOW() AS delete_time
FROM original_table
WHERE <condition>;
这样,就可以将原表中符合条件的数据插入到归档表中,并在归档表中记录删除时间。
物理删除+归档将插入操作和删除操作放在同一个事务中执行,以确保操作的一致性。这样可以保证在进行物理删除的同时,将数据归档到新表中,使得数据在被删除的同时也被保留下来以备后续需要。
采用物理删除并归档的方式,可以在一定程度上释放数据库的存储空间,同时也保留了被删除数据的历史记录,提高了数据的可追溯性和安全性。
物理删除+归档的方式实现有些复杂,我们就采用逻辑删除的方式。
逻辑删除的话,依然是更新逻辑,我们可以直接使用修改图书的接口。
[请求]
/book/updateBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[参数]
id=1&status=0
[响应]
"" //失败信息, 成功时返回空字符串
实现服务器代码
我们的后端代码其实已经写完了,只需要对updateBook代码进行微调。
首先添加一条状态表示“删除”:
/**
* 更新图书/删除图书
* @param bookInfo
* @return
*/
@RequestMapping("/updateBook")
public boolean updateBook(BookInfo bookInfo){
log.info("更新图书, updateBook:{}",bookInfo);
if (bookInfo.getId()<0){
return false;
}
try {
Integer result = bookService.updateBook(bookInfo);
if (result<=0){
return false;
}
return true;
}catch (Exception e){
log.error("更新图书失败, e:{}", e);
return false;
}
}
测试运行:
实现客户端代码
点击删除时,调用delete()方法,我们来完善delete方法。
前端代码已经提供了:
function deleteBook(id) {
var isDelete = confirm("确认删除?");//弹出确认框
if (isDelete) {
console.log(id);
//删除图书
$.ajax({
type: "post",
url: "/book/updateBook",
data:{
id: id,
status: 0
},
success: function(result){
alert("删除成功");
location.href = "book_list.html";
}
});
}
}
测试一下:
同时数据库内被删除的书本状态也被置于0:
批量删除
批量删除,其实就是批量修改数据。
约定前后端交互接口
[请求]
/book/batchDeleteBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[参数]
[响应]
"" //失败信息, 成功时返回空字符串
预期:点击【批量删除】按钮时只需要把复选框选中的图书的ID发送到后端即可。
因为是多个ID,我们使用 List 的形式来传递参数。
实现服务器代码
控制层
BookController
/**
* 批量删除
*/
@RequestMapping("/batchDelete")
public boolean batchDelete(List<Integer> ids){
log.info("批量删除数据, ids:{}", ids);
try {
Integer result = bookService.batchDelete(ids);
if (result<=0){
return false;
}
return true;
}catch (Exception e){
log.error("批量删除数据失败, ids:{}, e:{}", ids, e);
return false;
}
}
业务层
BookService
public Integer batchDelete(List<Integer> ids) {
return bookInfoMapper.batchDelete(ids);
}
数据层
批量删除需要用到动态SQL,一样的,建议我们初学者使用动态SQL的部分都用xml实现。
BookInfoMapper.java:
Integer batchDelete(List<Integer> ids);
BookInfoMapper.html:
<update id="batchDelete">
update book_info
SET `status`=0
where id in
<foreach collection="ids" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</update>
现在我们测试一下后端代码:
这个异常通常是因为Spring MVC试图将请求参数绑定到一个List类型的模型属性时出现问题。Spring MVC尝试将请求参数转换为List类型的对象,但在这种情况下,它无法确定如何实例化List对象,因为List是一个接口,没有默认的构造函数。
要解决这个问题,我们可以考虑以下几点:
- 确保控制器方法的参数类型正确:检查控制器方法的参数类型,特别是模型属性是否是List类型。如果是,确保我们在传递数据时正确地处理了列表参数。
- 使用具体的List实现:尝试使用具体的List实现类,例如ArrayList或LinkedList。这样Spring MVC就可以通过默认的无参构造函数实例化它们。
- 处理列表参数:如果我们的请求中包含了列表参数,确保在处理请求时正确地解析和处理了这些参数。可能需要使用@RequestParam注解或者@ModelAttribute注解来明确指定列表参数的处理方式。
- 检查Spring配置:检查我们的Spring配置文件,确保没有配置任何不必要的参数绑定或转换策略,这可能会导致参数绑定出错。
好的,我们回想起来了,当给List类型的对象传参时,确实需要使用@RequestParam注解。我们现在补上:
/**
* 批量删除
*/
@RequestMapping("/batchDelete")
public boolean batchDelete(@RequestParam List<Integer> ids){
log.info("批量删除数据, ids:{}", ids);
try {
Integer result = bookService.batchDelete(ids);
if (result<=0){
return false;
}
return true;
}catch (Exception e){
log.error("批量删除数据失败, ids:{}, e:{}", ids, e);
return false;
}
}
运行:
实现客户端代码
点击【批量删除】按钮时,需要获取到所有选中的复选框的值。
function batchDelete() {
var isDelete = confirm("确认批量删除?");
if (isDelete) {
//获取复选框的id
var ids = [];
$("input:checkbox[name='selectBook']:checked").each(function () {
ids.push($(this).val());
});
console.log(ids);
alert("批量删除成功");
$.ajax({
type: "post",
url: "/book/batchDelete?ids="+ids,
success:function(result){
if(result==true){
//删除成功
location.href = "book_list.html";
}else{
alter("删除失败, 请联系管理员");
}
}
});
}
}
重启服务器,测试一下代码:
批量删除成功!
强制登录
虽然我们已经实现了用户登录功能,但我们发现即使用户没有登录,仍然可以访问和操作图书。这样做存在极大的风险,因此我们需要进行强制登录的处理。
为了增强系统的安全性和合规性,我们需要在用户未登录时限制其对图书相关页面的访问。具体做法是,当用户尝试访问图书列表或者添加图书等页面时,如果用户尚未登录,则系统应该自动将其重定向到登录页面,要求用户先进行登录操作。
通过强制登录机制,我们可以确保只有经过认证的用户才能够访问敏感页面,从而有效地保护系统的安全性和用户数据的隐私性。这种措施能够防止未经授权的访问和操作,为系统的稳定运行和用户信息的安全提供了保障。
实现强制登录的思路可以根据已有的用户登录信息存储在Session中的情况进行判断。
实现思路:
-
当用户进行登录操作时,将用户的登录信息存储在Session中。通常,这些信息可能包括用户的ID、用户名或其他相关的身份验证信息。
-
当用户尝试访问需要登录才能访问的页面(如图书列表或添加图书页面)时,系统首先检查Session中是否存在登录用户的信息。
-
如果Session中存在登录用户的信息,则说明用户已经登录,系统可以允许用户继续访问并执行后续操作。
-
如果Session中不存在登录用户的信息,则说明用户尚未登录,系统应该将用户重定向到登录页面,要求用户进行登录操作。
-
在登录页面中,用户可以输入用户名和密码进行登录操作。登录成功后,系统会将用户的登录信息存储在Session中,并根据之前尝试访问的页面,将用户重定向回原本想要访问的页面。
通过这种实现方式,系统可以在用户尝试访问敏感页面时对用户的身份进行验证,从而保证只有经过认证的用户才能够访问相关页面,提高了系统的安全性和用户数据的保护程度。
以图书列表为例:
现在图书列表接⼝返回的内容如下:
records = null; count = null; 整个接口返回null ……
从结果上看,前端没办法确认用户是否登录了,并且后端返回数据为 null 时,前端也无法确认是后端无数据,还是后端出错了?
所以我们需要在接口的返回结果中添加新的字段,告诉前端后端的状态以及后端出错的原因。
package com.example.librarysystem.book.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class PageResult<T> {
private List<T> records;
private Integer count;
private PageRequest pageRequest;
private Integer code; //后端响应状态,业务状态码 200-成功, -1-失败, -2-表示未登录
private String errmsg; //后端发生错误的原因
}
业务状态码是指在业务逻辑处理过程中返回给客户端的状态码,用于表示业务操作的结果或状态。这些状态码通常是自定义的,根据具体业务需求定义的一系列状态码,用于指示业务操作的成功、失败或其他特定情况。
业务状态码的例子:
-
200 - 成功:表示业务操作成功。例如,当用户成功创建一个新的资源时,服务器可以返回状态码200。
-
400 - 请求错误:表示客户端发送的请求有误,服务器无法理解。例如,当客户端提交的数据格式不正确或者缺少必要的参数时,服务器可以返回状态码400。
-
401 - 未授权:表示客户端未经授权访问请求的资源。例如,当用户未登录或者登录信息不正确时,服务器可以返回状态码401。
-
403 - 禁止访问:表示服务器拒绝了客户端的请求。例如,当用户尝试访问未授权的资源时,服务器可以返回状态码403。
-
404 - 资源不存在:表示请求的资源不存在。例如,当客户端请求一个不存在的URL时,服务器可以返回状态码404。
-
500 - 服务器内部错误:表示服务器在处理请求时发生了意外错误。例如,当服务器在执行业务逻辑时出现了异常,无法完成请求处理时,可以返回状态码500。
-
-1 - 操作失败:表示业务操作失败,但失败的原因可能是多种情况,需要进一步的信息来确定。这个状态码可以用作通用的操作失败情况。(发生错误了,但是后端进行了捕获,比如参数传递异常)。
-
-2 - 未知错误:表示发生了未知的错误,通常是指在处理业务逻辑时出现了意外的异常或未能预料的错误情况。这个状态码用于表示无法识别或处理的错误情况。
这些负数状态码通常被用于区分正常的 HTTP 状态码,并且可以根据具体的业务需求来定义和解释。在使用时,需要确保对这些状态码进行了明确的定义和文档说明,以便客户端能够正确地理解和处理这些状态码。
HTTP状态码是指由HTTP协议定义的一组状态码,用于表示HTTP请求的处理结果。这些状态码由服务器返回给客户端,以便客户端了解请求的处理情况。HTTP状态码通常由三位数字组成,分为五个不同的类别,例如成功、重定向、客户端错误、服务器错误等。
两者的关系是,业务状态码可以作为HTTP响应的一部分返回给客户端,以提供关于业务操作结果的更详细的信息。例如,当客户端向服务器发起业务请求时,服务器可以根据业务逻辑处理结果返回相应的业务状态码,并将其包含在HTTP响应中的数据中。这样,客户端在接收到HTTP响应后,不仅可以根据HTTP状态码判断请求是否成功,还可以根据业务状态码进一步了解业务操作的结果。
它们的区别在于,HTTP状态码是由HTTP协议定义的,用于表示HTTP请求处理的结果;而业务状态码是根据具体业务需求自定义的,用于表示业务操作的结果或状态。HTTP状态码通常与HTTP协议交互相关,而业务状态码则更侧重于业务逻辑的处理结果。
在业务中,响应结果的前提是HTTP响应成功。这是因为在HTTP协议中,只有当服务器成功处理了客户端的请求并返回了正确的HTTP响应时,客户端才能够获取到服务器返回的业务响应结果。因此,HTTP响应的成功与否是业务响应结果的前提。
同理,
返回结果是图书信息,又存在2种情况:
用户未登录,返回null;用户登录,数据存在,返回BookInfo,数据不存在,返回null。
这个时候前端还是无法区分到底是用户未登录还是数据不存在,前者需要跳转到登录界面吗,后者则是正常现象,前端不展示数据即可。
这样子我们的BookInfo也得去加字段,告知前端响应情况……
我们不如直接将其包装成一个model——Result,所有的接口我们都返回这样的一个对象:
package com.example.librarysystem.book.model;
import lombok.Data;
@Data
public class Result<T> {
private Integer code;//后端响应状态, 业务状态码 200-成功, -1失败, -2表示未登录
private String errmsg;//后端发生错误的原因
private T data;
}
同时我们的接口恢复原样:
接下来修改BookController:
@RequestMapping("/getListByPage")
public Result<PageResult<BookInfo>> getListByPage(PageRequest pageRequest){
log.info("查询列表信息, pageRequest:{}", pageRequest);
if (pageRequest.getCurrentPage()<1){
Result result = new Result();
result.setCode(-1);
result.setErrmsg("非法参数!");
return null;
}
PageResult<BookInfo> pageResult = bookService.getListByPage(pageRequest);
Result result = new Result();
result.setData(pageResult);
result.setCode(200);
return result;
}
剩下的代码也同理一一修改。
但是不得不说实在是太麻烦了!
我们继续提取代码:
package com.example.librarysystem.book.model;
import lombok.Data;
@Data
public class Result<T> {
private Integer code;//后端响应状态, 业务状态码 200-成功, -1失败, -2表示未登录
private String errmsg;//后端发生错误的原因
private T data;
/**
* 成功时
*/
public static <T> Result<T> success(T data){
Result<T> result = new Result<T>();
result.setData(data);
result.setCode(200);
return result;
}
/**
* 失败时
*/
public static <T> Result<T> fail(T data, String errMsg){
Result<T> result = new Result<T>();
result.setData(data);
result.setCode(-1);
result.setErrmsg(errMsg);
return result;
}
public static <T> Result<T> fail(String errMsg){
Result<T> result = new Result<T>();
result.setCode(-1);
result.setErrmsg(errMsg);
return result;
}
/**
* 未登录时
*/
public static <T> Result<T> unlogin(){
Result<T> result = new Result<T>();
result.setCode(-2);
result.setErrmsg("用户未登录");
return result;
}
}
getListByPage方法就可以简化成:
@RequestMapping("/getListByPage")
public Result<PageResult<BookInfo>> getListByPage(PageRequest pageRequest){
log.info("查询列表信息, pageRequest:{}", pageRequest);
if (pageRequest.getCurrentPage()<1){
return Result.fail("非法参数");
}
PageResult<BookInfo> pageResult = bookService.getListByPage(pageRequest);
return Result.success(pageResult);
}
queryBookById也可以改改:
@RequestMapping("/queryBookById")
public Result<BookInfo> queryBookById(Integer bookId){
log.info("查询图书信息, bookId:{}", bookId);
// 参数校验
if (bookId < 1){
log.error("非法图书ID, bookId:{}", bookId);
return Result.fail("非法图书ID");
}
BookInfo bookInfo = bookService.queryBookById(bookId);
if (bookInfo == null) {
return Result.fail("未找到对应图书");
}
return Result.success(bookInfo);
}
接下来我们去修改服务器和客⼾端代码。
实现服务器代码
修改图书列表接口,进行登录校验。
先回顾一下UserController里面的session是怎么存的:
package com.example.librarysystem.book.controller;
import com.example.librarysystem.book.model.UserInfo;
import com.example.librarysystem.book.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public boolean login(String userName, String password, HttpSession session) {
//校验参数
if (!StringUtils.hasLength(userName) || (!StringUtils.hasLength(password))) {
return false;
}
//校验密码是否正确
//存Session
session.setAttribute("userName", userName);
//判断数据库的密码和用户输入的密码是否一致
//查询数据库, 得到数据库的密码
UserInfo userInfo = userService.queryByName(userName);
if (userInfo == null) {
return false;
}
if (password.equals(userInfo.getPassword())) {
userInfo.setPassword("");
//密码正确
session.setAttribute("user_session", userInfo);
return true;
}
return false;
}
}
现在修改:
@RequestMapping("/getListByPage")
public Result<PageResult<BookInfo>> getListByPage(PageRequest pageRequest, HttpSession session){
log.info("查询列表信息, pageRequest:{}", pageRequest);
//验证用户是否登录
UserInfo userInfo = (UserInfo) session.getAttribute("user_session");
if(userInfo==null || userInfo.getId()<1){
return Result.unlogin();
}
if (pageRequest.getCurrentPage()<1){
return Result.fail("非法参数");
}
PageResult<BookInfo> pageResult = bookService.getListByPage(pageRequest);
return Result.success(pageResult);
}
但是,对于"user_session"这种固定的字符串,特别是其他地方可能会使用的,我们通常定义为常量。JavaSE里面可能会这么定义:
但是也不算简单,因为我们得到每个类里面都定义一遍……
如果修改常量 session 的 key,就需要修改所有使用到这个key的地方,出于高内聚低耦合的思想,我们把常量集中在⼀个类里面,创建常量类: Constants
package com.example.librarysystem.book.model;
public class Constants {
public static final String USER_SESSION_KEY = "user_session";
}
常量名的命名规则是:
全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要在意名字长度。常量名应该尽可能详尽地描述其含义,以便在代码中易于理解和使用。即使这意味着常量名会稍长,也应该优先选择清晰的表达方式。
- MAX_STOCK_COUNT:表示最大库存数量。
- CACHE_EXPIRED_TIME:表示缓存过期时间。
@RequestMapping("/getListByPage")
public Result<PageResult<BookInfo>> getListByPage(PageRequest pageRequest, HttpSession session){
log.info("查询列表信息, pageRequest:{}", pageRequest);
//验证用户是否登录
UserInfo userInfo = (UserInfo) session.getAttribute(Constants.USER_SESSION_KEY);
if(userInfo==null || userInfo.getId()<1){
return Result.unlogin();
}
if (pageRequest.getCurrentPage()<1){
return Result.fail("非法参数");
}
PageResult<BookInfo> pageResult = bookService.getListByPage(pageRequest);
return Result.success(pageResult);
}
package com.example.librarysystem.book.controller;
import com.example.librarysystem.book.model.Constants;
import com.example.librarysystem.book.model.UserInfo;
import com.example.librarysystem.book.service.UserService;
import org.apache.tomcat.util.bcel.Const;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public boolean login(String userName, String password, HttpSession session) {
//校验参数
if (!StringUtils.hasLength(userName) || (!StringUtils.hasLength(password))) {
return false;
}
//校验密码是否正确
//存Session
session.setAttribute("userName", userName);
//判断数据库的密码和用户输入的密码是否一致
//查询数据库, 得到数据库的密码
UserInfo userInfo = userService.queryByName(userName);
if (userInfo == null) {
return false;
}
if (password.equals(userInfo.getPassword())) {
userInfo.setPassword("");
//密码正确
session.setAttribute(Constants.USER_SESSION_KEY, userInfo);
return true;
}
return false;
}
}
我们现在测试一下接口:
现在登录一下再测试:
实现客户端代码
function getBookList() {
$.ajax({
type: "get",
url: "/book/getListByPage"+location.search,
success: function (result) {
if(result.code == -2){
confirm("用户未登录,请先登录");
location.href = "login.html";
return;
}
if(result.code == -1){
alert("发生内部错误,请联系管理员");
return;
}
var data = result.data;
var books = data.records;
console.log(books);
var finalHtml = "";
for(var book of books){
//拼接html
finalHtml +='<tr>';
finalHtml +='<td><input type="checkbox" name="selectBook" value="'+book.id+'" id="selectBook" class="book-select"></td>';
finalHtml +='<td>'+book.id+'</td>';
finalHtml +='<td>'+book.bookName+'</td>';
finalHtml +='<td>'+book.author+'</td>';
finalHtml +='<td>'+book.count+'</td>';
finalHtml +='<td>'+book.price+'</td>';
finalHtml +='<td>'+book.publish+'</td>';
finalHtml +='<td>'+book.stateCN+'</td>';
finalHtml +='<td>';
finalHtml +='<div class="op">';
finalHtml +='<a href="book_update.html?bookId='+book.id+'">修改</a>';
finalHtml +='<a href="javascript:void(0)" onclick="deleteBook('+book.id+')">删除</a>';
finalHtml +='</div>';
finalHtml +='</td>';
finalHtml +='</tr>';
}
$("tbody").html(finalHtml);
//翻页信息
$("#pageContainer").jqPaginator({
totalCounts: data.count, //总记录数
pageSize: 10, //每页的个数
visiblePages: 7, //可视页数
currentPage: data.pageRequest.currentPage, //当前页码
first: '<li class="page-item"><a class="page-link">首页</a></li>',
prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',
next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',
last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',
page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',
//页码点击时都会执行
onPageChange: function (page, type) {
console.log("第" + page + "页, 类型:" + type);
if(type=="change"){
location.href = "book_list.html?currentPage="+page;
}
}
});
}
});
}
重启后测试:
好了……你以为结束了吗?
当然没有……
对于强制登录的模块,我们目前只实现了一个图书列表功能。然而,这个模块还需要实现图书修改、图书删除等接口。如果应用程序的功能继续增加,按照目前的方法逐一实现接口会非常耗时,并且容易出错。
有没有更简单的处理办法呢?接下来,我们将学习Spring Boot对于这种“统一问题”的处理方式。
我们设计算法遵循的是一种共性下沉、个性提取的设计思想。
共性下沉指的是将各个功能模块中共性的部分抽取出来,形成通用的功能或组件,以便在整个应用程序中共享和复用。在这个例子中,我们可以将登录验证的功能从每个接口中独立出来,形成一个统一的登录验证模块,以确保每个接口在调用之前都进行了登录验证。
个性提取则是指针对不同功能模块中的特定需求,提取出个性化的处理逻辑,使得每个模块能够根据自身的特点进行定制化的处理。对于图书修改、图书删除等接口,我们可以针对它们的具体功能需求进行个性化的实现,同时利用共性下沉的方式,将登录验证等通用功能模块引入,以保证整体的一致性和可维护性。
通过共性下沉、个性提取的设计方式,我们能够更加有效地管理和扩展应用程序的功能,提高开发效率并降低出错的风险。在接下来学习Spring Boot的过程中,我们将探讨如何利用其提供的便捷功能来实现这种设计理念,从而更好地满足应用程序的需求。