Ràng buộc để thực thi tối thiểu một hoặc một chính xác một cơ sở dữ liệu


24

Giả sử chúng tôi có người dùng và mỗi người dùng có thể có nhiều địa chỉ email

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

Một số hàng mẫu

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

Tôi muốn thực thi một ràng buộc rằng mọi người dùng đều có chính xác một địa chỉ hoạt động. Làm thế nào tôi có thể làm điều này trong Postgres? Tôi có thể làm điều này:

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

Điều này sẽ bảo vệ chống lại người dùng có nhiều hơn một địa chỉ hoạt động, nhưng tôi tin rằng sẽ không bảo vệ tất cả các địa chỉ của họ được đặt thành sai.

Nếu có thể, tôi muốn tránh một trình kích hoạt hoặc tập lệnh pl / pssql, vì hiện tại chúng tôi không có bất kỳ thứ nào trong số đó & rất khó để thiết lập. Nhưng tôi sẽ đánh giá cao việc biết "cách duy nhất để làm điều này là với một trình kích hoạt hoặc pl / pssql", nếu đó là trường hợp.

Câu trả lời:


17

Bạn hoàn toàn không cần kích hoạt hoặc PL / pgSQL.
Bạn thậm chí không cần các DEFERRABLE ràng buộc.
Và bạn không cần lưu trữ bất kỳ thông tin dư thừa.

Bao gồm ID của email hoạt động trong usersbảng, dẫn đến các tham chiếu lẫn nhau. Mọi người có thể nghĩ rằng chúng ta cần một DEFERRABLEràng buộc để giải quyết vấn đề trứng gà khi chèn người dùng và email đang hoạt động của mình, nhưng sử dụng CTE sửa đổi dữ liệu, chúng ta thậm chí không cần điều đó.

Điều này thực thi chính xác một email hoạt động cho mỗi người dùng mọi lúc:

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

Xóa các NOT NULLràng buộc users.email_idđể làm cho nó "nhiều nhất là một email hoạt động". (Bạn vẫn có thể lưu trữ nhiều email cho mỗi người dùng, nhưng không ai trong số họ là "hoạt động".)

Bạn có thể thực hiện active_email_fkey DEFERRABLEđể cho phép nhiều thời gian hơn (chèn người dùng và email vào các lệnh riêng biệt của cùng một giao dịch), nhưng điều đó không cần thiết .

Tôi đặt user_idđầu tiên trong các UNIQUEràng buộc email_fk_uniđể tối ưu hóa phạm vi chỉ số. Chi tiết:

Chế độ xem tùy chọn:

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

Đây là cách bạn chèn người dùng mới với một email hoạt động (theo yêu cầu):

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

Khó khăn cụ thể là chúng tôi không có user_idcũng không email_idbắt đầu. Cả hai đều là số serial được cung cấp từ tương ứng SEQUENCE. Nó không thể được giải quyết bằng một RETURNINGmệnh đề duy nhất (một vấn đề gà và trứng khác). Giải pháp là nextval()như giải thích chi tiết trong câu trả lời liên kết dưới đây .

Nếu bạn không biết tên của chuỗi được đính kèm cho serialcột, email.email_idbạn có thể thay thế:

nextval('email_email_id_seq'::regclass)

với

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

Đây là cách bạn thêm một email "hoạt động" mới:

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;

Câu đố SQL.

Bạn có thể gói gọn các lệnh SQL trong các chức năng phía máy chủ nếu một số ORM có đầu óc đơn giản không đủ thông minh để đối phó với điều này.

Liên quan chặt chẽ, với lời giải thích phong phú:

Cũng liên quan:

Về DEFERRABLEnhững hạn chế:

Giới thiệu nextval()pg_get_serial_sequence():


Điều này có thể được áp dụng cho 1 đến ít nhất một mối quan hệ không? Không phải 1 -1 như trong câu trả lời này.
CMCDragonkai

@CMCDragonkai: Vâng. Chính xác một email hoạt động cho mỗi người dùng được thi hành. Không có gì ngăn bạn thêm nhiều email (không hoạt động) cho cùng một người dùng. Nếu bạn không muốn vai trò đặc biệt cho email hoạt động, kích hoạt sẽ là một thay thế (ít nghiêm ngặt hơn). Nhưng bạn phải cẩn thận để bao gồm tất cả các bản cập nhật và xóa. Tôi đề nghị bạn hỏi một câu hỏi nếu bạn cần điều này.
Erwin Brandstetter

Có cách nào để xóa người dùng mà không sử dụng ON DELETE CASCADE? Chỉ tò mò (hiện tại tầng đang hoạt động tốt).
amoe

@amoe: Có nhiều cách khác nhau. CTE sửa đổi dữ liệu, kích hoạt, quy tắc, nhiều câu lệnh trong cùng một giao dịch, ... tất cả phụ thuộc vào yêu cầu chính xác. Đặt một câu hỏi mới với chi tiết cụ thể của bạn nếu bạn cần một câu trả lời. Bạn luôn có thể liên kết đến cái này cho bối cảnh.
Erwin Brandstetter

5

Nếu bạn có thể thêm một cột vào bảng, sơ đồ sau sẽ có gần 1 hoạt động:

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

Được dịch từ Máy chủ SQL gốc của tôi, với sự trợ giúp từ a_horse_with_no_name

Như ypercube đã đề cập trong một bình luận, bạn thậm chí có thể đi xa hơn:

  • Thả cột boolean; và
  • Tạo UNIQUE INDEX ON emails (UserID) WHERE (EmailAddress = ActiveAddress)

Hiệu quả là như nhau, nhưng nó được cho là đơn giản và gọn gàng hơn.


1 Vấn đề là các ràng buộc hiện tại chỉ đảm bảo rằng một hàng được gọi là 'hoạt động' bởi một hàng khác tồn tại , không phải là nó cũng thực sự hoạt động. Tôi không biết Postgres đủ tốt để tự thực hiện các ràng buộc bổ sung (ít nhất là không phải bây giờ), nhưng trong SQL Server, nó có thể được thực hiện như vậy:

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';

Nỗ lực này cải thiện một chút so với bản gốc bằng cách sử dụng thay thế thay vì sao chép địa chỉ email đầy đủ.


4

Cách duy nhất để thực hiện một trong hai cách này mà không thay đổi lược đồ là với trình kích hoạt PL / PGQuery.

Đối với trường hợp "chính xác là một", bạn có thể tạo các tham chiếu lẫn nhau, với một thực thể DEFERRABLE INITIALLY DEFERRED. Vì vậy, A.b_idtham chiếu (FK) B.b_id(PK) và B.a_id(FK) A.a_id(PK). Nhiều ORM vv không thể đối phó với các ràng buộc có thể bảo vệ mặc dù. Vì vậy, trong trường hợp này, bạn sẽ thêm FK có thể bảo vệ từ người dùng vào địa chỉ trên một cột active_address_id, thay vì sử dụng activecờ trên address.


FK thậm chí không phải như vậy DEFERRABLE.
Erwin Brandstetter
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.