** 1 @Query註解詳解及其用法**
說明:本文的寫作構建在我的公衆號文章SpringBoot之路(二)使用用Spring-Data-JPA訪問數據庫進行基本的CRUD操作這篇文章的基礎之上。
@Query註解在spring-data-jpa中可用來定製自定義sql語句的數據庫增刪改查操作,使用起來也是非常方便
1.1 源碼分析
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@QueryAnnotation
@Documented
public @interface Query {
/**
*指定JPAL查詢語句,nativeQuery=true時是原生的SQL語句
*/
String value() default "";
/**
*指定count的 JPQL語句,如果不指定將根據query自動生成;
*nativeQuery=true時是原生的SQL語句
*/
String countQuery() default "";
/**
*根據哪個字段來count,一般默認即可
*/
String countProjection() default "";
/**
*默認爲false,表示不是原生的SQL語句
*/
boolean nativeQuery() default false;
/**
*可以指定一個query的名字,必須是唯一的,
*如果不指定,默認的生產規則是{$domainClass}.{$queryMethodName}
*/
String name() default "";
/**
*指定一個count的query名字,必須是唯一的
*如果不指定,默認的生產規則是{$domainClass}.{$queryMethodName}.count
*/
String countName() default "";
}
1.2 @Query的用法
使用命名查詢爲實體聲明查詢是一種有效的方法,對於少量查詢 很有效;一般只需要關心@Query裏面的value和nativeQuery的值;使 用聲明式JPQL查詢有一個好處,就是啓動的時候就知道語法正確與否。
示例:聲名一個@Query註解裝飾的方法在UserRepository上
(1)@Query註解中自定義select語句
@Query(value="select user_id,user_name,password,user_sex,user_name_cn,user_role,tel_num," + "email,reg_date,birth_day,created_by,created_time,last_updated_by,last_updated_time from user_info where user_role=?1",nativeQuery = true)
List<UserInfo> findByUserRole(String userRole);
/**使用@Query裝飾的方法自定義SQL查詢時
*必須修改nativeQuery = true,表示這是一個原生SQL,
*否則項目啓動時就會報錯
*/
(2)@Query註解中自定義update語句
@Modifying
@Query(value="update user_info set email=?2 where user_name=?1",nativeQuery = true)
Integer updateEmailByUserName(String username,String email);
注意:update操作必須在@Query註解上方加上@Modifying註解,否則執行sql語句時會報無法提取結果集異常
1.3 like查詢,注意不會自動加上%關鍵字
@Query(value="select user_id,user_name,user_sex,"+ "user_name_cn,user_role,tel_num,email,reg_date from user_info where user_name like ?1 %",nativeQuery=true)
List<UserInfo> findByLikeUserName(String userName);
//注意以上1與%之間不能有空格
注意: nativeQuery不支持直接Sort的參數查詢
nativeQuery的錯誤寫法,下面這個寫法會使項目啓動時報錯:
@Query(value="select * from user_info where user_name like ?1%",nativeQuery = true)
List<UserInfo> findByUserName(String userName, Sort sort);
1.4 本節測試
以1.2中的在@Query註解中自定義update語句爲例,完成Service層和Controller層代碼
Service層代碼
//UserService 接口中添加抽象方法
Integer updateEmailByUsername(String username,String email);
//UserServiceImpl類中實現方法,注意數據庫增、刪、改方法都要加上@Transactional事務註解
@Override
@Transactional
public Integer updateEmailByUsername(String username, String email) {
return userRepository.updateEmailByUserName(username,email);
}
controller層代碼
@PutMapping("/email/{username}/{email}")
public ServiceResponse<Integer> updateUserEmailByUsername(@PathVariable("username") String username,@PathVariable("email") String email){
ServiceResponse<Integer> response = new ServiceResponse<>();
log.info("username={},email={}",username,email);
Integer count = userInfoService.updateEmailByUsername(username,email);
response.setData(count);
return response;
}
這時就可以通過postman測試效果了
// 請求方法與URL
PUT http://localhost:8088/apiBoot/user/email/LiSi/lisi1989@163.com
//返回結果
{
"status": 200,
"message": "ok",
"data": 1
}
//此時再來調用根據用戶名查看用戶信息接口
GET ttp://localhost:8088/apiBoot/user/userInfos/userName?userName=LiSi
//返回結果
{
"status": 200,
"message": "ok",
"data": [
{
"userId": 2,
"userName": "LiSi",
"password": "89d462491dd4f9e5f2125485a835540c",
"userNameCn": "李四",
"userSex": "M",
"userRole": "Admin",
"telNum": 13100001002,
"email": "[email protected]",
"regDate": "2018-06-10",
"birthDay": "1989-07-12",
"createdBy": "system",
"createdTime": "2020-03-13 23:48:30",
"lastUpdatedBy": "admin",
"lastUpdatedTime": "2020-04-26 11:28:29"
}
]
}
從返回結果的email字段可發現用戶的郵箱已經發生了改變
nativeQuery排序的正確寫法
@Query(value="select * from user_info where user_name like ?1% order by ?2 desc",nativeQuery = true)
List<UserInfo> findByUserNameAndOrderBy(String userName, String sort);
使用@Query註解查詢時的不足就是不支持動態條件查詢
2 javax.persistence概況介紹
雖然Spring Data JPA已經幫我們對數據的操作封裝得很好了, 約定大於配置思想,幫我們默認了很多東西。JPA(Java持久性API) 是存儲業務實體關聯的實體來源。它顯示瞭如何定義一個面向普通 Java對象(POJO)作爲一個實體,以及如何與管理關係實體提供一套 標準。因此,javax.persistence下面的有些註解還是必須要去了解的,以便於更好地提高工作效率.
(1)javax.persistence位於org.hibernate:hibernate-core.jar包裏面,可 以通過Intellij Idea的maven插件直接分析一下maven的依賴,也可以用 $ mvn dependency:tree分析,例如:
[INFO] | +- org.hibernate:hibernate-core:jar:5.3.9.Final:compile
[INFO] | | +- javax.persistence:javax.persistence-api:jar:2.2:compile
[INFO] | | +- org.javassist:javassist:jar:3.23.1-GA:compile
[INFO] | | +- antlr:antlr:jar:2.7.7:compile
[INFO] | | +- org.jboss:jandex:jar:2.0.5.Final:compile
[INFO] | | +- org.dom4j:dom4j:jar:2.1.1:compile
[INFO] | | \- org.hibernate.common:hibernate-commons-annotations:jar:5.0.4.Final:compile
[INFO] | +- org.springframework.data:spring-data-jpa:jar:2.1.6.RELEASE:compile
[INFO] | | +- org.springframework.data:spring-data-commons:jar:2.1.6.RELEASE:compile
[INFO] | | +- org.springframework:spring-orm:jar:5.1.6.RELEASE:compile
[INFO] | | \- org.springframework:spring-tx:jar:5.1.6.RELEASE:compile
[INFO] | \- org.springframework:spring-aspects:jar:5.1.6.RELEASE:compile
(2)通過Intellij Idea的Diagram來看一下此模塊類的關鍵關 系,如圖所示:
(3)下圖顯示了JPA的類的層次結構,包括核心類和JPA接口。
(4)JPA類層次結構的顯示單元
單元 | 描述 |
---|---|
EntityManagerFactory | 一個EntityManager的工廠類,創建並管理多個EntityManager實例 |
EntityManager | 一個接口,管理持久化操作對象,工作原理類似工廠的查詢實例 |
Entity | 實體是持久性對象,是存儲在數據庫中的記錄 |
EntityTransaction | 與EntityManager是一對一的關係,對於每個EntityManager,操作是由EntityTransaction來維護的 |
Persistence | 這個類包含使用靜態方法獲取EntityManagerFactory的實例 |
Query | 該接口由每個JPA供應商實現,能夠獲得符合標準的關係對象 |
上述的類和接口用於存儲實體到數據庫的一個記錄,幫助程序員 通過減少自己編寫的代碼將數據存儲到數據庫中,使他們能夠專注於 更重要的業務活動代碼,如數據庫表映射的類編寫代碼。
3 javax.persistence包下的基本註解
javax.persistence包下基本註解包括@Entity、@Table、@Id、@IdClass、 @GeneratedValue、@Basic、@Transient、@Column、@Temporal、 @Enumerated、@Lob。
3.1 @Entity
先看一個Blog的示例,其中實體的配置如下:
@Entity
@Table(name="user_blog",schema = "mysql")
@NoArgsConstructor
public class UserBlogEntity {
@Id
@Column(name="id",nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Setter
@Getter
private Integer Id;
@Column(name="title",nullable = true,length = 200)
@Setter
@Getter
private String title;
@Basic
@Column(name="create_user_id",nullable = true)
private Integer createUserId;
//由於blog_content字段使用blob類型時從數據庫查詢出的結果一直顯示亂碼,
//因此本實例使用text類型代替blob類型,
//在mysql數據庫中text類型的數據是一個大小寫不敏感的blob類型數據
@Basic
@Column(name="blog_content",nullable = true,length = 4000)
@Setter
@Getter
private String blogContent;
@Column(name="image",nullable = true)
@Lob
@Setter
@Getter
private String image;
//日期屬性必須加上日期格式化註解com.fasterxml.jackson.annotation.JsonFormat,
//防止序列化參數時報錯
@Column(name="create_time",nullable = true)
@Temporal(TemporalType.DATE)
@Setter
@Getter
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@Transient
@Setter
@Getter
private String transitSimple;
}
在數據庫中新建一張與UserBlogEntity實體類對應的表:
use mysql;
create table user_blog(
`id` integer unique key,
`create_user_id` integer,
`title` varchar(200),
`blog_content` text,
`image` varchar(500),
`create_time` TIMESTAMP,
primary key (`create_user_id`,`title`)
)engine=InnoDB default CHARSET=utf8;
@Entity定義對象將會成爲被JPA管理的實體,將映射到指定的數據庫表。
title` varchar(200),
`blog_content` text,
`image` varchar(500),
`create_time` TIMESTAMP,
primary key (`create_user_id`,`title`)
)engine=InnoDB default CHARSET=utf8;
@Entity定義對象將會成爲被JPA管理的實體,將映射到指定的數 據庫表。
public @interface Entity {
//默認是實體類的名字,全局唯一
String name() default "";
}
3.2 @Table
@Table指定數據庫的表名
public @interface Table {
//表名,可選,不填寫時默認爲實體類名
String name() default "";
//表的catalog
String catalog() default "";
//表所在的schema
String schema() default "";
//唯一約束,只有在創建表時有用,默認不需要
UniqueConstraint[] uniqueConstraints() default {};
//索引,只在創建表時有用,默認不需要
Index[] indexes() default {};
}
3.3 @Id
@Id定義屬性爲數據庫的主鍵,一個實體裏面必須有一個
3.4 @IdClass
@IdClass利用外部類的聯合主鍵
public @interface IdClass {
Class value();
}
-
作爲符合主鍵類,要滿足以下幾點要求。
-
必須實現Serializable接口。
-
必須有默認的public無參數的構造方法。
-
必須覆蓋equals和hashCode方法。equals方法用於判斷兩個對 象是否相同,EntityManger通過find方法來查找Entity時是根 據equals的返回值來判斷的。在本例中,只有對象的name和 email值完全相同或同一個對象時才返回true,否則返回 false。hashCode方法返回當前對象的哈希碼,生成的hashCode相同的概率越小越好,算法可以進行優化。
用法:假設UserBlog的聯合主鍵是createUserId和title,新增一個UserBlogKey的類,UserBlogkey.class代碼如下:
@NoArgsConstructor
@AllArgsConstructor
public class UserBlogKey implements Serializable {
@Setter
@Getter
private Integer createUserId;
@Setter
@Getter
private String title;
//注意:作爲聯合主鍵的類需要重寫equals和hashCode方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserBlogKey that = (UserBlogKey) o;
return Objects.equals(createUserId, that.createUserId) &&
Objects.equals(title, that.title);
}
@Override
public int hashCode() {
return Objects.hash(createUserId, title);
}
}
UserBlogRepository中的改動如下:
public interface UserBlogRepository extends JpaRepository<UserBlogEntity,UserBlogKey> {
}
@IdClass的用法實戰
(1)先寫兩個往User_Blog表中插入數據的接口和一個根據Id查詢UserBlogEntity的接口
service層代碼 UserBlogService.java
public interface UserBlogService {
UserBlogEntity saveOne(UserBlogEntity userBlogEntity);
List<UserBlogEntity> saveAll(List<UserBlogEntity> userBlogEntities);
UserBlogEntity findById(UserBlogKey blogKey);
}
UserBlogServiceImpl.java
@Service
public class UserBlogServiceImpl implements UserBlogService {
@Autowired
private UserBlogRepository userBlogRepository;
@Override
@Transactional
public UserBlogEntity saveOne(UserBlogEntity userBlogEntity) {
return userBlogRepository.save(userBlogEntity);
}
@Override
@Transactional
public List<UserBlogEntity> saveAll(List<UserBlogEntity> userBlogEntities) {
return userBlogRepository.saveAll(userBlogEntities);
}
@Override
public UserBlogEntity findById(UserBlogKey blogKey) {
return userBlogRepository.findById(blogKey).get();
}
}
controller層代碼 UserBlogController.java
@RestController
@RequestMapping("/userBlog")
@Slf4j
public class UserBlogController {
@Autowired
private UserBlogService userBlogService;
@PostMapping("/oneUserBlog")
public ServiceResponse<UserBlogEntity> saveOneUserBlog(@RequestBody UserBlogEntity blogEntity){
log.info("blogEntity={}", JSON.toJSON(blogEntity));
ServiceResponse<UserBlogEntity> response = new ServiceResponse<>();
UserBlogEntity data = userBlogService.saveOne(blogEntity);
response.setData(data);
return response;
}
@PostMapping("/batchUserBlog")
public ServiceResponse<List<UserBlogEntity>> batchSaveUserBlog(@RequestBody List<UserBlogEntity> blogEntities){
log.info("blogEntities={}", JSONArray.toJSONString(blogEntities,true));
ServiceResponse<List<UserBlogEntity>> response = new ServiceResponse<>();
List<UserBlogEntity> data = userBlogService.saveAll(blogEntities);
response.setData(data);
return response;
}
@GetMapping("/findOne")
public ServiceResponse<UserBlogEntity> findByBlogKey(@RequestParam("createUserId") Integer createUserId,
@RequestParam("title") String title){
log.info("createUserId={},title={}",createUserId,title);
ServiceResponse<UserBlogEntity> response = new ServiceResponse<>();
UserBlogEntity blogEntity = userBlogService.findById(new UserBlogKey(createUserId,title));
response.setData(blogEntity);
return response;
}
}
postman測試
(1) 插入一條數據
//請求方法與URL
POST http://localhost:8088/apiBoot/userBlog/oneUserBlog
//入參數據
{
"id":1,
"createUserId":1,
"title":"spring-data-jpa從入到精通(一)",
"image":"https://pic.ibaotu.com/game/title/20200518/5ec24079a8164.png",
"blogContent":"本博文講述了使用spring-data-jpa進行基本的CRUD操作",
"createTime":"2020-05-31 11:11:00",
"transitSimple": "test1"
}
//請求響應數據
{
"status": 200,
"message": "ok",
"data": {
"title": "spring-data-jpa從入到精通(一)",
"createUserId": 1,
"blogContent": "本博文講述了使用spring-data-jpa進行基本的CRUD操作",
"image": "https://pic.ibaotu.com/game/title/20200518/5ec24079a8164.png",
"createTime": "2020-05-31 11:11:00",
"transitSimple": null,
"id": 1
}
}
(2)批量插入多條數據
//請求方法與URL
POST http://localhost:8088/apiBoot/userBlog/batchUserBlog
//入參數據
[{
"id":2,
"createUserId":1,
"title":"spring-data-jpa從入到精通(二)",
"image":"https://pic.ibaotu.com/game/title/20200518/5ec240798e78c.png",
"blogContent":"本博文對spring-data-jpa框架中的常用註解及其用法進行了實戰講述",
"createTime":"2020-05-31 11:41:00",
"transitSimple": "test2"
},{
"id":3,
"createUserId":1,
"title":"spring-data-jpa從入到精通(三)",
"image":"https://pic.ibaotu.com/game/title/20200518/5ec240798e78c.png",
"blogContent":"本博文主要講述了在spring-boot項目中使用spring-data-jpa進行聯表查詢及複雜的動態查詢的用法",
"createTime":"2020-05-31 11:41:00",
"transitSimple": "test3"
},{
"id":4,
"createUserId":1,
"title":"spring-data-jpa從入到精通(四)",
"image":"https://pic.ibaotu.com/game/title/20200518/5ec24079a85cd.png",
"blogContent":"本博文主要講述了spring-data-jpa的擴展部分",
"createTime":"2020-05-31 11:42:00",
"transitSimple": "test4"
},{
"id":5,
"createUserId":2,
"title":"Spring從入門到精通(一)",
"image":"https://pic.ibaotu.com/game/title/20200518/5ec24079a85cd.png",
"blogContent":"Spring官網閱讀(一)容器及實例化",
"createTime":"2020-05-31 11:42:00",
"transitSimple": "test5"
},{
"id":6,
"createUserId":2,
"title":"Spring從入門到精通(二)",
"image":"https://pic.ibaotu.com/game/title/20200518/5ec24079a85cd.png",
"blogContent":"Spring官網閱讀(二)依賴注入及方法注入",
"createTime":"2020-05-31 11:43:00",
"transitSimple": "test6"
}]
//請求響應數據
{
"status": 200,
"message": "ok",
"data": [
{
"title": "spring-data-jpa從入到精通(二)",
"createUserId": 1,
"blogContent": "本博文對spring-data-jpa框架中的常用註解及其用法進行了實戰講述",
"image": "https://pic.ibaotu.com/game/title/20200518/5ec240798e78c.png",
"createTime": "2020-05-31 11:41:00",
"transitSimple": null,
"id": 2
},
{
"title": "spring-data-jpa從入到精通(三)",
"createUserId": 1,
"blogContent": "本博文主要講述了在spring-boot項目中使用spring-data-jpa進行聯表查詢及複雜的動態查詢的用法",
"image": "https://pic.ibaotu.com/game/title/20200518/5ec240798e78c.png",
"createTime": "2020-05-31 11:41:00",
"transitSimple": null,
"id": 3
},
{
"title": "spring-data-jpa從入到精通(四)",
"createUserId": 1,
"blogContent": "本博文主要講述了spring-data-jpa的擴展部分",
"image": "https://pic.ibaotu.com/game/title/20200518/5ec24079a85cd.png",
"createTime": "2020-05-31 11:42:00",
"transitSimple": null,
"id": 4
},
{
"title": "Spring從入門到精通(一)",
"createUserId": 2,
"blogContent": "Spring官網閱讀(一)容器及實例化",
"image": "https://pic.ibaotu.com/game/title/20200518/5ec24079a85cd.png",
"createTime": "2020-05-31 11:42:00",
"transitSimple": null,
"id": 5
},
{
"title": "Spring從入門到精通(二)",
"createUserId": 2,
"blogContent": "Spring官網閱讀(二)依賴注入及方法注入",
"image": "https://pic.ibaotu.com/game/title/20200518/5ec24079a85cd.png",
"createTime": "2020-05-31 11:43:00",
"transitSimple": null,
"id": 6
}
]
(3) 使用聯合主鍵查詢單條記錄
//請求方法與URL
GET http://localhost:8088/apiBoot/userBlog/findOne?createUserId=2&title=Spring從入門到精通
//請求響應數據
{
"status": 200,
"message": "ok",
"data": {
"title": "Spring從入門到精通(一)",
"createUserId": 2,
"blogContent": "Spring官網閱讀(一)容器及實例化",
"image": "https://pic.ibaotu.com/game/title/20200518/5ec24079a85cd.png",
"createTime": "2020-05-31 11:42:00",
"transitSimple": null,
"id": 5
}
}
3.5 @GeneratedValue
@GeneratedValue爲主鍵生成策略,例如:
public @interface GeneratedValue {
//ID生成策略
GenerationType strategy() default GenerationType.AUTO;
//通過Sequences生成ID,常見的是Oracle數據庫ID的生成規則,需要配合@SequenceGenerator使用
String generator() default "";
}
GenerationType一共有以下4個值:
public enum GenerationType {
//通過表產生主鍵,框架有表模擬序列產生主鍵,使用該策略可以使應用更易於數據庫移植
TABLE,
//通過序列產生主鍵,通過@SequenceGenerator指定序列名,Mysql不支持這種方式
SEQUENCE,
//採用數據庫自增長,一般用於Mysql數據庫
IDENTITY,
//JPA自動選擇合適的策略,是默認
AUTO;
}
3.6 @Basic
@Basic表示屬性是到數據庫表的字段的映射。如果實體的字段上 沒有任何註解,默認即爲@Basic。
public @interface Basic {
//可選,EAGER(默認)爲立即加載,LAZY爲延遲加載(LAZY主要應用在大字段上面)
FetchType fetch() default FetchType.EAGER;
//可選,設置這個字段是否可以爲空,默認爲true
boolean optional() default true;
}
3.7 @Transient
@Transient表示該屬性並非一個到數據庫表的字段的映射,表示 非持久化屬性;
與@Basic作用相反。JPA映射數據庫的時候忽略它。
3.8 @Column
@Column定義該屬性對應數據庫中的列名
public @interface Column {
//數據庫表的列名,可選;如果不填寫,默認爲字段名和實體類中的屬性名相同
String name() default "";
//是否唯一,默認爲false(可選)
boolean unique() default false;
//數據字段是否允許爲null,可選,默認爲true
boolean nullable() default true;
//執行insert操作時是否包含此字段,默認爲true,可選
boolean insertable() default true;
//執行update操作時是否包含此字段,默認true,可選
boolean updatable() default true;
//表示該字段在數據庫中的實際類型
String columnDefinition() default "";
//列對應的表名
String table() default "";
//字段允許的字符長度,默認爲255
int length() default 255;
//精度
int precision() default 0;
//小數點位數
int scale() default 0;
3.9 @Temporal
@Temporal用來設置Date類型的屬性映射到對應精度的字段。
(1)@Temporal(TemporalType.DATE)映射爲日期∥date(只有 日期)
(2)@Temporal(TemporalType.TIME)映射爲日期∥time(只有 時間)
(3)@Temporal(TemporalType.TIMESTAMP)映射爲日期∥date time(日期+時間)
3.10 @Enumerated
@Enumerated很好用,直接映射enum枚舉類型的字段。
(1) 看源碼:
public @interface Enumerated {
//枚舉映射的類型,默認是ORDINAL(枚舉字段的下標)
EnumType value() default EnumType.ORDINAL;
}
public enum EnumType {
//映射枚舉字段的下標
ORDINAL,
//映射枚舉的Name
STRING;
}
(2) 用法:
public enum Gender {
MAIL("男性"),
FEMALE("女性")
;
private String value;
Gender(String value) {
this.value = value;
}
}
@Entity
@Table(name="tb_user")
public class User implements Serializable{
@Enumerated(EnumType.STRING)
@Column(name="user_gender")
private Gender gender;
//....省略其他
}
這時插入兩條數據,數據庫裏面的值是MAIL/FMAIL,而不是“男 性”/“女性”。如果我們用@Enumerated(EnumType.ORDINAL),那麼 這時數據庫裏面的值是0,1。但是實際工作中,不建議用數字下標,
因爲枚舉裏面的屬性值是會不斷新增的,如果新增一個,位置變化了 就慘了。
3.11 @Lob
@Lob 將屬性映射成數據庫支持的大對象類型,支持以下兩種數 據庫類型的字段。
(1)Clob(Character Large Ojects)類型是長字符串類型, java.sql.Clob、Character[]、char[]和String將被映射爲Clob類 型
(2)Blob(Binary Large Objects)類型是字節類型, java.sql.Blob、Byte[]、byte[]和實現了Serializable接口的類型 將被映射爲Blob類型。
(3)Clob、Blob佔用內存空間較大,一般配合 @Basic(fetch=FetchType.LAZY)將其設置爲延遲加載。
小結
本文主要結合源碼和測試用例系統地講解了spring-data-jpa中@Query
註解及javax.persistence包下的基本註解及其用法和需要注意的坑,限於文章篇幅,關於聯表查詢的註解及其使用和JpoRepository擴展詳解及高級查詢用法將放在下一遍文章中完成。
推薦閱讀
SpringBoot之路(二)使用用Spring-Data-JPA訪問數據庫進行基本的CRUD操作
參考文檔
張振華著《Spring Data Jpa從入門到精通》第4章和第5章部分
歡迎新讀者掃描下方二維碼關注筆者的微信公衆號
需要源碼的讀者在公衆號中發送消息“bootApi項目源碼”即可獲取該項目源碼地址