赞
踩
书接上文 Spring Boot REST API - CRUD 操作,一些和数据库相关联的注解在 [spring] spring jpa - hibernate CRUD
主要的 layer 如下:
项目开始前的准备
Spring 依旧是从 https://start.spring.io/ 上下载的,具体配置如下:
properties 文件更新如下:
spring.datasource.url=jdbc:mysql://localhost:3306/employee_directory
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
用的是 mysql,具体跑的脚本如下:
CREATE DATABASE IF NOT EXISTS `employee_directory`; USE `employee_directory`; -- -- Table structure for table `employee` -- DROP TABLE IF EXISTS `employee`; CREATE TABLE `employee` ( `id` int NOT NULL AUTO_INCREMENT, `first_name` varchar(45) DEFAULT NULL, `last_name` varchar(45) DEFAULT NULL, `email` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; -- -- Data for table `employee` -- INSERT INTO `employee` VALUES (1,'Leslie','Andrews','leslie@google.com'), (2,'Emma','Baumgarten','emma@google.com'), (3,'Avani','Gupta','avani@google.com'), (4,'Yuri','Petrov','yuri@google.com'), (5,'Juan','Vega','juan@google.com');
最近卸载掉了一些服务然后移到了 docker 上跑景象,发现方便了不少,下面贴一下用 docker 运行 mysql 的指令
# 下载最新的 mysql 镜像 ❯ docker pull mysql:latest # 运行镜像启动容器 ❯ docker run --name mysql-container -e MYSQL_ROOT_PASSWORD=yourpassword -p 3306:3306 -d mysql:latest # 查看正在运行的 docker container ❯ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ec2f40d48498 mysql "docker-entrypoint.s…" 3 days ago Up 3 days 8080/tcp, 0.0.0.0:3306->3306/tcp, 33060/tcp nice_kirch # 可以查看 3306 是否被使用 # 正常来说上面的 PORTS 没问题就行了 # 我这里跑出来其实有一大堆的结果,因为 spring 跑起来了,也在和 3306 进行沟通,所以 spring 的 process 也会在这个列表中 # 如果刚刚启动了 docker,应该只有一条 docker 的 process 在使用 3306 ❯ lsof -i :3306 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME com.docke 1642 _____ 144u IPv6 0xf1d60cef5e02672d 0t0 TCP localhost:mysql->localhost:55086 (ESTABLISHED) com.docke 1642 _____ 642u IPv6 0xf1d60cef66beb72d 0t0 TCP *:mysql (LISTEN) # 复制 sql 文件到 container 里 ❯ docker cp <file_name> <container_name>:<path_name>
完成后使用 mysql 运行 sql 文件即可
这里就先用比较传统的实现
@Data @NoArgsConstructor @RequiredArgsConstructor @Entity @Table(name = "employee") public class Employee { // define fields @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private int id; @NonNull // must be the table name @Column(name = "first_name") private String firstName; @NonNull @Column(name = "last_name") private String lastName; @NonNull @Column(name = "email") private String email; }
使用 lombok 省略一些实现,关于其他的注解,都是来自 Hibernate 的部分
这里就是比较传统的 DAO+DAOImpl 的实现,依旧是 Hibernate 的东西:
DAO interface:
public interface EmployeeDAO {
List<Employee> findAll();
}
DAOImpl 实现:
@Repository public class EmployeeDAOImpl implements EmployeeDAO { // define field for entityManager private EntityManager entityManager; // setup constructor injection @Autowired public EmployeeDAOImpl(EntityManager entityManager) { this.entityManager = entityManager; } @Override public List<Employee> findAll() { // create a query TypedQuery<Employee> query = this.entityManager.createQuery("from Employee", Employee.class); // execute query and get result list // and return result return query.getResultList(); } }
这里依旧结合了一下 rest api 部分和 hibernate 的实现,后期会进行重构:
@RestController
@RequestMapping("/api")
public class EmployeeRestController {
private final EmployeeDAO employeeDAO;
public EmployeeRestController(EmployeeDAO employeeDAO) {
this.employeeDAO = employeeDAO;
}
@GetMapping("/employees")
public List<Employee> findAll() {
return employeeDAO.findAll();
}
}
实现效果如下:
上面的实现是直接通过 controller 和 DAO 层进行沟通,但是忽略了 service 层,这里把 service 层的实现补上
service 层本身是 façade 设计模式,其主要实现的功能就是让 DAO 层专注于实现数据的获取,controller 层专注于 HTTP 的处理,而 service 层则对 business logic 进行处理。由此可以延展出的优点/特性为:
对功能的实现进行抽象,将低耦合性
实现交易(transaction)管理
集中处理 business logic
复用性与灵活性
提高可维护性与可拓展性
具体实现如下:
首先声明一个 service 的 interface
public interface EmployeeService {
List<Employee> findAll();
}
实现 interface
@Service
public class EmployeeServiceImpl implements EmployeeService{
private final EmployeeDAO employeeDAO;
public EmployeeServiceImpl(EmployeeDAO employeeDAO) {
this.employeeDAO = employeeDAO;
}
@Override
public List<Employee> findAll() {
return this.employeeDAO.findAll();
}
}
可以看到,这个实现和 DAO 很像
将 controller 中调用的 DAO 替换为 service
@RestController
@RequestMapping("/api")
public class EmployeeRestController {
private final EmployeeService employeeService;
public EmployeeRestController(EmployeeService employeeService) {
this.employeeService = employeeService;
}
@GetMapping("/employees")
public List<Employee> findAll() {
return employeeService.findAll();
}
}
这里实现一个根据 id 获取 employee 的功能,主要也是更新 DAO, DAOImpl,Service,ServiceImpl 和 Controller 这个套路,这里就放在一起了。
public interface EmployeeDAO {
List<Employee> findAll();
Employee findById(int employeeId);
Employee save(Employee employee);
void deleteById(int employeeId);
}
@Repository public class EmployeeDAOImpl implements EmployeeDAO { // define field for entityManager private EntityManager entityManager; // setup constructor injection @Autowired public EmployeeDAOImpl(EntityManager entityManager) { this.entityManager = entityManager; } @Override public List<Employee> findAll() { // create a query TypedQuery<Employee> query = this.entityManager.createQuery("from Employee", Employee.class); // execute query and get result list // and return result return query.getResultList(); } @Override public Employee findById(int employeeId) { return this.entityManager.find(Employee.class, employeeId); } // No @Transactional here, it'll be handled at service layer @Override public Employee save(Employee employee) { // if id == 0, then insert/save // else, update return this.entityManager.merge(employee); } @Override public void deleteById(int employeeId) { Employee employee = this.findById(employeeId); this.entityManager.remove(employee); } }
这里几个点需要注意一下:
merge
是根据 id 进行的操作,如果 id == 0
,那么实现添加功能,不然就是修改@Transactional
注解这里和 DAO 一样,添加新的方法即可——我是直接从 DAO 那里 cv 过来的
public interface EmployeeService {
List<Employee> findAll();
Employee findById(int employeeId);
Employee save(Employee employee);
void deleteById(int employeeId);
}
@Service public class EmployeeServiceImpl implements EmployeeService{ private final EmployeeDAO employeeDAO; public EmployeeServiceImpl(EmployeeDAO employeeDAO) { this.employeeDAO = employeeDAO; } @Override public List<Employee> findAll() { return this.employeeDAO.findAll(); } @Override public Employee findById(int employeeId) { return this.employeeDAO.findById(employeeId); } @Override @Transactional public Employee save(Employee employee) { return this.employeeDAO.save(employee); } @Override @Transactional public void deleteById(int employeeId) { this.employeeDAO.deleteById(employeeId); } }
这里需要注意的就是:service 层中添加了 @Transactional
注解
相当于 business logic 在 service 中进行处理——不过这里的业务比较简单就是了
@RestController @RequestMapping("/api") public class EmployeeRestController { private final EmployeeService employeeService; public EmployeeRestController(EmployeeService employeeService) { this.employeeService = employeeService; } @GetMapping("/employees") public List<Employee> findAll() { return employeeService.findAll(); } @GetMapping("/employees/{employeeId}") public Employee findById(@PathVariable int employeeId) { Employee employeeFound = this.employeeService.findById(employeeId); if (employeeFound == null) { throw new RuntimeException("Employee id not found - " + employeeId); } return employeeFound; } @PostMapping("/employees") public Employee save(@RequestBody Employee newEmployee) { newEmployee.setId(0); return this.employeeService.save(newEmployee); } @PutMapping("/employees") public Employee updateEmployee(@RequestBody Employee updateEmployee) { return this.employeeService.save(updateEmployee); } @DeleteMapping("/employees/{employeeId}") public String deleteEmployee(@PathVariable int employeeId) { Employee employeeFound = this.employeeService.findById(employeeId); if (employeeFound == null) { throw new RuntimeException("Employee id not found - " + employeeId); } this.employeeService.deleteById(employeeId); return "Deleted employee id - " + employeeId; } }
效果如下:
实现功能 | 效果展示 |
---|---|
findById | |
save | |
updage | |
deleteById |
目前项目结构如下:
其实可以看到代码实现还是有很多的 boilerplate code,如 DAO/DAOImpl 和 Service/ServiceImpl,这 4 个类里的代码其实高度重合。为了解决这个问题,Spring 也实现了两个库,它们可以提供一些常规的 CRUD 功能的实现,减少 boilerplate code
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
主要是增加这两个依赖
二者都是可拓展,如果想要实现更加个性化的功能,也可以通过实现 @Query
去进行拓展
Sping Data JPA 可以通过实现 JpaRepository
去提供基础 CRUD 操作的支持
实现很简单:
package com.example.demo.dao;
import com.example.demo.entity.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
}
不需要写任何代码,就可以把 DAO 和 DAOImpl 删了,接下来就是修改所有使用 DAO 的地方,将其修改为调用 repository:
@Service public class EmployeeServiceImpl implements EmployeeService { private final EmployeeRepository employeeRepository; public EmployeeServiceImpl(EmployeeRepository employeeDAO) { this.employeeRepository = employeeDAO; } @Override public List<Employee> findAll() { return this.employeeRepository.findAll(); } @Override public Employee findById(int employeeId) { Optional<Employee> result = this.employeeRepository.findById(employeeId); if (result.isEmpty()) { throw new RuntimeException("Did not find employee id - " + employeeId); } return result.get(); } @Override @Transactional public Employee save(Employee employee) { return this.employeeRepository.save(employee); } @Override @Transactional public void deleteById(int employeeId) { this.employeeRepository.deleteById(employeeId); } }
需要注意的是这里对 findById
的具体实现有些不一样,这里是因为 employeeRepository.findById
的返回值是 Optional<Employee>
,因此需要对其进行一个空值的检查。
直接返回也不是不行,不过 Intellij 会报警告:
不影响正常使用,不过看着有点烦
DAO 可以用 JPA 代替,Service 也可以被 REST 所取代。这里其实不需要做什么事情,只要确定 pom 里有 spring-boot-starter-data-rest
,直接删除掉 controller 和 service 即可
这时候的调用结果如下:
实现功能 | 效果展示 |
---|---|
patch,之前没有实现 patch,所以这里就尝试调用一下 | |
retrieve 注意这里结构改了 | |
query,这也是 rest 自带的一些支持功能 |
我这里修改的 properties 文件如下:
# Spring Data Rest properties
spring.data.rest.base-path=/api
spring.data.rest.default-page-size=50
base-path
加回对 RequestMapping
的支持,然后修改了一下默认的 pagination size
另一个比较常见需要修改的可能是 resource 的名字,data-rest 是会按照 resource+s 这种方式去自动添加路径,但是英语的复数形态不一定遵从这个规则,比如 person 的复数是 people,mouse 的复数是 mice 等,要修改路径则需要在 Repo 上使用 @RepositoryRestResource
的方式去修改,如:
@RepositoryRestResource(path = "members")
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
}
这样就无法通过 employees
的路径去访问,而是需要使用 members
:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。