用Springboot + JPA + thymeleaf + MySQL + BootStrap完成一个设备管理系统案例

码云代码地址: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 对象作为参数,用于实现分页和排序。

JpaRepositorySpring 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

  1. 首先使用@Autowired注解将DeviceService和DepartmentService注入到当前的类中。
    在这里插入图片描述
  2. 显示添加设备页面
    在这里插入图片描述
  • @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将数据保存,最后显示设备列表页面。

  1. 更新数据
  • 更新数据也类似,主要是要如何实现点击按钮能获取到指定的id,并将此id的数据进行修改。
  • 就可以通过添加隐藏表单(隐藏其id,不让用户更改)字段来实现。
    在这里插入图片描述
  1. 删除数据
  • 删除数据与其一个道理,不在啰嗦。但值得一提的是,万一用户不小心点到了,而没有提示就把数据删除了,那就有点不知所措,
  • 所以我加了个模态框来提示用户,是否确定要删除。
    在这里插入图片描述
  1. 获取分页数据并对通过字段排序排序、模糊查询
  • 这里比较复杂,所以讲讲他的注解
    在这里插入图片描述

@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类似。

  1. 引入Bootstrap框架,因为做了一个模态框,需要用到jQuery,所以把它也引进来了,但需要注意点是,引入的版本要与Bootstrap的版本对应,不然是不会有效果的。其他的视图是不需要引的,因为本次案例主要用BootStrap。
    在这里插入图片描述

  2. 导航栏的制作。
    在这里插入图片描述
    在这里插入图片描述
    设备列表和部门列表添加一个超链接即可。搜索框是比较难的,需要构造跳转url,我使用的是通过一个隐藏的input来默认页码、排序字段、排序方式,而用户只需要输入设备名称,当点击搜索按钮时即可跳转跳转utl。

  3. 实现搜索到的设备也能实现分页,排序等功能。

在写搜索的时候遇到一个问题,搜索出结果后,能实现分页了,且也默认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页面。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值