ข้อ จำกัด ในการบังคับใช้“ อย่างน้อยหนึ่ง” หรือ“ หนึ่งอย่าง” ในฐานข้อมูล


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ข้อ จำกัด ด้วยซ้ำ
และคุณไม่จำเป็นต้องเก็บข้อมูลใด ๆ ซ้ำซ้อน

รวม ID ของอีเมลที่ใช้งานอยู่ในusersตารางทำให้มีการอ้างอิงซึ่งกันและกัน บางคนอาจคิดว่าเราจำเป็นต้องมีDEFERRABLEข้อ จำกัด ในการแก้ปัญหาไก่และไข่ในการแทรกผู้ใช้และอีเมลที่ใช้งานของเขา แต่ใช้ CTE ที่ปรับเปลี่ยนข้อมูลเราไม่จำเป็นต้องทำเช่นนั้น

สิ่งนี้จะบังคับให้มีอีเมลที่ใช้งานอยู่หนึ่งอีเมลต่อผู้ใช้หนึ่งรายตลอดเวลา:

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()เป็นอธิบายในรายละเอียดในคำตอบที่เชื่อมโยงด้านล่าง

หากคุณไม่ทราบชื่อของลำดับที่แนบมาสำหรับserialคอลัมน์email.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;

ซอ Fiddle

คุณอาจแค็ปซูลคำสั่ง SQL ในฟังก์ชั่นฝั่งเซิร์ฟเวอร์ถ้าคำสั่ง ORM แบบง่าย ๆ นั้นไม่ฉลาดพอที่จะรับมือกับสิ่งนี้

มีความเกี่ยวข้องอย่างใกล้ชิดพร้อมคำอธิบายที่เพียงพอ:

ยังเกี่ยวข้องกับ:

เกี่ยวกับDEFERRABLEข้อ จำกัด :

เกี่ยวกับnextval()และpg_get_serial_sequence():


สิ่งนี้สามารถนำไปใช้กับความสัมพันธ์อย่างน้อย 1 อย่างได้หรือไม่ ไม่ใช่ 1 -1 ดังที่แสดงในคำตอบนี้
CMCDragonkai

@CMCDragonkai: ใช่ มีการบังคับใช้อีเมลที่ใช้งานอยู่หนึ่งรายการต่อผู้ใช้หนึ่งราย ไม่มีอะไรทำให้คุณไม่ต้องเพิ่มอีเมล (ที่ไม่ได้ใช้งาน) สำหรับผู้ใช้เดียวกัน หากคุณไม่ต้องการให้มีบทบาทพิเศษสำหรับอีเมลที่ใช้งานทริกเกอร์จะเป็นทางเลือก (เข้มงวดน้อยกว่า) แต่คุณต้องระมัดระวังในการครอบคลุมการอัพเดทและการลบทั้งหมด ฉันแนะนำให้คุณถามคำถามหากคุณต้องการสิ่งนี้
Erwin Brandstetter

มีวิธีการลบผู้ใช้โดยไม่ใช้ON DELETE CASCADEหรือไม่ แค่อยากรู้อยากเห็น (cascading ทำงานได้ดีสำหรับตอนนี้)
อะมี

@amoe: มีหลายวิธี การปรับเปลี่ยนข้อมูล CTE, ทริกเกอร์, กฎ, คำสั่งหลายรายการในธุรกรรมเดียวกัน, ... ทั้งหมดขึ้นอยู่กับข้อกำหนดที่แน่นอน ถามคำถามใหม่กับข้อมูลเฉพาะของคุณหากคุณต้องการคำตอบ คุณสามารถเชื่อมโยงกับบริบทนี้ได้ตลอดเวลา
Erwin Brandstetter

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 Server ดั้งเดิมของฉันด้วยความช่วยเหลือจากa_horse_with_no_name

ตามที่ypercubeพูดถึงในความคิดเห็นคุณสามารถไปต่อได้:

  • วางคอลัมน์บูลีน และ
  • สร้าง UNIQUE INDEX ON emails (UserID) WHERE (EmailAddress = ActiveAddress)

เอฟเฟ็กต์เหมือนกัน แต่มันง่ายและเป็นเนื้อหา


1ปัญหาคือว่าข้อ จำกัด ที่มีอยู่ให้แน่ใจว่ามีแถวที่เรียกว่า 'ใช้งาน' โดยแถวอื่นอยู่ไม่ว่าจะใช้งานจริง ฉันไม่รู้จัก Postgres ดีพอที่จะใช้ข้อ จำกัด พิเศษด้วยตัวเอง (อย่างน้อยก็ไม่ได้ตอนนี้) แต่ใน SQL Server ก็สามารถทำได้ดังนี้:

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การอ้างอิงดังนั้น(FK) B.b_id(PK) และB.a_id(FK) การอ้างอิงA.a_id(PK) ORMs จำนวนมากและอื่น ๆ ไม่สามารถรับมือกับข้อ จำกัด ที่เลื่อนออกไปได้ ดังนั้นในกรณีนี้คุณต้องการเพิ่ม FK deferrable จากผู้ใช้ไปยังที่อยู่ในคอลัมน์active_address_id, แทนของการใช้ธงactiveaddress


FK DEFERRABLEไม่ได้จะต้องมีการ
Erwin Brandstetter
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.