एक डेटाबेस में "कम से कम एक" या "बिल्कुल एक" लागू करने के लिए बाधा


24

मान लें कि हमारे पास उपयोगकर्ता हैं और प्रत्येक उपयोगकर्ता के कई ईमेल पते हो सकते हैं

CREATE TABLE emails (
    user_id integer,
    email_address text,
    is_active boolean
)

कुछ नमूने पंक्तियों

user_id | email_address | is_active
1       | foo@bar.com   | t
1       | baz@bar.com   | f
1       | bar@foo.com   | f
2       | ccc@ddd.com   | t

मैं एक बाधा को लागू करना चाहता हूं कि हर उपयोगकर्ता का एक सक्रिय पता हो। Postgres में मैं यह कैसे कर सकता हूँ? मैं ऐसा कर सकता था:

CREATE UNIQUE INDEX "user_email" ON emails(user_id) WHERE is_active=true;

जो एक से अधिक सक्रिय पते वाले उपयोगकर्ता के विरुद्ध सुरक्षा करेगा, लेकिन मुझे विश्वास नहीं होगा कि उनके सभी पते झूठे होने के विरुद्ध सुरक्षित रहेंगे।

यदि संभव हो तो मैं ट्रिगर या pl / pgsql स्क्रिप्ट से बचना पसंद करूंगा, क्योंकि वर्तमान में हमारे पास इनमें से कोई भी नहीं है और इसे स्थापित करना मुश्किल होगा। लेकिन मुझे यह जानकर सराहना होगी कि ऐसा करने का एकमात्र तरीका ट्रिगर या pl / pgsql है, अगर ऐसा है।

जवाबों:


17

आपको ट्रिगर या PL / pgSQL की बिल्कुल भी आवश्यकता नहीं है।
तुम भी बाधाओं की जरूरत नहीं है DEFERRABLE
और आपको किसी भी जानकारी को अनावश्यक रूप से संग्रहीत करने की आवश्यकता नहीं है।

usersतालिका में सक्रिय ईमेल की आईडी शामिल करें , जिसके परिणामस्वरूप पारस्परिक संदर्भ मिलते हैं। कोई सोच सकता है कि हमें DEFERRABLEउपयोगकर्ता और उसके सक्रिय ईमेल को सम्मिलित करने के लिए चिकन-एंड-एग समस्या को हल करने के लिए एक बाधा की आवश्यकता है , लेकिन डेटा-संशोधित सीटीई का उपयोग करके हमें इसकी आवश्यकता भी नहीं है।

यह हर समय उपयोगकर्ता के प्रति एक सक्रिय ईमेल को लागू करता है :

CREATE TABLE users (
  user_id  serial PRIMARY KEY
, username text NOT NULL
, email_id int NOT NULL  -- FK to active email, constraint added below
);

CREATE TABLE email (
  email_id serial PRIMARY KEY
, user_id  int NOT NULL REFERENCES users ON DELETE CASCADE ON UPDATE CASCADE 
, email    text NOT NULL
, CONSTRAINT email_fk_uni UNIQUE(user_id, email_id)  -- for FK constraint below
);

ALTER TABLE users ADD CONSTRAINT active_email_fkey
FOREIGN KEY (user_id, email_id) REFERENCES email(user_id, email_id);

इसे "सबसे अधिक सक्रिय ईमेल पर" बनाने के लिए NOT NULLबाधा को दूर users.email_idकरें। (आप अभी भी प्रति उपयोगकर्ता कई ईमेल संग्रहीत कर सकते हैं, लेकिन उनमें से कोई भी "सक्रिय" नहीं है।)

आप अधिक लेवे की अनुमति दे सकते हैं active_email_fkey DEFERRABLE(एक ही लेनदेन के अलग-अलग कमांड में उपयोगकर्ता और ईमेल डालें ), लेकिन यह आवश्यक नहीं है

इंडेक्स कवरेज को ऑप्टिमाइज़ करने के लिए मैंने user_idपहले UNIQUEबाधा डाली email_fk_uni। विवरण:

वैकल्पिक दृश्य:

CREATE VIEW user_with_active_email AS
SELECT * FROM users JOIN email USING (user_id, email_id);

यहां बताया गया है कि आप नए उपयोगकर्ताओं को एक सक्रिय ईमेल के साथ कैसे डालें (आवश्यकतानुसार):

WITH new_data(username, email) AS (
   VALUES
      ('usr1', 'abc@d.com')   -- new users with *1* active email
    , ('usr2', 'def3@d.com')
    , ('usr3', 'ghi1@d.com')
   )
, u AS (
   INSERT INTO users(username, email_id)
   SELECT n.username, nextval('email_email_id_seq'::regclass)
   FROM   new_data n
   RETURNING *
   )
INSERT INTO email(email_id, user_id, email)
SELECT u.email_id, u.user_id, n.email
FROM   u
JOIN   new_data n USING (username);

विशिष्ट कठिनाई यह है कि हमारे पास शुरू करने के लिए न तो है user_idऔर न ही email_idहै। दोनों संबंधित से प्रदान किए गए सीरियल नंबर हैं SEQUENCE। यह एक एकल RETURNINGखंड (एक और चिकन और अंडे की समस्या) के साथ हल नहीं किया जा सकता है । समाधान है nextval()के रूप में नीचे लिंक किए गए जवाब में विस्तार से समझाया

यदि आप उस कॉलम के लिए संलग्न अनुक्रम का नाम नहीं जानते हैं जिसे आप प्रतिस्थापित कर सकते हैं:serialemail.email_id

nextval('email_email_id_seq'::regclass)

साथ में

nextval(pg_get_serial_sequence('email', 'email_id'))

यहां बताया गया है कि आप एक नया "सक्रिय" ईमेल कैसे जोड़ते हैं:

WITH e AS (
   INSERT INTO email (user_id, email)
   VALUES  (3, 'new_active@d.com')
   RETURNING *
   )
UPDATE users u
SET    email_id = e.email_id
FROM   e
WHERE  u.user_id = e.user_id;

एसक्यूएल फिडल।

यदि आप सर्वर-साइड फ़ंक्शंस में SQL कमांड को इनकैप्सुलेट कर सकते हैं, तो कुछ साधारण दिमाग वाले ORM इससे निपटने के लिए पर्याप्त स्मार्ट नहीं हैं।

बारीकी से संबंधित, पर्याप्त विवरण के साथ:

यह भी संबंधित:

DEFERRABLEबाधाओं के बारे में :

के बारे में nextval()और pg_get_serial_sequence():


क्या इसे 1 से कम से कम-एक संबंधों पर लागू किया जा सकता है? 1 -1 नहीं जैसा कि इस उत्तर में दिखाया गया है।
CMCDragonkai 3

@CMCDragonkai: हाँ। वास्तव में प्रति उपयोगकर्ता एक सक्रिय ईमेल लागू किया गया है। कुछ भी नहीं आपको एक ही उपयोगकर्ता के लिए अधिक (गैर-सक्रिय) ईमेल जोड़ने से रोकता है। यदि आप सक्रिय ईमेल के लिए विशेष भूमिका नहीं चाहते हैं, तो ट्रिगर एक (कम सख्त) विकल्प होगा। लेकिन आपको सभी अपडेट और डिलीट को कवर करने के लिए सावधान रहना होगा। मेरा सुझाव है कि यदि आपको इसकी आवश्यकता है तो आप एक प्रश्न पूछें ।
इरविन ब्रान्डेसटेटर

क्या बिना उपयोग किए उपयोगकर्ताओं को हटाने का कोई तरीका है ON DELETE CASCADE? बस जिज्ञासु (कैस्केडिंग अभी के लिए ठीक काम कर रहा है)।
अमोघ जूल

@amoe: विभिन्न तरीके हैं। डेटा-संशोधन CTE, ट्रिगर, नियम, एक ही लेन-देन में कई कथन, ... यह सब सटीक आवश्यकताओं पर निर्भर करता है। यदि आपको उत्तर की आवश्यकता है, तो अपनी बारीकियों के साथ एक नया प्रश्न पूछें। आप संदर्भ के लिए इसे हमेशा लिंक कर सकते हैं।
एरविन ब्रान्डेसटेटर

5

यदि आप तालिका में एक स्तंभ जोड़ सकते हैं, तो निम्नलिखित योजना लगभग 1 काम करेगी :

CREATE TABLE emails 
(
    UserID integer NOT NULL,
    EmailAddress varchar(254) NOT NULL,
    IsActive boolean NOT NULL,

    -- New column
    ActiveAddress varchar(254) NOT NULL,

    -- Obvious PK
    CONSTRAINT PK_emails_UserID_EmailAddress
        PRIMARY KEY (UserID, EmailAddress),

    -- Validate that the active address row exists
    CONSTRAINT FK_emails_ActiveAddressExists
        FOREIGN KEY (UserID, ActiveAddress)
        REFERENCES emails (UserID, EmailAddress),

    -- Validate the IsActive value makes sense    
    CONSTRAINT CK_emails_Validate_IsActive
    CHECK 
    (
        (IsActive = true AND EmailAddress = ActiveAddress)
        OR
        (IsActive = false AND EmailAddress <> ActiveAddress)
    )
);

-- Enforce maximum of one active address per user
CREATE UNIQUE INDEX UQ_emails_One_IsActive_True_PerUser
ON emails (UserID, IsActive)
WHERE IsActive = true;

Test SQLFiddle

मेरे स्थानीय SQL सर्वर से अनुवादित , a_horse_with_no_name की मदद से

जैसा कि ypercube ने एक टिप्पणी में उल्लेख किया है, आप आगे भी जा सकते हैं:

  • बूलियन कॉलम ड्रॉप; तथा
  • बनाएँ UNIQUE INDEX ON emails (UserID) WHERE (EmailAddress = ActiveAddress)

प्रभाव समान है, लेकिन यह यकीनन सरल और निरर्थक है।


1 समस्या यह है कि मौजूदा बाधाएं केवल यह सुनिश्चित करती हैं कि एक पंक्ति को दूसरी पंक्ति द्वारा 'सक्रिय' के रूप में संदर्भित किया गया है , न कि यह वास्तव में सक्रिय है। मुझे नहीं पता कि पोस्टग्रैस को अच्छी तरह से लागू करने के लिए पर्याप्त बाधा खुद (कम से कम अभी नहीं), लेकिन SQL सर्वर में, यह इस प्रकार किया जा सकता है:

CREATE TABLE Emails 
(
    EmailID integer NOT NULL UNIQUE,
    UserID integer NOT NULL,
    EmailAddress varchar(254) NOT NULL,
    IsActive bit NOT NULL,

    -- New columns
    ActiveEmailID integer NOT NULL,
    ActiveIsActive AS CONVERT(bit, 'true') PERSISTED,

    -- Obvious PK
    CONSTRAINT PK_emails_UserID_EmailAddress
        PRIMARY KEY (UserID, EmailID),

    CONSTRAINT UQ_emails_UserID_EmailAddress_IsActive
        UNIQUE (UserID, EmailID, IsActive),

    -- Validate that the active address exists and is active
    CONSTRAINT FK_emails_ActiveAddressExists_And_IsActive
        FOREIGN KEY (UserID, ActiveEmailID, ActiveIsActive)
        REFERENCES emails (UserID, EmailID, IsActive),

    -- Validate the IsActive value makes sense    
    CONSTRAINT CK_emails_Validate_IsActive
    CHECK 
    (
        (IsActive = 'true' AND EmailID = ActiveEmailID)
        OR
        (IsActive = 'false' AND EmailID <> ActiveEmailID)
    )
);

-- Enforce maximum of one active address per user
CREATE UNIQUE INDEX UQ_emails_One_IsActive_PerUser
ON emails (UserID, IsActive)
WHERE IsActive = 'true';

यह प्रयास पूर्ण ईमेल पते को डुप्लिकेट करने के बजाय एक सरोगेट का उपयोग करके मूल पर थोड़ा सुधार करता है।


4

स्कीमा परिवर्तन के बिना इनमें से किसी एक को करने का एकमात्र तरीका PL / PgSQL ट्रिगर के साथ है।

"बिल्कुल एक" मामले के लिए, आप एक होने के साथ संदर्भों को पारस्परिक बना सकते हैं DEFERRABLE INITIALLY DEFERRED। तो A.b_id(एफके) संदर्भ B.b_id(पीके) और B.a_id(एफके) संदर्भ A.a_id(पीके)। हालांकि कई ओआरएम आदि असाध्य बाधाओं का सामना नहीं कर सकते हैं। इसलिए इस मामले में आप एक deferrable FK उपयोगकर्ता से पता करने के लिए एक स्तंभ पर जोड़ना होगा active_address_id, बजाय एक का उपयोग कर के activeपर झंडा address


FK होना भी नहीं है DEFERRABLE
इरविन ब्रान्डसेट्टर
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.