響應式關係數據庫處理R2DBC

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"簡介","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"之前我們提到過,對於底層的數據源來說,MongoDB, Redis, 和 Cassandra 可以直接以reactive的方式支持Spring Data。而其他很多關係型數據庫比如Postgres, Microsoft SQL Server, MySQL, H2 和 Google Spanner 則可以通過使用R2DBC 來實現對reactive的支持。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"今天我們就來具體講解一下R2DBC的使用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"R2DBC介紹","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"之前我們介紹了Reactor還有基於其之上的Spring WebFlux框架。包括vert.x,rxjava等等reactive技術。我們實際上在應用層已經有很多優秀的響應式處理框架。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是有一個問題就是所有的框架都需要獲取底層的數據,而基本上關係型數據庫的底層讀寫都還是同步的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了解決這個問題,出現了兩個標準,一個是oracle提出的 ADBC (Asynchronous Database Access API),另一個就是Pivotal提出的R2DBC (Reactive Relational Database Connectivity)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"R2DBC是基於Reactive Streams標準來設計的。通過使用R2DBC,你可以使用reactive API來操作數據。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時R2DBC只是一個開放的標準,而各個具體的數據庫連接實現,需要實現這個標準。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"今天我們以r2dbc-h2爲例,講解一下r2dbc在Spring webFlux中的使用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"項目依賴","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們需要引入r2dbc-spi和r2dbc-h2兩個庫,其中r2dbc-spi是接口,而r2dbc-h2是具體的實現。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時我們使用了Spring webflux,所以還需要引入spring-boot-starter-webflux。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具體的依賴如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" \n \n io.r2dbc\n r2dbc-h2\n ${r2dbc-h2.version}\n \n\n \n org.springframework.boot\n spring-boot-starter-webflux\n \n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"創建ConnectionFactory","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ConnectionFactory是數據庫連接的一個具體實現,通過ConnectionFactory我們可以創建到數據庫的連接。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先看一下數據庫的配置文件,爲了方便起見,這裏我們使用的是內存數據庫H2 :","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"r2dbc.url=r2dbc:h2:mem://./r2dbc\nr2dbc.user=sa\nr2dbc.password=password\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一個url指定的是數據庫的連接方式,下面兩個是數據庫的用戶名和密碼。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我們看一下,怎麼通過這些屬性來創建ConnectionFactory:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" @Bean\n public ConnectionFactory connectionFactory() {\n ConnectionFactoryOptions baseOptions = ConnectionFactoryOptions.parse(url);\n ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions);\n if (!StringUtil.isNullOrEmpty(user)) {\n ob = ob.option(USER, user);\n }\n if (!StringUtil.isNullOrEmpty(password)) {\n ob = ob.option(PASSWORD, password);\n }\n return ConnectionFactories.get(ob.build());\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過url可以parse得到ConnectionFactoryOptions。然後通過ConnectionFactories的get方法創建ConnectionFactory。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果我們設置了USER或者PASSWORD,還可以加上這兩個配置。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"創建Entity Bean","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏,我們創建一個簡單的User對象:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"@Data\n@NoArgsConstructor\n@AllArgsConstructor\npublic class Users {\n\n private Long id;\n private String firstname;\n private String lastname;\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"初始化數據庫","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然H5有很多更加簡單的方式來初始化數據庫,比如直接讀取SQL文件,這裏爲了說明R2DBC的使用,我們使用手動的方式來創建:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" @Bean\n public CommandLineRunner initDatabase(ConnectionFactory cf) {\n\n return (args) ->\n Flux.from(cf.create())\n .flatMap(c ->\n Flux.from(c.createBatch()\n .add(\"drop table if exists Users\")\n .add(\"create table Users(\" +\n \"id IDENTITY(1,1),\" +\n \"firstname varchar(80) not null,\" +\n \"lastname varchar(80) not null)\")\n .add(\"insert into Users(firstname,lastname)\" +\n \"values('flydean','ma')\")\n .add(\"insert into Users(firstname,lastname)\" +\n \"values('jacken','yu')\")\n .execute())\n .doFinally((st) -> c.close())\n )\n .log()\n .blockLast();\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的代碼中,我們使用c.createBatch()來向數據庫插入一些數據。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了createBatch,還可以使用create來創建單個的執行語句。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"獲取所有的用戶","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Dao中,我們提供了一個findAll的方法:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" public Flux findAll() {\n\n return Mono.from(connectionFactory.create())\n .flatMap((c) -> Mono.from(c.createStatement(\"select id,firstname,lastname from users\")\n .execute())\n .doFinally((st) -> close(c)))\n .flatMapMany(result -> Flux.from(result.map((row, meta) -> {\n Users acc = new Users();\n acc.setId(row.get(\"id\", Long.class));\n acc.setFirstname(row.get(\"firstname\", String.class));\n acc.setLastname(row.get(\"lastname\", String.class));\n return acc;\n })));\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡單解釋一下上面的使用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲是一個findAll方法,我們需要找出所有的用戶信息。所以我們返回的是一個Flux而不是一個Mono。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"怎麼從Mono轉換成爲一個Flux呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏我們使用的是flatMapMany,將select出來的結果,分成一行一行的,最後轉換成爲Flux。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Prepare Statement","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了防止SQL注入,我們需要在SQL中使用Prepare statement:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" public Mono findById(long id) {\n\n return Mono.from(connectionFactory.create())\n .flatMap(c -> Mono.from(c.createStatement(\"select id,firstname,lastname from Users where id = $1\")\n .bind(\"$1\", id)\n .execute())\n .doFinally((st) -> close(c)))\n .map(result -> result.map((row, meta) ->\n new Users(row.get(\"id\", Long.class),\n row.get(\"firstname\", String.class),\n row.get(\"lastname\", String.class))))\n .flatMap( p -> Mono.from(p));\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看下我們是怎麼在R2DBC中使用prepare statement的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"事務處理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我們看一下怎麼在R2DBC中使用事務:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" public Mono createAccount(Users account) {\n\n return Mono.from(connectionFactory.create())\n .flatMap(c -> Mono.from(c.beginTransaction())\n .then(Mono.from(c.createStatement(\"insert into Users(firstname,lastname) values($1,$2)\")\n .bind(\"$1\", account.getFirstname())\n .bind(\"$2\", account.getLastname())\n .returnGeneratedValues(\"id\")\n .execute()))\n .map(result -> result.map((row, meta) ->\n new Users(row.get(\"id\", Long.class),\n account.getFirstname(),\n account.getLastname())))\n .flatMap(pub -> Mono.from(pub))\n .delayUntil(r -> c.commitTransaction())\n .doFinally((st) -> c.close()));\n\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的代碼中,我們使用了事務,具體的代碼有兩部分:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"c -> Mono.from(c.beginTransaction())\n.delayUntil(r -> c.commitTransaction())\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"開啓是的時候需要使用beginTransaction,後面提交就需要調用commitTransaction。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"WebFlux使用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,我們需要創建WebFlux應用來對外提供服務:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" @GetMapping(\"/users/{id}\")\n public Mono> getUsers(@PathVariable(\"id\") Long id) {\n\n return usersDao.findById(id)\n .map(acc -> new ResponseEntity<>(acc, HttpStatus.OK))\n .switchIfEmpty(Mono.just(new ResponseEntity<>(null, HttpStatus.NOT_FOUND)));\n }\n\n @GetMapping(\"/users\")\n public Flux getAllAccounts() {\n return usersDao.findAll();\n }\n\n @PostMapping(\"/createUser\")\n public Mono> createUser(@RequestBody Users user) {\n return usersDao.createAccount(user)\n .map(acc -> new ResponseEntity<>(acc, HttpStatus.CREATED))\n .log();\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"執行效果","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,我們運行一下代碼,執行下users:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" curl \"localhost:8080/users\" \n[{\"id\":1,\"firstname\":\"flydean\",\"lastname\":\"ma\"},{\"id\":2,\"firstname\":\"jacken\",\"lastname\":\"yu\"}]% \n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"完美,實驗成功。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文的代碼:","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/ddean2009/learn-reactive/tree/master/webflux-with-r2dbc","title":null},"content":[{"type":"text","text":"webflux-with-r2dbc","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"本文作者:flydean程序那些事","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"本文鏈接:","attrs":{}},{"type":"link","attrs":{"href":"http://www.flydean.com/r2dbc-introduce/","title":null},"content":[{"type":"text","text":"http://www.flydean.com/r2dbc-introduce/","attrs":{}}],"marks":[{"type":"italic"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"本文來源:flydean的博客","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"歡迎關注我的公衆號:「程序那些事」最通俗的解讀,最深刻的乾貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章