简单的设备管理系统
码云代码地址:https://gitee.com/LuckyYusc/java_springboot_project
1、拥有功能和效果展示
1.1 功能
- 添加设备、部门(部门与设备为一对多的关系)
- 删除设备
- 更新设备、部门
- 分页
- 按指定的字段排序
- 按设备名称查找(模糊查询)设备,且同样能分页和排序
- 通过部门id查设备
1.2 效果展示
2、技术和环境搭建
2.1 技术:Springboot + JPA + thymeleaf + MySQL + BootStrap
- JDK: 17.0.4
- spring-boot-starter-parent: 3.1.0
- spring-thymeleaf-project: 0.0.1-SNAPSHOT
- Maven
- Spring Data JPA
2.2 依赖选择:
2.2 项目结构:
2.4 配置数据库连接:
3、具体实现过程
3.1 model层
Model层
:Model层是应用程序的核心部分,负责处理业务逻辑和数据操作。它包含了应用程序的实体类、业务逻辑和数据访问对象(DAO)。Model层的主要职责包括封装业务逻辑、数据持久化、数据验证和转换以及业务逻辑处理等。
Device
:
// An highlighted block
package com.example.springthymeleafproject.model;
import jakarta.persistence.*;
import lombok.Data;
@Entity
@Data
@Table(name = "devices")
public class Device {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "type")
private String type;
@Column(name = "status")
private String status;
@Column(name = "equipment_model")
private String equipmentModel;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dept_id")
private Department department;
}
Department
:
package com.example.springthymeleafproject.model;
import jakarta.persistence.*;
import lombok.Data;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Entity
@Data
@Table(name = "departments")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
// 一对多单向映射
// OneToMany的默认获取类型:LAZY
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "department")
private Set<Device> deviceItems = new HashSet<>();
}
Device
类创建了6个属性,其中id为自增的,dept_id与Department进行关联。
关联解释:
- @ManyToOne表示多对一的关系,即一个Department对象可以对应多个其他实体对象(在此处未给出具体实体类),而这些实体对象都是属于同一个Department对象。这表示在数据库中,Department实体类的一条记录可以关联多个其他实体类的记录。
- fetch = FetchType.LAZY表示在加载实体对象时,关联的department属性使用延迟加载方式来获取。也就是说,只有当访问department属性时,才会从数据库中加载相关的Department对象。
- @JoinColumn(name = “dept_id”)指定了在数据库中存储关联关系的字段名为dept_id,这表示在与关联对象建立关系时,会在当前实体类的表中创建一个名为dept_id的外键来与Department表建立关联。
Department
类中主要有两个数据,同样的id为自增,必要重要的是需要设置与Device关联,这里很重要,对后续的通过部门id查设备有很关键的作用。
关联解释:
- @OneToMany 注解表示一个Department实体对象可以对应多个Device实体对象,建立了一对多的关系。它的mappedBy = "department"参数指定了在Device实体类中,通过哪个属性与Department实体类建立关系,这里是使用了名为"department"的属性。
- cascade = CascadeType.ALL 表示级联操作,所有与Department实体对象相关的Device实体对象的增删改操作都会被级联到数据库中。这意味着当新增、修改或删除Department实体对象时,相关的Device实体对象也会相应地被新增、修改或删除。
- fetch = FetchType.LAZY 表示在加载Department实体对象时,与之关联的Device实体对象使用延迟加载方式获取。也就是说,只有当访问department属性的deviceItems集合时,才会从数据库中加载相关的Device实体对象。相比于即时加载(EAGER),延迟加载可以减少不必要的数据库查询,提高性能。
- private Set deviceItems = new HashSet<>() 表示Department实体类中的一个属性,用来存储与该Department相关的Device实体类对象。这里使用了Set集合来存储Device对象,Set集合的特点是不允许重复的元素。通过使用Set集合,可以确保一个Department和它关联的Device对象之间的关系是唯一的。
3.2 repository层
Repository
层是负责与数据存储进行交互的层。它包含了数据访问对象和数据存储的相关操作。Repository层的主要职责是封装对数据存储的增删改查操作,提供了统一的接口给其他层进行数据访问。它可以与数据库、文件系统或其他数据存储进行交互。
DeviceRepository
:
package com.example.springthymeleafproject.repository;
import com.example.springthymeleafproject.model.Device;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
//它主要用于告诉Spring容器需要在该类中提供访问数据库的方法,并将该类扫描到Spring容器的上下文中。
public interface DeviceRepository extends JpaRepository<Device, Long> {
//我们在 DeviceRepository 接口中定义了一个方法 findByNameContainingIgnoreCase,它会根据设备名称进行模糊查询,并且忽略大小写。该方法还接受一个 Pageable 对象作为参数,用于实现分页和排序。
Page<Device> findByNameContainingIgnoreCase(String deviceName, Pageable pageable);
// Spring Data JPA 提供了一种基于方法命名约定的方式,它会根据方法名自动解析查询,并生成相应的 SQL 查询语句,从而实现查询的功能。在命名约定中,
// 关键词 Containing 表示模糊查询,关键词 IgnoreCase 表示忽略大小写
//通过部门的id查设备
Page<Device> findByDepartmentId(Long departmentId, Pageable pageable);
//在具体的实现类中,你只需要声明这个方法即可,而不需要实现它。Spring Data JPA 会自动根据命名约定以及实体类的定义,生成相应的查询代码
//接下来,在的 DeviceServiceImpl 类中,可以直接调用 deviceRepository.searchByNamePaginated(deviceName, pageable) 方法
// 来执行模糊查询并返回带分页和排序的结果。
}
DepartmentRepository
:
package com.example.springthymeleafproject.repository;
import com.example.springthymeleafproject.model.Department;
import org.springframework.data.jpa.repository.JpaRepository;
public interface DepartmentRepository extends JpaRepository<Department, Long> {
}
我们在 DeviceRepository
接口中定义了一个方法 findByNameContainingIgnoreCase
,它会根据设备名称进行模糊查询
,并且忽略大小写
。该方法还接受一个 Pageable 对象作为参数,用于实现分页和排序。
JpaRepository
是 Spring Data JPA
提供的一个通用的仓库接口,用于对实体对象进行持久化操作。它提供了许多常用的数据库操作方法,如保存实体、删除实体、查询实体等。通过继承 JpaRepository 接口
,DepartmentRepository 接口获得了这些方法的自动实现
代码解释
:
- findByNameContainingIgnoreCase
需要传入一个设备名和Pageable,从而能在实现查询的同时,让其能够实现分页等功能!
Spring Data JPA 提供了一种基于方法命名约定的方式,它会根据方法名自动解析查询,并生成相应的 SQL 查询语句,从而实现查询的功能。
在命名约定中,关键词 Containing 表示模糊查询
,关键词 IgnoreCase 表示忽略大小写
是实现模糊查询非常关键的一步
- findByDepartmentId
主要用来实现通过部门的id查设备,原理基本一样。
3.3service层
Service
层是实现业务逻辑的层,它通常被Controller层调用来完成具体的业务操作。Service层用于封装复杂的业务逻辑,协调Model层和Repository层。它包含了应用程序的服务类(Service)和相关的业务方法。Service层的主要职责是处理业务逻辑,组织数据操作和协调各个组件的工作。
一个实体类定义一个接口来确定具体的业务。在IMPL来写具体的业务逻辑。
DeviceService
:
package com.example.springthymeleafproject.service;
import com.example.springthymeleafproject.model.Department;
import com.example.springthymeleafproject.model.Device;
import org.springframework.data.domain.Page;
import java.util.List;
public interface DeviceService {
//获取所有设备
List<Device> getAllDevice();
//新增设备
void saveDevice(Device device);
//获取指定ID的的设备
Device getDeviceId(long id);
//删除指定ID的设备
void deleteDeviceById(long id);
//带有排序功能的分页
/*
pageNO 页码
pageSize 当前页面存放多少个数据
sortField 排序的字段
sortDirection 排序规则
Page<Device> findDevicePaginated(int pageNo, int pageSize, String sortField, String sortDirection);
*/
//带有排序、查询功能的分页
Page<Device> searchDevicesPaginated(int pageNo, int pageSize, String sortField, String sortDirection, String deviceName);
//带有排序、通过部门查设备的分页(通过输入部门id来查)
Page<Device> searchDevicesByDepartmentIdPaginated(int pageNo, int pageSize, String sortField, String sortDirection, Long departmentId);
}
DeviceServiceImpl
:
package com.example.springthymeleafproject.service;
import com.example.springthymeleafproject.model.Device;
import com.example.springthymeleafproject.repository.DeviceRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class DeviceServiceImpl implements DeviceService {
@Autowired
private DeviceRepository deviceRepository;
//所有设备信息
@Override
public List<Device> getAllDevice() {
return deviceRepository.findAll();
}
//新增设备
@Override
public void saveDevice(Device device) {
this.deviceRepository.save(device);
}
//获取指定id的设备
@Override
public Device getDeviceId(long id) {
Optional<Device> optional = deviceRepository.findById(id);
Device device = null;
//如果存在指定的id的设备,则赋值给device,不存在则抛出异常
if (optional.isPresent()) {
device = optional.get();
} else {
throw new RuntimeException("找不到指定设备的id : " + id);
}
return device;
}
//删除指定id的设备
@Override
public void deleteDeviceById(long id) {
this.deviceRepository.deleteById(id);
}
//带有排序、查询功能的分页
@Override
public Page<Device> searchDevicesPaginated(int pageNo, int pageSize, String sortField, String sortDirection, String deviceName) {
// 在这里编写带分页和排序的模糊查询的逻辑
// 使用传入的设备名称进行模糊查询操作,同时应用分页和排序参数,并返回相应的设备列表页
//设置排序参数,升序ASC/降序DESC?
Sort sort = sortDirection.equalsIgnoreCase(Sort.Direction.ASC.name())
? Sort.by(sortField).ascending()
: Sort.by(sortField).descending();
// 判断设备名称是否为空,为空则设置默认值为''
deviceName = deviceName != null ? deviceName : "";
//根据页号/每页记录数/排序依据返回某指定页面数据。
Pageable pageable = PageRequest.of(pageNo - 1, pageSize, sort);
// 执行模糊查询,并应用分页和排序参数
Page<Device> devicePage = deviceRepository.findByNameContainingIgnoreCase(deviceName, pageable);
return devicePage;
}
//带有排序、通过部门查设备的分页(通过输入部门id来查)
@Override
public Page<Device> searchDevicesByDepartmentIdPaginated(int pageNo, int pageSize, String sortField, String sortDirection, Long departmentId) {
// 在这里编写带分页和排序的模糊查询的逻辑
// 使用传入的设备名称进行模糊查询操作,同时应用分页和排序参数,并返回相应的设备列表页
//设置排序参数,升序ASC/降序DESC?
Sort sort = sortDirection.equalsIgnoreCase(Sort.Direction.ASC.name())
? Sort.by(sortField).ascending()
: Sort.by(sortField).descending();
// 判断设备名称是否为空,为空则设置默认值为''
departmentId = departmentId != null ? departmentId : 0;
//根据页号/每页记录数/排序依据返回某指定页面数据。
Pageable pageable = PageRequest.of(pageNo - 1, pageSize, sort);
// 执行模糊查询,并应用分页和排序参数
Page<Device> devicePage = deviceRepository.findByDepartmentId(departmentId, pageable);
return devicePage;
}
//带有排序功能的分页
/*
@Override
public Page<Device> findDevicePaginated(int pageNo, int pageSize, String sortField, String sortDirection) {
//设置排序参数,升序ASC/降序DESC?
Sort sort = sortDirection.equalsIgnoreCase(Sort.Direction.ASC.name())
? Sort.by(sortField).ascending()
: Sort.by(sortField).descending();
//根据页号/每页记录数/排序依据返回某指定页面数据。
Pageable pageable = PageRequest.of(pageNo - 1, pageSize, sort);
return this.deviceRepository.findAll(pageable);
}
*/
}
DepartmentService
:
package com.example.springthymeleafproject.service;
import com.example.springthymeleafproject.model.Department;
import com.example.springthymeleafproject.model.Device;
import org.springframework.data.domain.Page;
import java.util.List;
public interface DepartmentService {
//查找所有部门
List<Department> getAllDepartment();
//新增部门
void saveDepartment(Department department);
//获取指定ID的的部门
Department getDepartmentId(long id);
//分页并通过字段排序
Page<Department> findDepartmentPaginated(int pageNo, int pageSize, String sortField, String sortDirection);
}
DepartmentServiceImpl
:
package com.example.springthymeleafproject.service;
import com.example.springthymeleafproject.model.Department;
import com.example.springthymeleafproject.model.Device;
import com.example.springthymeleafproject.repository.DepartmentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class DepartmentServiceImpl implements DepartmentService{
@Autowired
private DepartmentRepository departmentRepository;
//查询所有部门信息
@Override
public List<Department> getAllDepartment() {
return departmentRepository.findAll();
}
//保存部门信息
@Override
public void saveDepartment(Department department) {
this.departmentRepository.save(department);
}
//获取指定id的部门
@Override
public Department getDepartmentId(long id) {
Optional<Department> optional = departmentRepository.findById(id);
Department department = null;
//如果存在指定的id的设备,则赋值给device,不存在则抛出异常
if (optional.isPresent()){
department = optional.get();
} else {
throw new RuntimeException("找不到指定设备的id : " + id);
}
return department;
}
//分页并通过字段排序
@Override
public Page<Department> findDepartmentPaginated(int pageNo, int pageSize, String sortField, String sortDirection) {
//设置排序参数,升序ASC/降序DESC?
Sort sort = sortDirection.equalsIgnoreCase(Sort.Direction.ASC.name())
? Sort.by(sortField).ascending()
: Sort.by(sortField).descending();
//根据页号/每页记录数/排序依据返回某指定页面数据。
Pageable pageable = PageRequest.of(pageNo - 1, pageSize, sort);
return this.departmentRepository.findAll(pageable);
}
}
增删改的逻辑比较简单,主要讲讲如何实现带有排序、查询功能的分页。
带有排序、查询功能的分页
:
1、首先,根据传入的参数设置排序规则(sortField和sortDirection)。根据sortDirection的值,判断排序方式是升序还是降序,然后创建对应的Sort对象用于排序操作。
2、接着,对传入的设备名称进行判断,如果为null,则将其设为一个空字符串"",用于模糊查询中的条件。
3、创建一个Pageable对象,该对象描述了分页和排序的规则,包括所请求页的页号(pageNo),每页的记录数(pageSize)和排序规则(sort)。
4、调用deviceRepository的findByNameContainingIgnoreCase方法进行模糊查询,传入设备名称和Pageable对象,以获取符合条件的设备列表。
5、返回查询结果,即一个Page对象,其中包含了查询到的设备列表数据。
带有排序、部门查设备的分页(通过输入部门id来查)也是同样的道理。
3.4 controller层和视图层
Controller
层负责接收和处理用户请求,并将请求转发给相应的业务逻辑。它通常包含了应用程序的控制器类(Controller)和处理请求的方法。Controller层的主要职责是解析请求参数、调用业务逻辑处理和返回响应结果给用户。它起到了用户与应用程序之间的桥梁作用。
以DeviceController,DepartmentController的实现过程都差不多,DepartmentController实现起来会DeviceController简单,所有以DeviceController为例进行讲解。
由于后面代码较多,所以有需求的小伙伴可以到我的码云提取。码云地址在最上面
DeviceController
- 首先使用@Autowired注解将DeviceService和DepartmentService注入到当前的类中。
- 显示添加设备页面
@GetMapping("/addDevice")
注解表示该方法处理HTTP GET请求,并处理的请求路径为“/addDevice”
- 方法参数
Model model
是 Spring MVC 中的一个类,用于向视图页面传递数据。 - 使用
model.addAttribute("device", device)
将 device 对象添加为模型属性,其中的 “device” 是属性的名称,可以在视图页面中使用该名称获取设备对象。 - 之所以要获取所有的Departmentd对象是因为这里设计了下拉框,这里这样设计是因为一部部门不会很多,选择框会是最好的选择
- 最后,返回字符串
“addDevice”
,它表示要渲染的视图页面的名称。
3. 数据保存
- 需要注意要使用Post请求
- 其中逻辑与添加设备类似
- 最后需要通过重定向,将控制权交给根路径的请求处理方法,显示设备列表页面。
- 在视图层我们可以添加一个按钮来跳转到这个页面即可将数据保存。
例如:添加设备中
其中th:action="@{/saveDevice}" 是一个Thymeleaf模板引擎的语法,用于指定表单提交的目标URL。就是当我们点击提交后,就会跳转到/saveDevice将数据保存,最后显示设备列表页面。
- 更新数据
- 更新数据也类似,主要是要如何实现点击按钮能获取到指定的id,并将此id的数据进行修改。
- 就可以通过添加隐藏表单(隐藏其id,不让用户更改)字段来实现。
- 删除数据
- 删除数据与其一个道理,不在啰嗦。但值得一提的是,万一用户不小心点到了,而没有提示就把数据删除了,那就有点不知所措,
- 所以我加了个模态框来提示用户,是否确定要删除。
- 获取分页数据并对通过字段排序排序、模糊查询
- 这里比较复杂,所以讲讲他的注解
@PathVariable(value = "pageNo") int pageNo
注解表示通过路径参数绑定了一个整型的变量 pageNo。路径参数值将被转换为整型并绑定到 pageNo 变量上。路径参数名称为 pageNo。其实就是当前页面为第几页。
@RequestParam("sortField")、@RequestParam("sortDir")
注解表示这是一个查询参数,它用于指定排序字段。查询参数的值将被绑定到 sortField 字符串变量上,后续我们可以添加一个超链接来改变这个参数,即可做到点击表头的指定字段来进行排序。
@RequestParam(required = false) String deviceName
注解表示这是一个可选的查询参数,required = false 表示该查询参数不是必需的,如果没有提供该参数,则将设备名称设置为 null。但要查询的时候,就可以获取用户输入的值来绑定deviceName ,且构造url来实现查询。
@RequestParam(required = false)
一样的道理
- 分页
用简单的if-else来构造listDevice
- 向模型对象添加属性
- 主页面
通过调用searchDevicePaginated
方法实现了默认的首页展示功能。这个方法为分页的那个方法。
3.5 视图层
主要讲讲index.html,因为许多业务都是围绕index.html来的,部门的视图与index.html类似。
-
引入Bootstrap框架,因为做了一个模态框,需要用到jQuery,所以把它也引进来了,但需要注意点是,引入的版本要与Bootstrap的版本对应,不然是不会有效果的。其他的视图是不需要引的,因为本次案例主要用BootStrap。
-
导航栏的制作。
设备列表和部门列表添加一个超链接即可。搜索框是比较难的,需要构造跳转url,我使用的是通过一个隐藏的input来默认页码、排序字段、排序方式,而用户只需要输入设备名称,当点击搜索按钮时即可跳转跳转utl。 -
实现搜索到的设备也能实现分页,排序等功能。
在写搜索的时候遇到一个问题,搜索出结果后,能实现分页了,且也默认id排序了,但是我想点击下一页或者想按我点击的字段排序,它就会跳转到所以设备的列表中,就不是我们搜索的列表的,那么要怎么解决这个问题呢?
其实就是让其在保留用户输入的参数即可。
用一个嵌套的三元运算符来判断用户进行了搜索功能没有,如果进行了搜索功能,它的deviceName或者departmentID参数是不会为null的。这样就可以在点击时保留用户输入的参数
。
其他的都是类似的,只是拿出其中一部分举个例子。
总结
在最后其实还做了一个,登录页面。
当我们输入对了用户名和密码之后,就会自动跳转到设备列表页面。但输入的用户名或密码,与数据库存的密码不一致或不存在用户名时,就提示用户名或密码错误。其次在我们输入密码的时候旁白还有个小眼睛,可以让我们看到输入的密码。
但是因为时间关系和技术原因,没能把他们设计的更合理。也是一个小小的遗憾。
整个案例流程
1、配置Spring Boot项目:创建Spring Boot项目并添加所需的依赖,包括Spring Boot Web、Spring Data JPA、MySQL驱动等。
2、设置数据库连接:在项目的配置文件中配置MySQL数据库的连接参数,包括数据库URL、用户名、密码等。
3、创建实体类:使用JPA注解创建实体类,与数据库中的表进行映射。
4、创建数据访问层(Repository):使用JPA的Repository接口定义数据访问操作,如增删改查分页等。
5、创建业务逻辑层(Service):实现业务逻辑操作,包括对数据库进行增删改查的处理,并可以调用Repository层相关方法。
6、创建控制器(Controller):处理用户请求和相应的逻辑,在Controller中可以调用Service层提供的方法进行数据处理。
7、创建Thymeleaf视图模板:在resources/templates目录下创建Thymeleaf模板文件,用于生成动态的HTML页面。
8、创建前端页面:使用Bootstrap框架创建美观的前端页面,可以使用Bootstrap提供的组件和样式进行页面布局和美化。
9、编写控制器方法:在Controller中编写请求处理方法,通过指定URL路径和请求方法,将用户请求映射到相应的控制器方法,并返回相应的视图模板或数据。
10、渲染数据:在Thymeleaf模板中使用表达式语法,从控制器中传递的数据进行渲染,生成最终的HTML页面。