# jpa-crud **Repository Path**: ashinigit/jpa-crud ## Basic Information - **Project Name**: jpa-crud - **Description**: 【Toy Project】Spring Boot 自定义 Starter 的 demo - **Primary Language**: Java - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-02-11 - **Last Updated**: 2025-03-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: SpringBoot, JPA, MySQL ## README # jpa-crud Spring Boot 自定义 Starter 的 demo #### 介绍 jpa-crud-starter 功能:使用一个注解 `@Crud` 创建单表的增查删改 HTTP 接口, 支持 Spring Boot 2.x 和 Spring Boot 3.x 暂时仅支持 MySQL 数据源,HTTP 接口为 REST 风格 #### 软件架构 Spring Boot + Spring Data JPA + MySQL #### 安装教程 1. 使用命令 `git clone` 拉取项目到本地 2. 使用 Intellij IDEA 打开项目 3. 在 `demo/src/main/resources/application.yml` 文件配置数据库,需要手动在 MySQL 中创建空数据库 4. 运行项目 `demo/src/main/java/com/example/demo/Application.java`(Spring Boot 3.x 依赖 Java 17+) 5. API 见 `demo/src/test/resources/api-users.http` #### 使用说明 1. 核心代码位于 `com.example.jpa.crud` 包 2. `com.example.demo.entity` 包有一个示例:User.java 3. 只需加上 `@Crud` 注解就能自动创建 REST 风格接口 ```java import com.example.jpa.crud.annotation.Crud; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; import static jakarta.persistence.GenerationType.IDENTITY; @Entity @Table(name = "t_user") @Crud(name = "users") public class User { @Id @GeneratedValue(strategy = IDENTITY) public long id; public String name; public String email; public int age; public String password; public boolean isAdmin; } ``` `@Entity` 和 `@Table(name = "t_user")` 用于关联数据库中的表,具体用法参考 Spring Data JPA 文档 **`@Crud` 注解是本项目的核心注解,它有三个属性:`name`、`value` 和 `autoGenerateId`** 其中 `name` 和 `value` 两个属性的作用完全相同,都是用于指定 URL 中的资源名称,默认值为类名 "User",`@Crud(name = "users")` 将资源名称指定为小写复数形式 "users" `autoGenerateId` 属性的默认值为 `true`,表示开启自动生成主键的功能,在调用 POST http://localhost:8080/api/users 接口进行新增操作时不需要在 json 参数中设置 id: ```json { "name": "用户名", "email": "test@test.com", "age": 20, "password": "密码", "isAdmin": true } ``` 1. 如果 id 是 int、long、Integer、Long 类型,并且设置了 `@GeneratedValue(strategy = GenerationType.IDENTITY)`,则 id 会由数据库自增生成 2. 如果 id 是 String 类型,则会自动生成 UUID 作为主键 如果将 `autoGenerateId` 属性设置为 false,进行新增操作时需要由调用者在参数中指定 id: ```json { "id": 100, "name": "用户名", "email": "test@test.com", "age": 20, "password": "密码", "isAdmin": true } ``` #### 特性 ##### 1. REST 接口 ```http request ### 分页查询数据 GET http://localhost:8080/api/users Accept: application/json ### 根据 id 查询数据 GET http://localhost:8080/api/users/2 Accept: application/json ### 新增一条数据 POST http://localhost:8080/api/users Content-Type: application/json Accept: application/json { "name": "用户名", "email": "test@test.com", "age": 20, "password": "密码", "isAdmin": true } ### 根据 id 删除数据 DELETE http://localhost:8080/api/users/2 Accept: application/json ### 根据 id 更新数据 PUT http://localhost:8080/api/users/1 Content-Type: application/json Accept: application/json { "name": "新的用户名", "age": 21 } ``` ##### 2. 分页 使用 page 和 size 指定页码和容量,如果没有提供这两个参数,则使用默认值 page = 1, size = 10 | 参数 key | 参数 value | |--------|----------| | page | `2` | | size | `100` | ```http request GET http://localhost:8080/api/users?page=2&size=100 Accept: application/json ``` ##### 3. 排序 使用 sort 表达式可以指定排序条件: name asc, age desc | 参数 key | 参数 value | |--------|----------------------| | page | `2` | | size | `100` | | sort | `name asc, age desc` | ```http request GET http://localhost:8080/api/users?page=2&size=100&sort=name asc, age desc Accept: application/json ``` ##### 4. 过滤 通过直接拼接参数的方式指定数据过滤条件: isAdmin 等于 true 且 age 等于 20 | 参数 key | 参数 value | |---------|----------| | isAdmin | `true` | | age | `20` | ```http request GET http://localhost:8080/api/users?isAdmin=true&age=20 Accept: application/json ``` > 注意,如果要使用拼接参数的方式指定数据过滤条件,则存在一些限制: > 1. 不可以和 search 表达式同时使用 > 2. 参数名(也就是字段名)不可以是 `page`、`size`、`sort`、`search` 这几个保留字 > 3. 只能够表达形如 `(字段1 等于 值1) and (字段2 等于 值2) and (字段3 等于 值3) ...` 的过滤语义 ##### 5. search 表达式 目前,search 表达式只能用于指定过滤条件,支持的运算符包括: | 运算符 | 语义 | |--------------|-------------| | `eq` | 等于 | | `ne` | 不等于 | | `lt` | 小于 | | `gt` | 大于 | | `le` | 小于或等于 | | `ge` | 大于或等于 | | `in` | 包含于 | | `not_in` | 不包含于 | | `like` | 包含(字符串) | | `start_with` | 以...开始(字符串) | | `end_with` | 以...结束(字符串) | 过滤条件的基本格式是 `<字段名> <运算符> :<参数名>`,简单的条件可以借助 `and` 和 `or` 组合出更复杂的过滤条件 示例:使用 search 表达式指定数据过滤条件:isAdmin 等于 true 且 age 大于或等于 20 | 参数 key | 参数 value | |--------|-----------------------------------------| | param1 | `true` | | param2 | `20` | | search | `isAdmin eq :param1 and age ge :param2` | ```http request GET http://localhost:8080/api/users?param1=true¶m2=20&search=isAdmin eq :param1 and age ge :param2 Accept: application/json ``` 在这个例子中,`isAdmin` 和 `age` 是 `<字段名>` `eq` 和 `ge` 是 `<运算符>` `param1` 和 `param2` 是 `<参数名>` `<参数名>` 可以是任意的有效标识符( 当然,`page`、`size`、`sort`、`search` 这几个保留字除外),但是更建议与对应的字段名保持一致: | 参数 key | 参数 value | |---------|---------------------------------------| | isAdmin | `true` | | age | `20` | | search | `isAdmin eq :isAdmin and age ge :age` | ```http request GET http://localhost:8080/api/users?isAdmin=true&age=20&search=isAdmin eq :isAdmin and age ge :age Accept: application/json ``` 注意到,`in` 和 `not_in` 需要数组类型的参数值,本项目的处理方式是**拼接多个同名参数**来传递数组,这里的 `param2` 就是一个例子: | 参数 key | 参数 value | |--------|----------------------------------------------------------------------------| | param1 | `用户` | | param2 | `18` | | param2 | `19` | | param2 | `20` | | param3 | `true` | | search | `((name start_with :param1) and (age in :param2)) or (isAdmin eq :param3)` | ```http request GET http://localhost:8080/api/users?param1=用户¶m2=18¶m2=19¶m2=20¶m3=true&search=((name start_with :param1) and (age in :param2)) or (isAdmin eq :param3) Accept: application/json ``` 实际上,这个 HTTP 请求最终会被翻译为如下 SQL: ```text select `is_admin`,`password`,`name`,`id`,`age`,`email` from `t_user` where (((`name` like :name_x) and (`age` in (:age_y) )) or (`is_admin` = :isAdmin_z)) limit 0, 10 ``` 原始的参数名 `param1`、`param2` 和 `param3` 只是占位符,被替换为 `name_x`、`age_y` 和 `isAdmin_z`: ```text isAdmin_z => true name_x => 用户% age_y => [18, 19, 20] ``` 为了避免影响接口的执行效率,这里的 search 表达式使用了一个简单的文法来简化解析过程,因此存在一些限制: > 1. `<运算符>` 的左操作数只能是 `<字段名>`,右操作数只能是 `:<参数名>` > 2. 暂不支持字面量,只支持 `:<参数名>` 以占位符的形式提供运算符的右操作数 > 3. 暂不支持 `and` 和 `or` 的优先级关系,即二者被视为同样的优先级 ##### 6. 备注 > 1. 当过滤条件中同时出现 `and` 和 `or` 时,需要通过添加括号来明确表达优先级关系 > 2. 本项目创建的接口暂时无法提供参数校验、权限控制等功能 #### 更新日志 ##### 0.0.2 🐞 修复 `page` 和 `size` 类型转换失败的 bug 🌟 新增 `@CrudField` 注解,用于 `隐藏` 或 `替换` 指定的字段值 ```java @Entity @Table(name = "t_user") @Crud(name = "users") public class User { @Id @GeneratedValue(strategy = IDENTITY) public long id; public String name; public String email; public int age; @CrudField(display = DisplayStrategy.REPLACE, replace = ReplacePassword.class) public String password; public boolean isAdmin; } ``` 其中, `ReplacePassword` 的逻辑是将 `password` 字段的值用 `******` 代替: ```java public class ReplacePassword implements Function { @Override public String apply(String raw) { return "******"; } } ``` 接口的返回结果如下: ```json { "page": 1, "size": 1, "total": 1, "totalPage": 1, "data": [ { "name": "用户名", "password": "******", "id": 1, "isAdmin": true, "email": "test@test.com", "age": 20 } ] } ``` 为了解决 JavaScript 在处理长整数( 64 位,Java 的 Long 类型)时发生的精度丢失问题,可以使用 `@CrudField` 注解进行类型转换 ```java @Entity @Table(name = "t_user") @Crud(name = "users") public class User { @Id @GeneratedValue(strategy = IDENTITY) @CrudField(display = DisplayStrategy.REPLACE, replace = LongToString.class) public long id; } ``` 其中,`LongToString` 的定义如下: ```java public class LongToString implements Function { @Override public String apply(Long a) { return String.valueOf(a); } } ``` ##### 0.0.3 🌟 新增 `crud.path-prefix` 配置项,用于自定义接口 URL 的路径前缀,默认值是 `api` 🌟 新增 `crud.remove-path-prefix` 配置项,用于移除接口 URL 的路径前缀,默认值是 `false` 🌟 新增 `@ViewTemplate` 注解,用于自定义接口返回值的数据格式 默认情况下,接口 URL 的路径前缀是 `api`: ```http request GET http://localhost:8080/api/users?page=2&size=100 Accept: application/json ``` 在 application.yml 添加如下配置将前缀修改为 `api-v1`: ```yaml crud: path-prefix: api-v1 ``` 修改后的接口路径: ```http request GET http://localhost:8080/api-v1/users?page=2&size=100 Accept: application/json ``` 在 application.yml 添加如下配置: ```yaml crud: remove-path-prefix: true ``` 修改后的接口路径: ```http request GET http://localhost:8080/users?page=2&size=100 Accept: application/json ``` > 注意:`crud.remove-path-prefix` 的优先级比 `crud.path-prefix` 更高 如果需要将返回值统一为 `{ "code": xxx, "message": xxx, "data": xxx }` 格式,需要自定义一个 JsonViewTemplate 接口的实现类,并使用 `@ViewTemplate` 注解进行标记 ```java @ViewTemplate public class CodeMessageData implements JsonViewTemplate { @Setter @Getter static class Model { private long code; private String message; private Object data; } @Override public Object ok(Object data, boolean update, long effect) { final Model model = new Model(); if (update) { if (effect > 0L) { model.setCode(200L); model.setMessage("ok"); } else { model.setCode(500L); model.setMessage("err"); } } else { model.setCode(200L); model.setMessage("ok"); } model.setData(data); return model; } @Override public Object err(Exception e) { final Model model = new Model(); model.setCode(500L); model.setData(null); model.setMessage(e.getMessage()); return model; } } ``` 接口的返回结果如下: ```json { "code": 200, "message": "ok", "data": { "page": 1, "size": 1, "total": 1, "totalPage": 1, "data": [ { "name": "用户名", "password": "******", "id": 1, "isAdmin": true, "email": "test@test.com", "age": 20 } ] } } ``` ##### 0.0.4 🌟 新增 `crud.show-sql` 配置项,用于在日志里打印 SQL ,默认值是 `false`,表示不打印 SQL ##### 0.0.5 🌟 提供对 `Spring Boot 2.x` 的支持 #### 参与贡献 1. Fork 本仓库 2. 新建 Feat_xxx 分支 3. 提交代码 4. 新建 Pull Request