पुराने दस्तावेज़ को सहेजने से रोकने के लिए, अपडेट की शर्त में संपादक द्वारा मूल रूप से लोड किया गया संशोधन शामिल करें, फिर उसे परमाणु रूप से बढ़ाएँ। SERIALIZABLE अकेले इस आवश्यकता को व्यक्त नहीं कर सकता, जब दस्तावेज़ पहले के किसी लेनदेन में लोड किया गया हो।

यह ट्यूटोरियल Mac पर Terminal में काम कर रहे उस डेवलपर के लिए है जिसे PostgreSQL लेख संपादक में पुराना प्रतिस्थापन पाठ अस्वीकार करवाना है। इसमें कृत्रिम लेख पंक्तियों और node-postgres का पूरा प्रदर्शन शामिल है। आवश्यकता स्पष्ट है: तभी सहेजें जब संग्रहीत संशोधन उस संशोधन से मेल खाता हो जिस पर भेजा गया मसौदा आधारित था।

SERIALIZABLE पुराने प्रतिस्थापन को अनुमति क्यों दे सकता है?

PostgreSQL के अनुसार, serializable निष्पादन का प्रभाव ऐसा होता है मानो प्रतिबद्ध लेनदेन किसी क्रम में एक-एक करके चलाए गए हों। यह गारंटी डेटाबेस लेनदेन और उनके संचालन से संबंधित है। PostgreSQL: Transaction Isolation।

इस उदाहरणात्मक क्रम पर विचार करें:

  1. एक संपादक संशोधन 1 पर लेख 1 लोड करता है और पढ़ने का लेनदेन समाप्त हो जाता है।
  2. कोई दूसरा सहेजना लेख के पाठ को बदलता है और संशोधन 2 प्रतिबद्ध करता है।
  3. मूल संपादक संशोधन 1 पर आधारित प्रतिस्थापन पाठ भेजता है।
  4. एक नया serializable लेनदेन केवल उसकी ID के आधार पर लेख 1 को अपडेट करता है।

अंतिम लेनदेन सफल हो सकता है: लेनदेन पहले ही क्रमिक रूप से चल चुके हैं। केवल id = 1 पर निर्भर अपडेट में ऐसा कुछ नहीं है जो कहे कि भेजे गए पाठ के लिए संशोधन 1 आवश्यक है। यह प्रलेखित serializability परिभाषा का अनुप्रयोग है, उससे कोई अपवाद नहीं।

एक-दूसरे पर अतिव्यापी लेनदेन अलग स्थिति हैं। PostgreSQL किसी serializable लेनदेन को तब अस्वीकार कर सकता है जब कोई समवर्ती अपडेट उसके स्नैपशॉट से टकराता हो या निर्भरताएँ serial निष्पादन को रोकती हों। स्नैपशॉट पहले ऐसे कथन पर स्थापित होता है जो लेनदेन-नियंत्रण कथन न हो; पहले केवल BEGIN जारी करने से संपादक का पुराना पठन सुरक्षित नहीं रहता। PostgreSQL: Transaction Isolation।

इसलिए निदान का प्रश्न है: सहेजना स्पष्ट रूप से कहाँ यह आवश्यक करता है कि संशोधन वही हो जो संपादक ने लोड किया था? यदि इसका उत्तर कहीं नहीं है, तो isolation स्तर बदलने से वह छूटी हुई शर्त नहीं जुड़ती।

संशोधन जाँच को किस बात की गारंटी देनी चाहिए?

हर लोड किए गए मसौदे को एक जोड़ी मानें: उसका संपादन योग्य पाठ और उसका आधार संशोधन। सहेजने की कार्रवाई में लेख ID, प्रतिस्थापन पाठ और अपरिवर्तित आधार संशोधन भेजा जाता है। डेटाबेस अपडेट को ID और उस संशोधन—दोनों से मेल खाना चाहिए, पाठ बदलना चाहिए और उसी कथन में संग्रहीत संशोधन बढ़ाना चाहिए।

उदाहरण के लिए, शर्त WHERE id = $1 AND revision = $2 पूर्वापेक्षा व्यक्त करती है, जबकि SET body = $3, revision = revision + 1 परिवर्तन करता है। PostgreSQL शर्त से मेल खाने वाली पंक्तियाँ अपडेट करता है, और RETURNING वास्तव में अपडेट की गई पंक्तियों से मान देता है। शून्य पंक्तियाँ अपडेट होना एक सफल SQL कमांड है, इसलिए एप्लिकेशन को इसकी व्याख्या स्पष्ट रूप से करनी होगी। PostgreSQL: UPDATE।

इस तरीके को अक्सर आशावादी समवर्ती नियंत्रण या compare-and-swap कहा जाता है। इस लेख की रूपरेखा में, संपादन योग्य लेख सामग्री बदलने वाले हर ऑपरेशन को संशोधन आगे बढ़ाना होगा। संशोधनों को non-null रखें, उन्हें रीसेट न करें और किसी पुरानी संशोधन कुंजी के साथ लेख की पहचान दोबारा उपयोग न करें। वरना समानता से गलत संकेत मिल सकता है कि पुराना मसौदा अब भी मेल खाता है।

अलग से पढ़कर फिर केवल ID के आधार पर अपडेट करने से यह आवश्यकता लिखने की कार्रवाई में शामिल नहीं होती। इसी तरह, सहेजने से ठीक पहले नवीनतम संशोधन प्राप्त करके उसे भेजे गए संशोधन की जगह रखना अनुरोध का अर्थ बदल देता है: इससे उस संस्करण को बदलने की अनुमति मिलती है जिसे संपादक ने आधार के रूप में इस्तेमाल नहीं किया था।

अपने Mac पर पूरा कृत्रिम उदाहरण चलाएँ

एक चालू PostgreSQL इंस्टेंस, पहले से बना एक अस्थायी डेटाबेस और npm के साथ वर्तमान या समर्थित LTS Node.js इंस्टॉलेशन उपलब्ध रखें। Terminal खोलें, काम करने के लिए फ़ोल्डर बनाएँ और प्रलेखित npm install pg कमांड से ड्राइवर इंस्टॉल करें। node-postgres: Introduction and Install।

mkdir revision-write-demo
cd revision-write-demo
npm install pg

नीचे दिए गए पूरे प्रोग्राम को उस फ़ोल्डर में revision-demo.mjs नाम से सहेजें। इसमें प्रलेखित ESM-संगत default import का उपयोग होता है। Client और Pool, दोनों पर्यावरण वेरिएबल से PostgreSQL कनेक्शन सेटिंग पढ़ सकते हैं; चलाने से पहले अपने मौजूदा अस्थायी डेटाबेस के लिए वे सेटिंग कॉन्फ़िगर करें। node-postgres: ESM, node-postgres: Connecting।

यह प्रोग्राम दो कृत्रिम लेखों की तुलना करता है। लेख 1 पर दो unconditional serializable saves होते हैं। लेख 2 पर उसी मूल संशोधन का उपयोग करते हुए दो संशोधन-जाँचे गए saves होते हैं। इसकी अस्थायी तालिका प्रदर्शन को स्थायी लेख तालिकाओं से अलग रखती है। JavaScript सिंटैक्स को Node.js से जाँचा गया; शोध परिवेश में डेटाबेस प्रदर्शन नहीं चलाया गया।

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();
}

यदि मौजूदा अस्थायी डेटाबेस का नाम revision_demo है, तो नीचे दिया गया कमांड चलाएँ। अपने वास्तविक डेटाबेस का नाम रखें और स्थानीय इंस्टेंस के लिए आवश्यक कोई भी कनेक्शन पर्यावरण सेटिंग बनाए रखें। node-postgres: Connecting।

PGDATABASE=revision_demo node revision-demo.mjs

अपेक्षित अंतिम मान ये हैं:

लेख मुख्य पाठ संशोधन अर्थ
1 Stale replacement 3 दोनों unconditional saves प्रतिबद्ध हुए
2 First edit 2 दूसरा संशोधन-जाँचा गया save अस्वीकार हुआ

प्रिंट की गई अंतिम स्थिति stale-or-missing होनी चाहिए।

यह क्रम जानबूझकर समय पर निर्भर समवर्ती निष्पादन से बचता है। यह मुख्य सीमा सीधे दिखाता है: पुराना प्रतिस्थापन सफल हो सकता है, भले ही हर save लेनदेन serializable हो, क्योंकि saves वैध serial क्रम में चलते हैं।

node-postgres को हर परिणाम कैसे संभालना चाहिए?

प्रोग्राम ड्राइवर के प्रलेखित parameterized-query API का उपयोग करके मानों को SQL पाठ से अलग भेजता है। यह rowCount भी जाँचता है, जो बताता है कि अपडेट ने कितनी पंक्तियाँ संसाधित कीं; केवल खाली rows array, अपडेट की संख्या जाँचने का सामान्य विकल्प नहीं है। node-postgres: Queries, node-postgres: pg.Result।

फ़ंक्शन को अनुकूलित करते समय यह निर्णय तालिका उपयोग करें:

परिणाम एप्लिकेशन की कार्रवाई
एक पंक्ति अपडेट हुई और प्रतिबद्धता सफल रही सहेजना स्वीकार करें और लौटाया गया संशोधन रखें
शून्य पंक्तियाँ अपडेट हुईं इस सहेजने को पुराना या अनुपस्थित मानकर अस्वीकार करें; भेजा गया मसौदा सुरक्षित रखें
SQLSTATE 40001 पूरी ट्रांज़ैक्शन को सीमित नीति के अनुसार वापस लें और फिर से प्रयास करें
अन्य त्रुटि या प्रयास सीमा पूरी विफलता दिखाएँ; सफलतापूर्वक सहेजे जाने की सूचना न दें

शून्य पंक्तियों का परिणाम यह स्थापित नहीं करता कि लेख हटाया गया था या उसका संशोधन बदल गया था। प्रदर्शन जानबूझकर इन दोनों परिणामों को एक साथ रखता है। यदि इंटरफ़ेस को अधिक विवरण चाहिए, तो बाद में मौजूदा लेख लोड करके वह परिणाम दिखाएँ। उसे विफल अपडेट के ठीक समय की स्थिति का प्रमाण न बताएँ।

Serialization विफलताओं के लिए SQLSTATE 40001 आता है। PostgreSQL के अनुसार पूरी ट्रांज़ैक्शन फिर से चलानी चाहिए, जिसमें कथनों और मानों का निर्णय करने वाला लॉजिक भी शामिल है। कई प्रयास करने पड़ सकते हैं और सफलतापूर्वक पूरा होने की गारंटी नहीं है। उदाहरण में तीन प्रयास एक उदाहरणात्मक एप्लिकेशन नीति है। PostgreSQL: Serialization Failure Handling।

इन प्रयासों के दौरान expectedRevision को अपरिवर्तित रखें। यह भेजे गए मसौदे की पूर्वापेक्षा दर्शाता है। यदि कोई दूसरा save संशोधन आगे बढ़ा चुका है, तो नया लेनदेन शून्य पंक्तियाँ अपडेट करके पुरानी कुंजी अस्वीकार कर सकता है। कुंजी को नवीनतम संशोधन से अपने आप बदलने पर पुराने लेखन से सुरक्षा हट जाएगी।

फ़ंक्शन सफलता तभी लौटाता है जब COMMIT पूरा हो जाए। node-postgres के अनुसार, सभी ट्रांज़ैक्शन कथनों में एक ही Client का उपयोग होना चाहिए। इसे pool के साथ अनुकूलित करते समय, pool.connect() से एक client प्राप्त करें, पूरे लेनदेन में उसी client का उपयोग करें और फिर उसे रिलीज़ करें। node-postgres: Transactions, node-postgres: pg.Pool।

क्या आपको अब भी SERIALIZABLE या SELECT FOR UPDATE की ज़रूरत है?

इस सीमित, एक-पंक्ति वाले सशर्त अपडेट के लिए Read Committed भी संशोधन की पूर्वापेक्षा लागू कर सकता है: कोई समवर्ती updater प्रतिबद्ध होने के बाद उसकी प्रतीक्षा समाप्त होती है, तो PostgreSQL अपडेट की शर्त को संशोधित पंक्ति पर फिर से जाँचता है। इससे Read Committed को जटिल, कई-पंक्तियों वाले नियमों की गारंटी तक विस्तारित नहीं किया जा सकता। PostgreSQL: Transaction Isolation।

जब आसपास का लेनदेन व्यापक पठन और लेखन पर निर्भर हो और उन पर इसकी गारंटी लागू होती हो, तो SERIALIZABLE बनाए रखें। संशोधन की शर्त संपादक की पूर्वापेक्षा व्यक्त करती है; isolation, लेनदेन के डेटाबेस संचालनों के बीच के संबंधों को नियंत्रित करता है। दोनों का चयन वास्तविक आवश्यकता के आधार पर करें।

SELECT FOR UPDATE उस लेनदेन में उपयोगी हो सकता है जिसे किसी पंक्ति का निरीक्षण करके कई निर्भर ऑपरेशन करने हों। उसका पंक्ति लॉक लेनदेन समाप्त होने तक टकराने वाले लेखकों को रोकता है। Repeatable Read या Serializable में, यदि लेनदेन के स्नैपशॉट के बाद कोई पंक्ति बदली गई हो, तो उसे लॉक करने पर त्रुटि आ सकती है। PostgreSQL: Explicit Locking।

सहेजने का अनुरोध आने पर ही वह लॉक हासिल करने से एप्लिकेशन की यह ज़िम्मेदारी समाप्त नहीं होती कि वह भेजे गए आधार संशोधन की तुलना करे। मौजूदा पंक्ति को लॉक करने से डेटाबेस को यह पता नहीं चलता कि मसौदा किस पुराने संशोधन से बना था। यहाँ दिखाए गए एक-पंक्ति प्रतिस्थापन के लिए, सशर्त अपडेट तुलना और बदलाव को पहले ही एक साथ करता है।

इस तरीके को लागू करने से पहले, सामग्री लिखने वाले सभी रास्ते जाँचें, अस्वीकृति पर संपादक का मसौदा सुरक्षित रखें और सफलतापूर्वक सहेजे जाने की पुष्टि के बाद ही संपादक का आधार संशोधन अपडेट करें। जब पुराना save अस्वीकार हो, तो पाठक को मौजूदा लेख के साथ सुरक्षित रखा गया मसौदा भी दें, ताकि वह पाठों का मिलान कर सके और वास्तव में देखे गए संशोधन पर आधारित नया save भेज सके।

स्रोत