Skip to content

Commit d5a6e0b

Browse files
committed
add crud guide.
1 parent 5bb5c16 commit d5a6e0b

File tree

4 files changed

+273
-41
lines changed

4 files changed

+273
-41
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Sample codes of RSocket Java and Spring RSocket integration.
66

77
[Using Rsocket with Spring Boot](./docs/GUIDE.md)([medium's link](https://medium.com/@hantsy/using-rsocket-with-spring-boot-cfc67924d06a))
88

9+
[Building a CRUD application with RSocket and Spring](./docs/crud.md)
10+
911
## Sample Codes
1012

1113
* **vanilla** rsocket-java sample

crud/server/pom.xml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,6 @@
1818
<properties>
1919
<java.version>11</java.version>
2020
</properties>
21-
<!-- <dependencyManagement>-->
22-
<!-- <dependencies>-->
23-
<!-- <dependency>-->
24-
<!-- <groupId>org.springframework.boot.experimental</groupId>-->
25-
<!-- <artifactId>spring-boot-bom-r2dbc</artifactId>-->
26-
<!-- <version>0.1.0.BUILD-SNAPSHOT</version>-->
27-
<!-- <type>pom</type>-->
28-
<!-- <scope>import</scope>-->
29-
<!-- </dependency>-->
30-
<!-- </dependencies>-->
31-
<!-- </dependencyManagement>-->
3221
<dependencies>
3322
<dependency>
3423
<groupId>org.springframework.boot</groupId>

crud/server/src/main/java/com/example/demo/DemoApplication.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.springframework.data.r2dbc.connectionfactory.init.ConnectionFactoryInitializer;
1515
import org.springframework.data.r2dbc.connectionfactory.init.ResourceDatabasePopulator;
1616
import org.springframework.data.r2dbc.repository.Query;
17+
import org.springframework.data.r2dbc.repository.R2dbcRepository;
1718
import org.springframework.data.relational.core.mapping.Column;
1819
import org.springframework.data.relational.core.mapping.Table;
1920
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
@@ -123,7 +124,7 @@ public Mono<Void> delete(@DestinationVariable("id") Integer id) {
123124

124125
}
125126

126-
interface PostRepository extends ReactiveCrudRepository<Post, Integer> {
127+
interface PostRepository extends R2dbcRepository<Post, Integer> {
127128

128129
@Query("SELECT * FROM posts WHERE title like $1")
129130
Flux<Post> findByTitleContains(String name);

docs/crud.md

Lines changed: 269 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,290 @@
1-
# Creating a CRUD application with Spring RSocket
1+
# Building a CRUD application with RSocket and Spring
22

3+
In [the last post](https://medium.com/@hantsy/using-rsocket-with-spring-boot-cfc67924d06a), we explored the basic RSocket support in Spring and Spring Boot. In this post, we will create a CRUD application which is more close to the real world applications.
34

5+
We will create a *client* and *server* applications to demonstrate the interactions between RSocket client and server side.
46

7+
Firstly let's create the server application.
58

9+
You can simply generate a project template from [Spring initialzr](https://start.spring.io), set the following properties.
610

711

12+
* Build: Maven
13+
* Java: 11
14+
* Spring Boot version: 2.3.0.M3(I preferred the new version for practicing new techniques)
15+
* Dependencies: RSocket, Spring Data R2dbc, H2 Database, Lombok
16+
17+
> If you are new to Spring Data R2dbc, check the post [ Accessing RDBMS with Spring Data R2dbc](https://medium.com/@hantsy/reactive-accessing-rdbms-with-spring-data-r2dbc-d6e453f2837e).
18+
19+
In the server application, we will use RSocket to serve a RSocket server via TCP protocol.
20+
21+
Open the *src/main/resources/application.properties*.
22+
23+
```properties
24+
spring.rsocket.server.port=7000
25+
spring.rsocket.server.transport=tcp
26+
```
27+
28+
Like what I have done in the former posts, firstly create a simple POJO.
29+
30+
```java
31+
@Data
32+
@ToString
33+
@Builder
34+
@NoArgsConstructor
35+
@AllArgsConstructor
36+
@Table("posts")
37+
class Post {
38+
39+
@Id
40+
@Column("id")
41+
private Integer id;
42+
43+
@Column("title")
44+
private String title;
45+
46+
@Column("content")
47+
private String content;
48+
49+
}
50+
```
51+
52+
And create a simple Repository for `Post`.
53+
54+
```java
55+
56+
interface PostRepository extends R2dbcRepository<Post, Integer> {
57+
}
58+
```
59+
60+
Create a `Controller` class to handle the request meassages.
61+
62+
```java
63+
@Controller
64+
@RequiredArgsConstructor
65+
class PostController {
66+
67+
private final PostRepository posts;
68+
69+
@MessageMapping("posts.findAll")
70+
public Flux<Post> all() {
71+
return this.posts.findAll();
72+
}
73+
74+
@MessageMapping("posts.findById.{id}")
75+
public Mono<Post> get(@DestinationVariable("id") Integer id) {
76+
return this.posts.findById(id);
77+
}
78+
79+
@MessageMapping("posts.save")
80+
public Mono<Post> create(@Payload Post post) {
81+
return this.posts.save(post);
82+
}
83+
84+
@MessageMapping("posts.update.{id}")
85+
public Mono<Post> update(@DestinationVariable("id") Integer id, @Payload Post post) {
86+
return this.posts.findById(id)
87+
.map(p -> {
88+
p.setTitle(post.getTitle());
89+
p.setContent(post.getContent());
90+
91+
return p;
92+
})
93+
.flatMap(p -> this.posts.save(p));
94+
}
95+
96+
@MessageMapping("posts.deleteById.{id}")
97+
public Mono<Void> delete(@DestinationVariable("id") Integer id) {
98+
return this.posts.deleteById(id);
99+
}
100+
101+
}
102+
```
103+
104+
Create a `schema.sql` and a `data.sql` to create tables and initialize the data.
105+
106+
```sql
107+
-- schema.sql
108+
CREATE TABLE posts (id SERIAL PRIMARY KEY, title VARCHAR(255), content VARCHAR(255));
109+
```
110+
111+
```sql
112+
-- data.sql
113+
DELETE FROM posts;
114+
INSERT INTO posts (title, content) VALUES ('post one in data.sql', 'content of post one in data.sql');
115+
```
116+
> Note: In the Spring 2.3.0.M3, Spring Data R2dbc is merged in the Spring Data release train. But unfornately, the `ConnectionFactoryInitializer` is NOT ported.
117+
118+
To make the the `schema.sql` and `data.sql` is loaded and executed at the application startup, declare a `ConnectionFactoryInitializer` bean.
119+
120+
```java
121+
@Bean
122+
public ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) {
123+
124+
ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer();
125+
initializer.setConnectionFactory(connectionFactory);
126+
127+
CompositeDatabasePopulator populator = new CompositeDatabasePopulator();
128+
populator.addPopulators(new ResourceDatabasePopulator(new ClassPathResource("schema.sql")));
129+
populator.addPopulators(new ResourceDatabasePopulator(new ClassPathResource("data.sql")));
130+
initializer.setDatabasePopulator(populator);
131+
132+
return initializer;
133+
}
134+
```
135+
136+
Now, you can start the server application.
137+
138+
```bash
139+
mvn spring-boot:run
140+
```
141+
142+
Next, let's move to the client application.
143+
144+
Similarly, generate a project template from [Spring Initalizr](https://start.spring.io), in the *Dependencies* area, make sure you have choose *WebFlux*, *RSocket*, *Lombok*.
145+
146+
The client application is a generic Webflux application, but use `RSocketRequester` to shake hands with the RSocket Servser.
147+
148+
Declare a `RSocketRequester` bean.
149+
150+
```java
151+
@Bean
152+
public RSocketRequester rSocketRequester(RSocketRequester.Builder b) {
153+
return b.dataMimeType(MimeTypeUtils.APPLICATION_JSON)
154+
.connectTcp("localhost", 7000)
155+
.block();
156+
}
157+
```
158+
159+
Create a generic `RestController` and use the `RSocketRequester` to communicate with the RSocket server.
160+
161+
```java
162+
@Slf4j
163+
@RequiredArgsConstructor
164+
@RestController()
165+
@RequestMapping("/posts")
166+
class PostClientController {
167+
168+
private final RSocketRequester requester;
169+
170+
@GetMapping("")
171+
Flux<Post> all() {
172+
return this.requester.route("posts.findAll")
173+
.retrieveFlux(Post.class);
174+
}
175+
176+
@GetMapping("{id}")
177+
Mono<Post> findById(@PathVariable Integer id) {
178+
return this.requester.route("posts.findById." + id)
179+
.retrieveMono(Post.class);
180+
}
181+
182+
@PostMapping("")
183+
Mono<Post> save(@RequestBody Post post) {
184+
return this.requester.route("posts.save")
185+
.data(post)
186+
.retrieveMono(Post.class);
187+
}
188+
189+
@PutMapping("{id}")
190+
Mono<Post> update(@PathVariable Integer id, @RequestBody Post post) {
191+
return this.requester.route("posts.update."+ id)
192+
.data(post)
193+
.retrieveMono(Post.class);
194+
}
195+
196+
@DeleteMapping("{id}")
197+
Mono<Void> delete(@PathVariable Integer id) {
198+
return this.requester.route("posts.deleteById."+ id).send();
199+
}
200+
201+
}
202+
```
203+
204+
Create a POJO `Post` to present the message payload transfered between client and server side.
205+
206+
```java
207+
@Data
208+
@ToString
209+
@Builder
210+
@NoArgsConstructor
211+
@AllArgsConstructor
212+
class Post {
213+
private Integer id;
214+
private String title;
215+
private String content;
216+
}
217+
```
218+
Start up the client application.
219+
220+
Try to test the CRUD opertaions by curl.
221+
8222

9223
```bash
10-
D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts
224+
# curl http://localhost:8080/posts
11225
[{"id":1,"title":"post one in data.sql","content":"content of post one in data.sql"},{"id":2,"title":"Post one","content":"The content of post one"},{"id":3,"title":"Post tow","content":"The content of post tow"}]
12-
D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts/1
13-
{"id":1,"title":"post one in data.sql","content":"content of post one in data.sql"}
14-
D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts/3
15-
{"id":3,"title":"Post tow","content":"The content of post tow"}
16-
D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts/2
17-
{"id":2,"title":"Post one","content":"The content of post one"}
18-
D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts -d '{"title":"my save title", "content":"my content of my post"}'
19-
{"timestamp":"2020-03-13T11:09:52.653+0000","path":"/posts","status":500,"error":"Internal Server Error","message":"In a WebFlux application, form data is accessed via ServerWebExchange.getFormData().","requestId":"efbd0803-5"}curl: (3) [globbing] unmatched close brace/bracket in column 30
20226

21-
D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts -d '{"title":"my save title", "content":"my content of my post"}' -H "Content-Type:application/json"
22-
{"timestamp":"2020-03-13T11:10:18.814+0000","path":"/posts","status":400,"error":"Bad Request","message":"Failed to read HTTP message","requestId":"b02c8cb9-6"}curl: (3) [globbing] unmatched close brace/bracket in column 30
227+
# curl http://localhost:8080/posts/1
228+
{"id":1,"title":"post one in data.sql","content":"content of post one in data.sql"}
23229

24-
D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts -d '{"title":"my save title", "content":"my content of my post"}' -H "Content-Type:application/json" -X POST
25-
{"timestamp":"2020-03-13T11:11:07.798+0000","path":"/posts","status":400,"error":"Bad Request","message":"Failed to read HTTP message","requestId":"96dca4ed-7"}curl: (3) [globbing] unmatched close brace/bracket in column 30
230+
# curl http://localhost:8080/posts/3
231+
{"id":3,"title":"Post tow","content":"The content of post tow"}
26232

27-
D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts -d \"{\"title\":\"my save title\", \"content\":\"my content of my post\"}\" -H "Content-Type:application/json" -X POST
28-
{"timestamp":"2020-03-13T11:12:49.995+0000","path":"/posts","status":400,"error":"Bad Request","message":"Failed to read HTTP message","requestId":"52bb940b-8"}curl: (6) Could not resolve host: save
29-
curl: (6) Could not resolve host: title",
30-
curl: (3) Port number ended with '"'
31-
curl: (6) Could not resolve host: content
32-
curl: (6) Could not resolve host: of
33-
curl: (6) Could not resolve host: my
34-
curl: (3) [globbing] unmatched close brace/bracket in column 6
233+
# curl http://localhost:8080/posts/2
234+
{"id":2,"title":"Post one","content":"The content of post one"}
35235

36-
D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts -d "{\"title\":\"my save title\", \"content\":\"my content of my post\"}" -H "Content-Type:application/json" -X POST
236+
# curl http://localhost:8080/posts -d "{\"title\":\"my save title\", \"content\":\"my content of my post\"}" -H "Content-Type:application/json" -X POST
37237
{"id":4,"title":"my save title","content":"my content of my post"}
38-
D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts/4 -d "{\"title\":\"my save title\", \"content\":\"update my content of my post\"}" -H "Content-Type:application/json" -X PUT
39-
{"id":4,"title":"my save title","content":"update my content of my post"}
40-
D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts
238+
239+
# curl http://localhost:8080/posts
41240
[{"id":1,"title":"post one in data.sql","content":"content of post one in data.sql"},{"id":2,"title":"Post one","content":"The content of post one"},{"id":3,"title":"Post tow","content":"The content of post tow"},{"id":4,"title":"my save title","content":"update my content of my post"}]
42-
D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts/4 -X DELETE
43241

44-
D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts
242+
# curl http://localhost:8080/posts/4 -X DELETE
243+
244+
# curl http://localhost:8080/posts
45245
[{"id":1,"title":"post one in data.sql","content":"content of post one in data.sql"},{"id":2,"title":"Post one","content":"The content of post one"},{"id":3,"title":"Post tow","content":"The content of post tow"}]
46-
D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts
246+
```
247+
248+
As a bonus, try to add a filter to find the posts by keyword.
249+
250+
In the server application, create a new method in the `PostRepository`.
251+
252+
```java
253+
@Query("SELECT * FROM posts WHERE title like $1")
254+
Flux<Post> findByTitleContains(String name);
255+
```
47256

257+
And create a new route to handle this request from client.
48258

259+
```java
260+
class PostController {
261+
//...
262+
@MessageMapping("posts.titleContains")
263+
public Flux<Post> titleContains(@Payload String title) {
264+
return this.posts.findByTitleContains(title);
265+
}
266+
//...
267+
}
49268
```
50269

270+
In the client side, change the `PostClientController` 's `all` method to the following:
271+
272+
```java
273+
class PostClientController {
274+
//...
275+
Flux<Post> all(@RequestParam(name = "title", required = false) String title) {
276+
if (StringUtils.hasText(title)) {
277+
return this.requester.route("posts.titleContains")
278+
.data(title).retrieveFlux(Post.class);
279+
} else {
280+
return this.requester.route("posts.findAll")
281+
.retrieveFlux(Post.class);
282+
}
283+
}
284+
//...
285+
}
286+
```
287+
288+
Now, try to add a `title` request param to access http://localhost:8080/posts.
289+
290+
Get the [source codes](https://github.com/hantsy/rsocket-sample/tree/master/crud) from my Github.

0 commit comments

Comments
 (0)