PostgreSQL transaction
Request-scoped PgDbContext ile birden fazla repository işlemini aynı transaction’da yürütün.
Bir workflow iki repository çağrısı yapıyorsa aynı PgDbContext instance’ını paylaşmaları transaction’ı birlikte kullanmalarını sağlar. Bunun için workflow ve repository servisleri Scoped olmalıdır.
Ön koşullar#
PostgreSQL kurulumunu tamamlayın. Örnekte users(id BIGINT, name TEXT) tablosu ve 1, 2 ID’li kayıtlar önceden bulunmalıdır. Kod tablo oluşturmaz.
Controller’dan transaction’a#
use std::sync::Arc;
use lilyrs::http_api::Injectable;
use lilyrs::http_api::{
controller, Controller, ControllerInitError, ControllerTrait, Extensions,
ExecutionCancellation, HttpApiError, NoContent, Service, ServiceTrait,
};
use lilyrs::postgresql::{diesel, diesel_async::RunQueryDsl, PgDbContext, PgResult};
#[derive(Injectable)]
#[service(lifetime = "Scoped")]
struct UserRepository {
#[inject]
context: Arc<PgDbContext>,
}
impl ServiceTrait for UserRepository {}
impl UserRepository {
async fn rename(&self, id: i64, name: String) -> PgResult<()> {
self.context.with_connection(move |connection, _| Box::pin(async move {
diesel::sql_query("UPDATE users SET name = $1 WHERE id = $2")
.bind::<diesel::sql_types::Text, _>(name)
.bind::<diesel::sql_types::BigInt, _>(id)
.execute(connection).await?;
Ok(())
}), None).await
}
}
#[derive(Injectable)]
#[service(lifetime = "Scoped")]
struct UserService {
#[inject]
context: Arc<PgDbContext>,
#[inject]
users: Arc<UserRepository>,
}
impl ServiceTrait for UserService {}
impl UserService {
async fn rename_pair(&self, cancellation: ExecutionCancellation) -> PgResult<()> {
let users = self.users.clone();
self.context.transaction(move |_| async move {
users.rename(1, "Ada".into()).await?;
users.rename(2, "Deniz".into()).await?;
Ok(())
}, Some(cancellation)).await
}
}
#[derive(Controller)]
#[base_path("/postgresql-users")]
struct UsersController;
#[lilyrs::http_api::async_trait::async_trait]
impl ControllerTrait for UsersController {
async fn new(_: Arc<Extensions>) -> Result<Self, ControllerInitError> { Ok(Self) }
}
#[controller]
impl UsersController {
#[post("/rename-pair")]
async fn rename_pair(&self, Service(service): Service<UserService>, cancellation: ExecutionCancellation)
-> Result<NoContent, HttpApiError>
{
service.rename_pair(cancellation).await
.map_err(|_| HttpApiError::ServiceUnavailable("database operation failed".into()))?;
Ok(NoContent)
}
}POST /postgresql-users/rename-pair iki güncellemeyi aynı transaction’da çalıştırır ve başarıda 204 döner. Dış transaction’a verilen cancellation, iç repository çağrılarındaki None değerinden önceliklidir.
Commit, rollback ve hata#
Workflow Ok dönerse commit, Err dönerse rollback seçilir. Query hatasını yakalayıp Ok dönmek rollback-only durumunu ortadan kaldırmaz. Aynı context’te paralel/üst üste işlem ContextBusy üretir; nested transaction reddedilir.
Örnek DB hatalarını güvenli 503 olarak eşler. Gerçek API’nizde not-found, conflict ve validation gibi domain sonuçlarını ayrı HTTP hata sözleşmeleriyle ifade edin.
Cancellation sınırları#
PgDbContext callback’i E: From<PgError> + Send + 'static sağlayan uygulama hatalarını koruyabilir. Bu generic hata desteği bütün PostgreSQL API’lerine ait değildir.
Başlamış COMMIT, sonradan gelen cancellation ile geri alınmış sayılamaz. PgDatabaseService::transaction future’ını dış select! ile bırakmak da tamamlanmış rollback kanıtı değildir. Scoped context’in owner tarafından tutulan cleanup yolu ile doğrudan connection callback yolunun garantilerini ayırın.
Bu sayfanın kaynakları v0.1.0
Anlatım, incelenen framework commit’ine dayanır.