Eski bir belge kaydını reddetmek için düzenleyicinin başlangıçta yüklediği revizyonu güncelleme koşuluna ekleyin ve ardından revizyonu atomik olarak artırın. Belge daha önceki bir işlemde yüklenmişse SERIALIZABLE tek başına bu gereksinimi ifade edemez.
Bu eğitim, Terminal’de Mac kullanan ve PostgreSQL tabanlı bir makale düzenleyicisinin güncelliğini yitirmiş yeni metni reddetmesini sağlaması gereken geliştiricilere yöneliktir. Sentetik makale satırları ve eksiksiz bir node-postgres gösterimi kullanır. Amaç nettir: yalnızca veritabanında kayıtlı revizyon, gönderilen taslağın dayandığı revizyonla hâlâ eşleşiyorsa kaydetmek.
SERIALIZABLE güncelliğini yitirmiş bir değişikliğe neden izin verebilir?
PostgreSQL, serileştirilebilir yürütmeyi, tamamlanan işlemleri bir sırayla tek tek çalıştırmakla eşdeğer bir etki üretecek şekilde tanımlar. Bu güvence, veritabanı işlemleri ve bunların operasyonlarıyla ilgilidir. PostgreSQL: Transaction Isolation.
Şu açıklayıcı sıralamayı ele alalım:
- Bir düzenleyici 1. makaleyi 1. revizyonda yükler ve okuma işlemi sona erer.
- Başka bir kayıt makale metnini değiştirir ve 2. revizyonu işler.
- İlk düzenleyici, 1. revizyona dayanan yeni metni gönderir.
- Yeni bir serileştirilebilir işlem, yalnızca kimliğini kullanarak 1. makaleyi günceller.
Son işlem başarılı olabilir: işlemler zaten sıralı biçimde yürütülmüştür. Yalnızca id = 1 koşuluna bağlı bir güncellemede, gönderilen metnin 1. revizyonu gerektirdiğini belirten hiçbir şey yoktur. Bu, belgelenmiş serileştirilebilirlik tanımına bir istisna değil, onun bir uygulamasıdır.
Örtüşen işlemler farklıdır. Eşzamanlı bir güncelleme anlık görüntüsüyle çakıştığında veya bağımlılıklar serileştirilmiş yürütmeyi engellediğinde PostgreSQL serileştirilebilir bir işlemi reddedebilir. Anlık görüntü, işlem denetimi dışındaki ilk ifadeyle oluşturulur; daha önce yalnızca BEGIN yürütmek, düzenleyicinin daha önceki okumasını korumaz. PostgreSQL: Transaction Isolation.
Bu nedenle tanılayıcı soru şudur: Kaydetme işlemi, düzenleyicinin yüklediği revizyonu nerede açıkça şart koşuyor? Yanıt hiçbir yerse yalıtım düzeyini değiştirmek bu eksik koşulu sağlamaz.
Revizyon kontrolü neyi garanti etmeli?
Yüklenen her taslağı, düzenlenebilir metni ve temel revizyonundan oluşan bir çift olarak ele alın. Kaydetme işlemi makale kimliğini, yeni metni ve değişmeden kalan temel revizyonu taşır. Veritabanı güncellemesi hem kimlikle hem de bu revizyonla eşleşmeli, metni değiştirmeli ve kayıtlı revizyonu aynı ifadede artırmalıdır.
Örneğin WHERE id = $1 AND revision = $2 koşulu ön gereksinimi ifade ederken SET body = $3, revision = revision + 1 geçişi gerçekleştirir. PostgreSQL koşulu sağlayan satırları günceller; RETURNING ise yalnızca gerçekten güncellenen satırlardan değerler döndürür. Sıfır satırın güncellendiği bir ifade yine de başarılı bir SQL komutudur; bu nedenle uygulama sonucu açıkça yorumlamalıdır. PostgreSQL: UPDATE.
Bu örüntü genellikle iyimser eşzamanlılık denetimi veya karşılaştır-ve-değiştir olarak adlandırılır. Bu makalenin tasarımında, düzenlenebilir makale içeriğini değiştiren her işlem revizyonu ilerletmelidir. Revizyonları NULL olamaz biçimde tutun, sıfırlamayın ve eski bir revizyon belirteciyle makale kimliğini yeniden kullanmayın. Aksi hâlde eşitlik, eski bir taslağın hâlâ güncel olduğu izlenimini yanlış biçimde verebilir.
Önce ayrı bir okuma yapıp ardından yalnızca kimliği kullanan bir güncelleme, gereksinimi yazma işleminin dışında bırakır. Benzer şekilde, kaydetmeden hemen önce en yeni revizyonu getirip gönderilen revizyonun yerine koymak isteğin anlamını değiştirir: düzenleyicinin temel almadığı bir sürümün üzerine yazılmasına izin verir.
Mac’inizde eksiksiz bir sentetik örnek çalıştırın
Çalışan bir PostgreSQL örneğine, mevcut bir deneme veritabanına ve npm içeren güncel veya desteklenen bir LTS Node.js kurulumuna sahip olun. Terminal’i açın, bir çalışma klasörü oluşturun ve belgelenmiş npm install pg komutuyla sürücüyü yükleyin. node-postgres: Introduction and Install.
mkdir revision-write-demo
cd revision-write-demo
npm install pg
Aşağıdaki eksiksiz programı bu klasörde revision-demo.mjs adıyla kaydedin. Belgelenmiş ve ESM ile uyumlu varsayılan içe aktarmayı kullanır. Hem Client hem de Pool, PostgreSQL bağlantı ayarlarını ortam değişkenlerinden okuyabilir; çalıştırmadan önce bu ayarları mevcut deneme veritabanınız için yapılandırın. node-postgres: ESM, node-postgres: Connecting.
Program iki sentetik makaleyi karşılaştırır. 1. makaleye koşulsuz iki serileştirilebilir kayıt uygulanır. 2. makaleye, aynı başlangıç revizyonunu kullanan ve revizyonu kontrol eden iki kayıt uygulanır. Geçici tablosu, gösterimi kalıcı makale tablolarından ayrı tutar. JavaScript sözdizimi Node.js ile denetlendi; veritabanı gösterimi araştırma ortamında çalıştırılmadı.
import pg from 'pg';
import assert from 'node:assert/strict';
const { Client } = pg;
const client = new Client();
async function saveArticle(id, expectedRevision, body) {
for (let attempt = 1; attempt <= 3; attempt += 1) {
let active = false;
try {
await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
active = true;
const result = await client.query(
`UPDATE revision_demo_articles
SET body = $3, revision = revision + 1
WHERE id = $1 AND revision = $2
RETURNING id, body, revision`,
[id, expectedRevision, body]
);
if (result.rowCount === 0) {
await client.query('ROLLBACK');
active = false;
return { status: 'stale-or-missing' };
}
await client.query('COMMIT');
active = false;
return { status: 'saved', article: result.rows[0] };
} catch (error) {
if (active) await client.query('ROLLBACK');
if (error.code !== '40001' || attempt === 3) throw error;
}
}
}
await client.connect();
try {
await client.query(`
CREATE TEMP TABLE revision_demo_articles (
id integer PRIMARY KEY,
body text NOT NULL,
revision integer NOT NULL CHECK (revision > 0)
)
`);
await client.query(
`INSERT INTO revision_demo_articles (id, body, revision)
VALUES (1, 'Opening draft', 1), (2, 'Opening draft', 1)`
);
const loaded = await client.query(
'SELECT id, body, revision FROM revision_demo_articles ORDER BY id'
);
const expectedRevision = loaded.rows[1].revision;
// Illustrative blind saves: each is a separate transaction.
for (const body of ['First edit', 'Stale replacement']) {
await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
try {
await client.query(
`UPDATE revision_demo_articles
SET body = $1, revision = revision + 1 WHERE id = 1`,
[body]
);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
}
}
const first = await saveArticle(2, expectedRevision, 'First edit');
const stale = await saveArticle(2, expectedRevision, 'Stale replacement');
assert.equal(first.status, 'saved');
assert.equal(first.article.revision, 2);
assert.equal(stale.status, 'stale-or-missing');
const final = await client.query(
'SELECT id, body, revision FROM revision_demo_articles ORDER BY id'
);
assert.deepEqual(final.rows, [
{ id: 1, body: 'Stale replacement', revision: 3 },
{ id: 2, body: 'First edit', revision: 2 }
]);
console.table(final.rows);
console.log(stale.status);
} finally {
await client.end();
}
Mevcut deneme veritabanının adı revision_demo ise aşağıdaki komutu çalıştırın. Gerçek veritabanı adınızı yazın ve yerel örneğinizin gerektirdiği bağlantı ortam ayarlarını koruyun. node-postgres: Connecting.
PGDATABASE=revision_demo node revision-demo.mjs
Beklenen son değerler şunlardır:
| Makale | İçerik | Revizyon | Anlamı |
|---|---|---|---|
| 1 | Stale replacement |
3 | Koşulsuz iki kayıt da işlendi |
| 2 | First edit |
2 | Revizyonu kontrol edilen ikinci kayıt reddedildi |
Yazdırılan son durum stale-or-missing olmalıdır.
Sıralama, zamanlamaya bağlı eşzamanlılıktan özellikle kaçınır. Temel sınırı doğrudan gösterir: her kayıt işlemi serileştirilebilir olsa bile güncelliğini yitirmiş bir değişiklik başarılı olabilir; çünkü kayıtlar geçerli bir sıralı düzende yürütülür.
node-postgres her sonucu nasıl ele almalı?
Program, sürücünün belgelenmiş parametreli sorgu API’sini kullanarak değerleri SQL metninden ayrı iletir. Ayrıca bir güncellemenin işlediği satır sayısını bildiren rowCount değerini de kontrol eder; boş bir rows dizisi tek başına güncelleme sayılarını kontrol etmenin genel bir alternatifi değildir. node-postgres: Queries, node-postgres: pg.Result.
İşlevi uyarlarken şu karar tablosunu kullanın:
| Sonuç | Uygulamanın yapacağı |
|---|---|
| Bir satır güncellendi ve işlem başarıyla tamamlandı | Kaydı kabul edin ve döndürülen revizyonu koruyun |
| Sıfır satır güncellendi | Bu kaydı güncelliğini yitirmiş veya bulunamayan olarak reddedin; gönderilen taslağı koruyun |
SQLSTATE 40001 |
İşlemi geri alın ve sınırlandırılmış bir politika çerçevesinde işlemin tamamını yeniden deneyin |
| Başka bir hata veya yeniden deneme sınırına ulaşılması | Hatayı bildirin; kaydın başarılı olduğunu söylemeyin |
Sıfır satır sonucu, makalenin silindiğini mi yoksa revizyonunun mu değiştiğini göstermez. Gösterim bu sonuçları bilerek birleştirir. Arayüz daha fazla ayrıntı gerektiriyorsa sonrasında güncel makaleyi yükleyin ve sonucu daha sonraki bir gözlem olarak sunun. Başarısız güncelleme anındaki kesin durumun kanıtı olarak tanımlamayın.
Serileştirme hatalarında SQLSTATE 40001 kullanılır. PostgreSQL, karar veren ifade ve değerler dâhil işlemin tamamının yeniden denenmesini gerektirir. Yeniden deneme birden çok girişim gerektirebilir ve işlemin sonunda tamamlanacağını garanti etmez. Örnekte açıklayıcı bir uygulama politikası olarak üç deneme kullanılmıştır. PostgreSQL: Serialization Failure Handling.
Bu denemeler boyunca expectedRevision değerini değiştirmeyin. Bu değer, gönderilen taslağın ön koşulunu temsil eder. Başka bir kayıt revizyonu ilerletmişse yeni bir işlem eski belirteci sıfır güncellenen satırla reddedebilir. Belirteci otomatik olarak en yeni revizyonla değiştirmek, eski yazmalara karşı korumayı ortadan kaldırır.
İşlev yalnızca COMMIT tamamlandıktan sonra başarı döndürür. node-postgres’in gerektirdiği şekilde tüm işlem ifadeleri aynı Client örneğini kullanır. Bunu bir havuza uyarlarken pool.connect() ile bir istemci edinin, işlem boyunca bu istemciyi kullanın ve ardından serbest bırakın. node-postgres: Transactions, node-postgres: pg.Pool.
SERIALIZABLE veya SELECT FOR UPDATE hâlâ gerekli mi?
Bu dar kapsamlı, tek satırlı koşullu güncelleme için Read Committed da revizyon ön koşulunu uygulayabilir: PostgreSQL, tamamlanmış eşzamanlı bir güncelleyiciyi bekledikten sonra güncelleme koşulunu değiştirilen satıra göre yeniden değerlendirir. Bu, Read Committed düzeyini karmaşık çok satırlı kurallar için bir güvenceye dönüştürmez. PostgreSQL: Transaction Isolation.
Çevresindeki işlem, daha geniş kapsamlı ve bu düzeyde güvence gerektiren okuma ve yazmalara dayanıyorsa SERIALIZABLE kullanmaya devam edin. Revizyon koşulu düzenleyicinin ön koşulunu ifade eder; yalıtım ise işlemin veritabanı operasyonları arasındaki etkileşimleri yönetir. Her ikisini de gerçek göreve göre seçin.
SELECT FOR UPDATE, bir satırı incelemesi ve birbirine bağlı birkaç işlem yapması gereken bir işlemi destekleyebilir. Satır kilidi, işlem bitene kadar çakışan yazanları engeller. Repeatable Read veya Serializable düzeyinde, işlemin anlık görüntüsünden bu yana değişmiş bir satırı kilitlemek hataya yol açabilir. PostgreSQL: Explicit Locking.
Kaydetme isteği geldiğinde bu kilidi almak, gönderilen temel revizyonu karşılaştırma sorumluluğunu yine uygulamaya bırakır. Geçerli satırı kilitlemek, veritabanına taslağın hangi eski revizyondan üretildiğini söylemez. Burada gösterilen tek satırlık değişiklik için koşullu güncelleme karşılaştırma ve değişikliği zaten bir araya getirir.
Bu örüntüyü uygulamaya entegre etmeden önce içerik yazan tüm yolları kontrol edin, reddedildiğinde düzenleyicinin taslağını koruyun ve düzenleyicinin temel revizyonunu yalnızca kaydın onaylandığı doğrulandıktan sonra güncelleyin. Eski bir kayıt reddedildiğinde, okuyucunun metinleri karşılaştırıp gerçekten incelediği revizyona dayalı yeni bir kayıt gönderebilmesi için güncel makaleyi korunan taslağın yanında sunun.
Kaynaklar
- 13.2. Transaction Isolation — PostgreSQL Global Development Group.
- UPDATE — PostgreSQL Global Development Group.
- 13.5. Serialization Failure Handling — PostgreSQL Global Development Group.
- 13.3. Explicit Locking — PostgreSQL Global Development Group.
- node-postgres: Introduction and Install — node-postgres.
- ESM — node-postgres.
- Connecting — node-postgres.
- Queries — node-postgres.
- pg.Result — node-postgres.
- Transactions — node-postgres.
- pg.Pool — node-postgres.