در این بخش بوسیله WebFlux یک REST Application ساده همراه با تعدادی از عملیات CRUD مینویسیم و بوسیله R2DBC عملیات دیتابیسی را بصورت Asynchronous اجرا خواهیم کرد
R2DBC چیست ؟
امروزه توسعه برنامه ها بر اساس معماری React در حال رشد است ولی یکی از مشکلات توسعه بر اساس React اتصال به دیتابیس مبتنی بر Asynchronous در جاوا است که از معماری JDBC ناشی میشود و نیاز است راه حل جدیدتر برای حل آن داشته باشیم
برای حل این مشکل دو استاندارد در جاوا معرفی شده است ADBC که توسط اوراکل توسعه داده میشد که بنظر میاد متوقف شده باشد
و مورد بعدی R2DBC است (Reactive Relational Database Connectivity) که حاصل تلاش تیمی از شرکت pivotal و سایر شرکت ها است که این پروژه هنوز در مرحله Beta بسر میبرد و درایور هایی برای H2 , MSSQL , Postgres دارد
برای راه اندازی R2DBC نیاز است کتابخانه انرا اضافه کنیم :
<dependency> <groupId>io.r2dbc</groupId> <artifactId>r2dbc-spi</artifactId> <version>0.8.0.M7</version> </dependency> <dependency> <groupId>io.r2dbc</groupId> <artifactId>r2dbc-h2</artifactId> <version>0.8.0.M7</version> </dependency>
از آنجا که مخزن Maven هنوز Artifact مربوط به R2DBC را ندارد نیاز است که مخازن دیگری را به پروژه Spring اضافه کنیم :
<repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories>
Connection Factory :
اولین چیزی که موقع کار با R2DBC نیاز داریم ایجاد ابجکتی از ConnectionFactory است که مشابه DataSource در JDBC است راحتترین راه ایجاد ConnectionFactory استفاده از کلاس ConnectionFactories است که متد static آن نیاز دارد که یک ابجکت از ConnectionFactoryOptions را دریافت کند و بعد یک ConnectionFactory بسازد و برگرداند
از آنجا که ما یک نمونه از ConnectionFactory نیاز داریم یک متد Bean@ ایجاد میکنیم :
@Bean public ConnectionFactory connectionFactory(R2DBCConfigurationProperties properties) { ConnectionFactoryOptions baseOptions = ConnectionFactoryOptions.parse(properties.getUrl()); Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); if (!StringUtil.isNullOrEmpty(properties.getUser())) { ob = ob.option(USER, properties.getUser()); } if (!StringUtil.isNullOrEmpty(properties.getPassword())) { ob = ob.option(PASSWORD, properties.getPassword()); } return ConnectionFactories.get(ob.build()); }
در این کد ما یکسری مقادیر property را با استفاده از یک کلاس Helper استخراج کردیم
این کلاس با کمک ConfigurationProperties@ یکسری property ها را از داخل فایل properties که مربوط به تنظیمات Spring Boot است را استخراج کرده ایم و توانستیم داخل کد استفاده کنیم و در نهایت ابجکت ConnectionFactoryOptions را ساخته ایم
کلاس ConnectionFactoryOptions با استفاده از پترن builder این option ها را دریافت میکند مانند USERNAME و PASSWORD
راه دیگر استفاده از متد ()parse است **
و در نهایت با استفاده از متد factory که اینجا ConnectionFactories::get است یک ابجکت از نوع ConnectionFactory ساخته میشود
ساختار R2DBC URL Connection :
r2dbc:h2:mem://./testdb
:r2dbc اسکیمای ثابت است و اگر از ssl استفاده مکنیم بدین شکل میشود :r2dbcs
:h2 مربوط به درایور دیتابیس استفاده شده است
:mem پروتکل تعریف شده درایور برای استفاده است که ما نوع in-memory را استفاده کردیم
testdb/.// طبق چیزی که در پروتکل درایور تعریف شده است مربوط به اپشن های اضافی میباشد مانند هاست و نام دیتابیس و ...
نحوه اجرای دستورات:
همانند JDBC دستورات SQL به دیتابیس ارسال میشوند و در قالب مجموعه جواب هایی نتایج پردازش میشوند و از آنجا که R2DBC از React API استفاده میکند به نوع React Stream نیز بستگی دارد مانند Publisher و Subscriber
از آنجا که استفاده مستقیم از این نوع ها سخت است ما از انواع Mono و Flux استفاده میکنیم که کمک میکند کد تمیز تر و کمتری داشته باشیم
در ادامه خواهیم دید که چگونه یک React DAO با یک کلاس معمولی Entity میتواند کار کند
کلاس Entity زیر را در نظر بگیرید :
public class Account { private Long id; private String iban; private BigDecimal balance; // ... getters and setters omitted }
نحوه ساخت Connection :
قبل از اینکه دستوراتی را به دیتابیس ارسال کنیم به Connection نیاز داریم، در قبل دیدیم که چگونه یک Bean برای ساخت ConnectionFactory ایجاد کنیم و از همان برای ایجاد Connection استفاده خواهیم کرد
نکته ای که وجود دارد این است که بجای دریافت یک Connection معمولی (آنچه که در JDBC داشتیم) اینجا یک Publisher از یک Connection داریم
کلاس ReactAccountDao ای طراحی کردیم که با Component@ ست شده است و ConnectionFactory را توسط Constructor تزریق میکند و در پایین متد findById را میبینیم که از ConnectionFactory استفاده میکند :
public Mono<Account>> findById(Long id) { return Mono.from(connectionFactory.create()) .flatMap(c -> // use the connection ) // ... downstream processing omitted }
در این کد با استفاده از ConnectionFactory در ابجکت Mono که منبع اصلی Event Stream است یک Publisher فراهم کرده ایم
حال ببنیم چگونه دستورات SQL را میتوان ارسال کرد :
.flatMap( c -> Mono.from(c.createStatement("select id,iban,balance from Account where id = $1") .bind("$1", id) .execute()) .doFinally((st) -> close(c)) )
متد createStatement ابجکت ConnectionFactory یک رشته SQL دریافت میکند که میتواند دارای پارامتر هایی باشد که این متد بصورت Synchronous است و میتوان مقادیری را به پارامتر ها bind کرد به سینتکس دریافت پارامتر توجه کنید که با $ و یک عدد مشخص است و مربوط به سینتکس H2 است و ممکن است در دیتابیس دیگر کوئری طور دیگری این پارامتر ها را دریافت کند
با متد execute یک Publisher برای ابجکت Result دریافت خواهیم کرد و در نهایت با متد doFinally به Mono میگوییم که در نهایت میخواهیم connection را close کنیم
پردازش نتایج کوئری :
مرحله بعدی پردازش ابجکت های Result و تولید Stream ای از <ResponseEntity<Account است از آنجا که میدانیم هر نتیجه ای که بوسیله P.key فراخوانی شود شامل یک نتیجه خواهد بود یک Mono stream را برمیگردانیم
تبدیل واقعی داخل متد ()map بدین صورت انجام میشود :
.map(result -> result.map((row, meta) -> new Account(row.get("id", Long.class), row.get("iban", String.class), row.get("balance", BigDecimal.class)))) .flatMap(p -> Mono.from(p));
ورودی متد map روی ابجکت result شامل دو نوع است که نوع اول row که مقادیر column ها را میتوان از آن دریافت کرد و ابجکت entity را ساخت و دومی meta نمونه ای از RowMetadata است که اطلاعات جانبی مربوط به row مثل column name و نوع انها را در اختیار ما میگذارد
متد map اصلی مسول ایجاد <<Mono<Producer<Account است ولی ما یک ابجکت <Mono<Account نیاز داریم که اینکار توسط متد flatMap در انتها انجام میگیرد و producer را به Mono تبدیل میکند
عملیات Batch :
ما میتوانیم دسته از عملیات دیتابیسی را یکباره انجام دهیم ولی دیگر مانند قبل عمل bind کردن مقادیر به پارامتر ها را نداریم و بیشتر برای عملیات ETL استفاده میشود
@Bean public CommandLineRunner initDatabase(ConnectionFactory cf) { return (args) -> Flux.from(cf.create()) .flatMap(c -> Flux.from(c.createBatch() .add("drop table if exists Account") .add("create table Account(" + "id IDENTITY(1,1)," + "iban varchar(80) not null," + "balance DECIMAL(18,2) not null)") .add("insert into Account(iban,balance)" + "values('BR430120980198201982',100.00)") .add("insert into Account(iban,balance)" + "values('BR430120998729871000',250.00)") .execute()) .doFinally((st) -> c.close()) ) .log() .blockLast(); }
در این نوع از عملیات انتظار نداریم که نتیجه ای برگشت داده شود و میخواهیم که تنها دستورات بخوبی اجرا شوند ولی اگر نیاز بود نتیجه ای داشته باشیم کافی است یک مرحله دیگر به Stream اضافه کنیم تا نتایج پردازش و آماده شوند
وضعیت Transaction ها :
در اینجا هم انتظار میرود همانند JDBC بتوانیم Transaction داشته باشیم و روی آن مدیریت کنیم ولی باید یادمان باشد که یک فرق بزرگ با JDBC Transaction وجود دارد و آن Asynchronous بودن Transaction است و Publisher ای که باید در نقطه ای مناسب به Stream اضافه کنیم
public Mono<Account> createAccount(Account account) { return Mono.from(connectionFactory.create()) .flatMap(c -> Mono.from(c.beginTransaction()) .then(Mono.from(c.createStatement("insert into Account(iban,balance) values($1,$2)") .bind("$1", account.getIban()) .bind("$2", account.getBalance()) .returnGeneratedValues("id") .execute())) .map(result -> result.map((row, meta) -> new Account(row.get("id", Long.class), account.getIban(), account.getBalance()))) .flatMap(pub -> Mono.from(pub)) .delayUntil(r -> c.commitTransaction()) .doFinally((st) -> c.close())); }
به نقاطی که Transaction را start و commit کردیم توجه کنید
برای commit کردن Transaction از متد delayUntil استفاده شده و این بخاطر این است که میخواهیم مطمئن شویم که رکورد دریافت شده همان رکوردی است که بعد از commit ذخیره شده
نمونه ای از استفاده DAO :
حالا که ما یک React DAO داریم بیاییم یک Spring WebFlux Application بسازیم و ببینیم چطور میتوان از React DAO استفاده کنیم :
@RestController public class AccountResource { private final ReactiveAccountDao accountDao; public AccountResource(ReactiveAccountDao accountDao) { this.accountDao = accountDao; } @GetMapping("/accounts/{id}") public Mono<ResponseEntity<Account>> getAccount(@PathVariable("id") Long id) { return accountDao.findById(id) .map(acc -> new ResponseEntity<>(acc, HttpStatus.OK)) .switchIfEmpty(Mono.just(new ResponseEntity<>(null, HttpStatus.NOT_FOUND))); } // ... other methods omitted }
در این کد ما با استفاده از مقدار برگشتی DAO که یک Mono است یک ResponseEntity همراه با کد وضعیت OK ساختیم با اضافه کردن کد برای کد وضعیت موفقیت آمیز میتوانیم وقتی که Entity با Id مربوطه پیدا نشد کد وضعیت NOT_FOUND را هم ارسال کنیم