1. 添加“类别”的业务
在项目的根包下创建pojo.dto.CategoryAddNewDTO
类:
@Data
public class CategoryAddNewDTO implements Serializable {
private String name;
private Long parentId;
private String keywords;
private Integer sort;
private String icon;
private Integer enable;
private Integer isDisplay;
}
在项目的根包下创建service.ICategoryService
接口:
public interface ICategoryService {
void addNew(CategoryAddNewDTO categoryAddNewDTO);
}
在项目的根包下创建service.impl.CategoryServiceImpl
类,实现以上接口,并添加@Service
注解:
@Service
public class CategoryServiceImpl implements ICategoryService {
@Autowired
private CategoryMapper categoryMapper;
@Override
public void addNew(CategoryAddNewDTO categoryAddNewDTO) {
// 调用Mapper对象的【根据名称统计数量】方法进行统计
// 判断统计结果是否大于0
// 是:名称已经被占用,抛出异常(CONFLICT)
// 声明局部变量depth,默认值为1
// 取出参数中的parentId
// 判断parentId是否不为0
// 是:调用Mapper对象的【根据id查询详情】,使用parentId作为参数,执行查询
// -- 判断查询结果是否不为null
// -- 是:局部变量depth=父级depth+1
// -- 否:父级类别不存在,抛出异常(NOT_FOUND)
// 创建Category实体类的对象
// 将参数DTO的各属性值复制到Category实体类对象中
// 补全Category实体类对象的属性:depth
// 补全Category实体类对象的属性:is_parent:固定为0
// 调用Mapper对象的方法,将数据插入到数据库,并获取返回值
// 判断返回值是否不为1
// 是:抛出异常(ERR_INSERT)
// 判断parentId是否不为0
// 是:判断父级类别的isParent是否为0
// -- 是:创建新的Category对象,封装:parentId,isParent(1)
// -- -- 调用Mapper对象的【更新】方法,执行修改数据,并获取返回值
// -- -- 判断返回值是否不为1
// -- -- -- 是:抛出异常(ERR_UPDATE)
}
}
具体实现为:
@Override
public void addNew(CategoryAddNewDTO categoryAddNewDTO) {
// 调用Mapper对象的【根据名称统计数量】方法进行统计
String name = categoryAddNewDTO.getName();
int count = categoryMapper.countByName(name);
// 判断统计结果是否大于0
if (count > 0) {
// 是:名称已经被占用,抛出异常(CONFLICT)
String message = "添加类别失败,尝试添加的类别名称【" + name + "】已经存在!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_CONFLICT, message);
}
// 声明父级类别对象
CategoryStandardVO parentCategory = null;
// 声明局部变量depth,默认值为1
Integer depth = 1;
// 取出参数中的parentId
Long parentId = categoryAddNewDTO.getParentId();
// 判断parentId是否不为0
if (parentId != 0) {
// 是:调用Mapper对象的【根据id查询详情】,使用parentId作为参数,执行查询
parentCategory = categoryMapper.getStandardById(parentId);
// 判断查询结果是否不为null
if (parentCategory != null) {
// -- 是:局部变量depth=父级depth+1
depth += parentCategory.getDepth();
} else {
// -- 否:父级类别不存在,抛出异常(NOT_FOUND)
String message = "添加类别失败,选定的父级类别不存在!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_NOT_FOUND, message);
}
}
// 创建Category实体类的对象
Category category = new Category();
// 将参数DTO的各属性值复制到Category实体类对象中
BeanUtils.copyProperties(categoryAddNewDTO, category);
// 补全Category实体类对象的属性:depth
category.setDepth(depth);
// 补全Category实体类对象的属性:isParent:固定为0
category.setIsParent(0);
// 调用Mapper对象的方法,将数据插入到数据库,并获取返回值
int rows = categoryMapper.insert(category);
// 判断返回值是否不为1
if (rows != 1) {
// 是:抛出异常(ERR_INSERT)
String message = "添加类别失败,服务器忙,请稍后再尝试!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_INSERT, message);
}
// 判断parentId是否不为0
if (parentId != 0) {
// 是:判断父级类别的isParent是否为0
if (parentCategory.getIsParent() == 0) {
// 是:创建新的Category对象,封装:parentId,isParent(1)
Category updateParentCategory = new Category();
updateParentCategory.setId(parentId);
updateParentCategory.setIsParent(1);
// 调用Mapper对象的【更新】方法,执行修改数据,并获取返回值
int updateRows = categoryMapper.updateById(updateParentCategory);
// 判断返回值是否不为1
if (updateRows != 1) {
// 是:抛出异常(ERR_UPDATE)
String message = "添加类别失败,服务器忙,请稍后再尝试!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_UPDATE, message);
}
}
}
}
完成后,测试:
package cn.tedu.csmall.product.service;
import cn.tedu.csmall.product.ex.ServiceException;
import cn.tedu.csmall.product.pojo.dto.CategoryAddNewDTO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@Slf4j
@SpringBootTest
public class CategoryServiceTests {
@Autowired
ICategoryService service;
@Test
void testAddNew() {
CategoryAddNewDTO categoryAddNewDTO = new CategoryAddNewDTO();
categoryAddNewDTO.setName("休闲衬衣");
categoryAddNewDTO.setParentId(23L);
try {
service.addNew(categoryAddNewDTO);
log.debug("添加成功!");
} catch (ServiceException e) {
log.debug("serviceCode : " + e.getServiceCode());
log.debug("message : " + e.getMessage());
}
}
}
2. 删除“类别”的业务
在CategoryMapper.java
接口中添加新的抽象方法:
int countByParentId(Long parentId);
在CategoryMapper.xml
中配置SQL:
<select id="countByParentId" resultType="int">
SELECT count(*) FROM pms_category WHERE parent_id=#{parentId}
</select>
在CategoryMapperTests
中编写并执行测试:
@Test
void testCountByParentId() {
Long parentId = 1L;
int count = mapper.countByParentId(parentId);
log.debug("根据父级id【{}】统计数量完成,数量:", parentId, count);
}
在ICategoryService
接口中添加抽象方法:
void deleteById(Long id);
在CategoryServiceImpl
类中实现以上抽象方法:
@Override
public void deleteById(Long id) {
// 调用Mapper对象的【根据id查询详情】查询数据,是当前尝试删除的数据
// 判断查询结果是否为null
// 是:数据不存在,抛出异常(ERR_NOT_FOUND)
// 检查当前尝试删除的类别是否存在子级类别:判断以上查询结果的isParent是否为1
// 是:当前尝试删除的类别“是父级类别”(包含子级),抛出异常(ERR_CONFLICT)
// 调用Mapper对象的【根据id删除】执行删除,并获取返回值
// 判断返回值是否不为1
// 是:抛出异常(ERR_DELETE)
// ====== 如果这是父级类别中的最后一个子级,则将父级的isParent改为0 =====
// 从当前尝试删除的类别对象中取出parentId
// 调用Mapper对象的countByParentId(parentId)进行统计
// 判断统计结果是否为0
// 创建新的Category对象,用于更新父级,此Category对象中需要封装:id(parentId),isParent(0)
// 调用Mapper对象的【更新】功能,执行修改数据,并获取返回值
// 判断返回值是否不为1
// 是:抛出异常(ERR_UPDATE)
}
具体实现为:
@Override
public void deleteById(Long id) {
// 调用Mapper对象的【根据id查询详情】查询数据,是当前尝试删除的数据
CategoryStandardVO currentCategory = categoryMapper.getStandardById(id);
// 判断查询结果是否为null
if (currentCategory == null) {
// 是:数据不存在,抛出异常(ERR_NOT_FOUND)
String message = "删除类别失败,尝试删除的类别不存在!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_NOT_FOUND, message);
}
// 检查当前尝试删除的类别是否存在子级类别:判断以上查询结果的isParent是否为1
if (currentCategory.getIsParent() == 1) {
// 是:当前尝试删除的类别“是父级类别”(包含子级),抛出异常(ERR_CONFLICT)
String message = "删除类别失败,尝试删除的类别仍包含子级类别!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_CONFLICT, message);
}
// 调用Mapper对象的【根据id删除】执行删除,并获取返回值
int rows = categoryMapper.deleteById(id);
// 判断返回值是否不为1
if (rows != 1) {
// 是:抛出异常(ERR_DELETE)
String message = "删除类别失败,服务器忙,请稍后再尝试!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_DELETE, message);
}
// ====== 如果这是父级类别中的最后一个子级,则将父级的isParent改为0 =====
// 从当前尝试删除的类别对象中取出parentId
Long parentId = currentCategory.getParentId();
// 判断当前类别是否不为1级类别,即parentId不为0
if (parentId != 0) {
// 调用Mapper对象的countByParentId(parentId)进行统计
int count = categoryMapper.countByParentId(parentId);
// 判断统计结果是否为0
if (count == 0) {
// 创建新的Category对象,用于更新父级,此Category对象中需要封装:id(parentId),isParent(0)
Category parentCategory = new Category();
parentCategory.setId(parentId);
parentCategory.setIsParent(0);
// 调用Mapper对象的【更新】功能,执行修改数据,并获取返回值
rows = categoryMapper.updateById(parentCategory);
// 判断返回值是否不为1
if (rows != 1) {
// 是:抛出异常(ERR_UPDATE)
String message = "删除类别失败,服务器忙,请稍后再尝试!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_UPDATE, message);
}
}
}
}
完成后,测试:
@Test
void testDeleteById() {
Long id = 18L;
try {
service.deleteById(id);
log.debug("删除成功!");
} catch (ServiceException e) {
log.debug("serviceCode : " + e.getServiceCode());
log.debug("message : " + e.getMessage());
}
}
3. 事务(Transaction)
事务是数据库中的可以保证多个(至少2个)写操作(增、删、改)要么全部执行成功,要么全部执行失败的机制!
在基于Spring JDBC的项目中,使用@Transactional
注解,即可使得注解的方法是事务性的。
关于@Transactional
注解,可以添加在:
- 接口上
- 等效于在每个抽象方法上添加了此注解
- 接口的抽象方法上
- 仅作用于当前方法(重写的方法运行时)
- 实现类上
- 等效于在每个重写的接口的方法上添加了此注解
- 实现类中重写的接口的方法上
- 仅作用于当前方法
提示:此注解可以配置一些参数,如果同时在接口/类、接口的抽象方法/类重写的方法上添加此注解并配置了不同的参数值,则以方法上的配置为准。
注意:Spring JDBC是基于接口的代理模式来实现事务管理的!所以,如果在实现类中的自定义方法上添加@Transactional
注解是错误的做法!仅当对应的方法是在业务接口中已经声明的,使用@Transactional
才是正确的!
关于事务处理过程中的几个概念:
- 开启事务:BEGIN
- 提交事务:COMMIT
- 回滚事务:ROLLBACK
在Spring JDBC的事务管理中,大致是:
开启事务
try {
执行你的业务方法
提交事务
} catch (RuntimeException e) {
回滚事务
}
可以看到,Spring JDBC的事务管理中,默认将根据RuntimeException
进行回滚,可以通过@Transactional
注解的rollbackFor
/ rollbackForClassName
这2个属性中的某1个进行修改,设置为对特定的异常进行回滚,还可以配置noRollbackFor
/ noRollbackForClassName
这2个属性,设置对特定的异常不回滚。
【小结】关于事务:
- 如果某个业务方法涉及超过1次的增、删、改操作,需要保证此业务方法是事务性的;
- 推荐在业务的抽象方法上添加
@Transactional
注解即可保证此业务方法是事务性- 对于初学者,更推荐在业务接口上添加
@Transactional
,则此接口中所有抽象方法都是事务性,可能其中的某些抽象方法并不需要是事务性的,但是,这种做法比较稳妥,能避免遗漏导致的错误
- 对于初学者,更推荐在业务接口上添加
- 为了保证事务能够按照预期进行回滚,需要:
- 业务层必须由接口和实现类组成
- Spring JDBC是基于接口代理模式实现事务管理的,如果没有接口,则无法实现
- 所有增、删、改操作完成后,应该及时获取返回结果,并对结果进行判断,如果结果不符合预期,应该抛出异常,且异常应该是
RuntimeException
或其子孙类异常- Spring JDBC在管理事务时,默认按照
RuntimeException
进行回滚
- Spring JDBC在管理事务时,默认按照
- 业务层必须由接口和实现类组成
4. 关于@RequestMapping
注解
在Spring MVC框架中,@RequestMapping
注解的主要作用是:绑定“请求路径”与“处理请求的方法”的映射关系。
关于此注解的声明的源代码:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
}
以上源代码中的@Target({ElementType.TYPE, ElementType.METHOD})
就表示了此注解可以添加在哪里,其中,ElementType.TYPE
表示可以添加在“类型”上,ElementType.METHOD
表示可以添加在方法上。
在控制器类上,添加@RequestMapping
配置的路径值,将作为当前控制器类中每个处理请求的路径的前缀部分!例如在类上配置为@RequestMapping("/brands")
,在方法上配置为@RequestMapping("/delete")
,则完整路径为/brands/delete
,并且,此控制器类中所有处理请求的方法的路径上都有/brands
前缀!
所以,强烈推荐在每个控制器类上都使用@RequestMapping
配置路径的前缀部分。
关于注解内部的源代码,以@RequestMapping
为例,其内部有:
String name() default "";
以上源代码中,name()
表示此注解有名为name
的属性,左侧的String
表示此属性的数据类型,右侧的default ""
表示此属性的默认值。例如:可以在@RequestMapping
中配置名为name
的属性:
@RequestMapping(name = "test")
在@RequestMapping
中,还有:
@AliasFor("path")
String[] value() default {};
则以上源代码表示:此注解中有名为value
的属性,值是String[]
类型的,则默认值是空数组。
在Java语言中,注解的value
属性是默认的属性,如果注解只需要配置这1个属性的值,则可以不必显式的声明属性名称,例如:
@RequestMapping(value = {"value1", "value2", "value3"})
@RequestMapping({"value1", "value2", "value3"})
以上2种配置是完全等效的!
在Java语言中,如果某个属性的值是某种数组类型,但是,配置的数组值只有1个元素,则不必使用大括号将其框住,例如:
@RequestMapping(value = {"value1"})
@RequestMapping(value = "value1")
以上2种配置是完全等效的!
关于源代码中value
属性的声明,还添加了@AliasFor("path")
,表示此value
属性等效于当前注解中的path
属性!
在@RequestMapping
注解的源代码中,还有:
RequestMethod[] method() default {};
以上注解的作用是“限制请求方式”,在没有配置此属性的情况,任何请求方式都是允许的!例如:
@RequestMapping(value = "/add-new", method = RequestMethod.POST)
如果使用以上配置,则/add-new
路径只允许通过POST
方式提交请求,如果使用其它请求方式,将出现405
错误!
强制推荐将所有请求都限制请求方式!
在Spring MVC框架中,还定义了相关注解,以简化限制请求方式的配置:
@GetMapping
@PostMapping
PutMapping
DeleteMapping
PatchMapping
以上这些都相当于是限制了请求方式的@RequestMapping
!
小结:
- 推荐在每个控制器类上使用
@RequestMapping
配置请求路径前缀 - 推荐在每个处理请求的方法上使用
@GetMapping
/@PostMapping
配置请求路径
作业
完成以下页面设计:
1. 添加类别
菜单位置:临时页面
> 添加类别
文件名:/src/views/sys-admin/temp/CategoryAddNewView.vue
访问路径:/sys-admin/temp/category/add-new
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KfXeIDKv-1662131094759)(images/image-20220830175113683.png)]
2. 添加品牌
菜单位置:临时页面
> 添加品牌
文件名:/src/views/sys-admin/temp/BrandAddNewView.vue
访问路径:/sys-admin/temp/brand/add-new
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lgCDkdBw-1662131094762)(images/image-20220830175357675.png)]