相關(guān)源碼: spring cloud demo
Spring Data Rest 是基于 Spring Data repositories,分析實體之間的關(guān)系。為我們生成Hypermedia API(HATEOAS)風(fēng)格的Http Restful API接口。
Spring Data Rest 官方首頁中提到了它所具有的特性,比如:
- 根據(jù)model,生成HAL風(fēng)格的restful API
- 根據(jù)model,維護(hù)實體之間的關(guān)系
- 支持分頁
...
諸多的特性,官方文檔都會有提及。這里我們著重關(guān)注在Spring Data Rest中基于JPA維護(hù)實體之間關(guān)系。
資源實體的關(guān)系
- 一個用戶(user)擁有一個身份證(card)
- 一個用戶(user)擁有多輛車(car)
- 一個用戶(user)擁有多門語音(language)
- 一門語言(language)擁有多個用戶(user)
關(guān)系不用在意是否合理,只是為了涵蓋幾個基本的關(guān)系
one to one 關(guān)系
數(shù)據(jù)實體
@Data
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
private Date createAt;
@OneToOne(mappedBy = "user")
private Card card;
}
@Data
@Entity
public class Card {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String card_num;
private Date createAt;
@OneToOne
@JoinColumn(name = "user_id")
@RestResource(path = "user", rel = "user")
private User user;
}
Repository結(jié)構(gòu)
public interface UserRepository extends JpaRepository<User, Long> {
}
public interface CardRepository extends JpaRepository<Card, Long> {
}
通過以上的代碼,Spring Data Rest 就已經(jīng)足夠幫我們維護(hù)其用戶(user)和身份證(card)二者的關(guān)系,并且提供了HAL的接口。就是這么方便!下面,我們使用HAL Browser ,可以更加方便的在瀏覽器中查看接口,以及他們之間的關(guān)系。
HAL Browser 使用
HAL-browser 是基于hal+json的media type的API瀏覽器,Spring Data Rest 提供了集成,pom文件中加個這個。
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>
啟動我們的程序,打開瀏覽器 http://127.0.0.1:8081/api/v1/browser/index.html#/api/v1
api/v1 是我指定的接口前綴,通過配置項 spring.data.rest.base-path 指定
可以看到如下界面
具體的使用這里不再贅述,可以自己點(diǎn)點(diǎn)或者看看這個。
接口調(diào)用
新增一個user
curl -i -X POST -H "Content-Type:application/json" -d "{\"name\":\"Lucy\",\"age\":25}" http://127.0.0.1:8081/api/v1/users
結(jié)果返回:
返回結(jié)果的狀態(tài)碼是201
返回實體內(nèi)容
返回_links資源,分別指向自己和對應(yīng)的card資源的URI
新增一個card
curl -i -X POST -H "Content-Type:application/json" -d "{\"cardNum\":\"num1\"}" http://127.0.0.1:8081/api/v1/cards
建立關(guān)系
創(chuàng)建的兩個實體之后,我們需要建立起二者的關(guān)系。在Spring Data Rest中,二者的關(guān)系綁定,是通過URI來維護(hù),用PUT請求動作。
curl -i -X PUT -H "Content-Type:text/uri-list" -d "http://127.0.0.1:8081/api/v1/users/1" http://127.0.0.1:8081/api/v1/cards/1/user
也可以用這樣維護(hù)
curl -i -X PUT -H "Content-Type:text/uri-list" -d "1" http://127.0.0.1:8081/api/v1/cards/1/user
或者:
curl -i -X PUT -H "Content-Type:text/uri-list" -d "api/v1/users/1" http://127.0.0.1:8081/api/v1/cards/1/user
三者是等價的操作。
如果創(chuàng)建成功,將會返回響應(yīng)碼204,如下圖:
我們來核實下user下的card
curl -i -X GET http://127.0.0.1:8081/api/v1/users/1/card
返回如下:
上圖可以看出,user-card的關(guān)系維護(hù)成功!
以上是通過PUT Card資源的User來維護(hù)二者關(guān)系。外鍵在于Card上,是資源維護(hù)方。反過來就通過User資源的Card維護(hù)是不被允許的。這個是我的一個疑問,沒有深入研究過,一個tip
one to many 關(guān)系
數(shù)據(jù)實體
@Data
@Entity
public class Car {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String carNum;
private Date createAt;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
@Data
@Entity
public class User {
// ...
@OneToMany(mappedBy = "user")
private List<Car> cars = new ArrayList<>();
// ...
}
Repository結(jié)構(gòu)
public interface CarRepository extends JpaRepository<Car, Long> {
}
接口調(diào)用
新增一個car
curl -i -X POST -H "Content-Type:application/json" -d "{\"carNum\":\"A1001\"}" http://127.0.0.1:8081/api/v1/cars
返回如下:
建立關(guān)系
一對多的關(guān)系中,也是通過URI通過PUT請求維護(hù)關(guān)系。
curl -i -X PUT -H "Content-Type:text/uri-list" -d "http://127.0.0.1:8081/api/v1/users/1" http://127.0.0.1:8081/api/v1/cars/1/user
結(jié)果返回:
我們看下User下的Cars
curl -i -X GET http://127.0.0.1:8081/api/v1/users/1/cars
結(jié)果如下:
很明顯,已經(jīng)可以看到User下的Cars。
也是只有通過多的一方維護(hù)關(guān)系
many to many 關(guān)系
數(shù)據(jù)實體
@Data
@Entity
public class Language {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "rel_user_language",
joinColumns = @JoinColumn(name = "language_id", referencedColumnName = "id", nullable = false),
inverseJoinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false))
private List<User> users = new ArrayList<>();
}
@Data
@Entity
public class User {
// ...
@ManyToMany(mappedBy = "users", cascade = CascadeType.ALL)
private List<Language> languages = new ArrayList<>();
//...
}
Repository結(jié)構(gòu)
public interface LanguageRepository extends JpaRepository<Language, Long> {
}
接口調(diào)用
同樣新增方式,多門語言,多個用戶
curl -i -X POST -H "Content-Type:application/json" -d "{\"name\":\"eng\"}" http://127.0.0.1:8081/api/v1/languages
curl -i -X POST -H "Content-Type:application/json" -d "{\"name\":\"chs\"}" http://127.0.0.1:8081/api/v1/languages
curl -i -X POST -H "Content-Type:application/json" -d "{\"name\":\"jp\"}" http://127.0.0.1:8081/api/v1/languages
curl -i -X POST -H "Content-Type:application/json" -d "{\"name\":\"Jack\",\"age\":25}" http://127.0.0.1:8081/api/v1/users
創(chuàng)建關(guān)系
多對多的關(guān)系創(chuàng)建方式比之前兩種更為豐富。
PUT方式添加關(guān)系
curl -i -X PUT -H "Content-Type:text/uri-list" -d "api/v1/users/1" http://127.0.0.1:8081/api/v1/languages/1/users
POST方式添加關(guān)系
curl -i -X POST -H "Content-Type:text/uri-list" -d "api/v1/users/1" http://127.0.0.1:8081/api/v1/languages/2/usersrs
PATCH方式添加關(guān)系
curl -i -X PATCH -H "Content-Type:text/uri-list" -d "api/v1/users/1" http://127.0.0.1:8081/api/v1/languages/3/users
以上三種方式都可以用于創(chuàng)建多對多的關(guān)系,可以查看下:
curl -i -X GET http://127.0.0.1:8081/api/v1/users/1/languages
{
"_embedded" : {
"languages" : [ {
"name" : "eng",
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/1"
},
"language" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/1"
},
"users" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/1/users"
}
}
}, {
"name" : "chs",
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/2"
},
"language" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/2"
},
"users" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/2/users"
}
}
}, {
"name" : "jp",
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/3"
},
"language" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/3"
},
"users" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/3/users"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1/languages"
}
}
}
結(jié)果上是可以看出,三種方式的結(jié)果都成功了。
反過來,通過語言查看用戶
curl -i -X GET http://127.0.0.1:8081/api/v1/languages/3/users
{
"_embedded" : {
"users" : [ {
"name" : "Lucy",
"age" : 25,
"createAt" : null,
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1"
},
"user" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1"
},
"card" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1/card"
},
"languages" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1/languages"
},
"cars" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1/cars"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/3/users"
}
}
}
結(jié)果也是我們所想要的。
PUT POST PATCH 三者之間區(qū)別
PATCH 請求的作用等同于 POST 請求,而他們二者有點(diǎn)不同于PUT請求。大家都知道 PUT 請求是整體替換,而PATCH是局部更新。在Spring Data Rest 中 PATCH 表示添加,而不是覆蓋,PUT請求是完全覆蓋。
我們在原來的數(shù)據(jù)基礎(chǔ)上給User2(Jack)添加一門Language:
curl -i -X PATCH -H "Content-Type:text/uri-list" -d "api/v1/users/2" http://127.0.0.1:8081/api/v1/languages/1/users
之后,我們查看下 id 為1的 language 的users
curl -i -X GET http://127.0.0.1:8081/api/v1/languages/1/users
{
"_embedded" : {
"users" : [ {
"name" : "Lucy",
"age" : 25,
"createAt" : null,
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1"
},
"user" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1"
},
"card" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1/card"
},
"languages" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1/languages"
},
"cars" : {
"href" : "http://127.0.0.1:8081/api/v1/users/1/cars"
}
}
}, {
"name" : "Jack",
"age" : 25,
"createAt" : null,
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2"
},
"user" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2"
},
"card" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2/card"
},
"languages" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2/languages"
},
"cars" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2/cars"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/1/users"
}
}
}
可以看到擁有兩個user。這里可以看出 PATCH 的作用添加了一個item。
接下來,我們再調(diào)用 PUT 請求,更新下。
curl -i -X PUT -H "Content-Type:text/uri-list" -d "api/v1/users/2" http://127.0.0.1:8081/api/v1/languages/1/users
與上一個請求的唯一區(qū)別是用了PUT做更新
再GET下User,結(jié)果如下:
{
"_embedded" : {
"users" : [ {
"name" : "Jack",
"age" : 25,
"createAt" : null,
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2"
},
"user" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2"
},
"card" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2/card"
},
"languages" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2/languages"
},
"cars" : {
"href" : "http://127.0.0.1:8081/api/v1/users/2/cars"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8081/api/v1/languages/1/users"
}
}
}
對比很明顯,PUT請求覆蓋了之前的數(shù)據(jù),只留下了一個Jack的user關(guān)聯(lián)。這就是PUT 和 PATCH 的區(qū)別。
tip1:POST的效果同PATCH 不做多說明。
tip2:多對多的關(guān)系維護(hù)中,維護(hù)方的資源來維護(hù)二者關(guān)系。
Spring Data Rest Events
Spring Data Rest Events 提供了AOP方式的開發(fā),定義了10種不同事件。
- 資源保存前 @HandleBeforeCreate
- 資源保存后 @HandleAfterCreate
- 資源更新前 @HandleBeforeSave
- 資源更新后 @HandleAfterSave
- 資源刪除前 @HandleBeforeDelete
- 資源刪除后 @HandleAfterDelete
- 關(guān)系創(chuàng)建前 @HandleBeforeLinkSave
- 關(guān)系創(chuàng)建后 @HandleAfterLinkSave
- 關(guān)系刪除前 @HandleBeforeLinkDelete
- 關(guān)系刪除后 @HandleAfterLinkDelete
不同的事件觸發(fā)的場景不同,我們可以自定義這些事件內(nèi)容來完成我們的業(yè)務(wù)。這個以后再說...有興趣可以看看我的樣例代碼