데이터베이스에서 "적어도 하나"또는 "정확하게 하나"를 적용하는 제약


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;

둘 이상의 활성 주소를 가진 사용자를 보호하지만 모든 주소가 false로 설정되는 것을 방지하지는 않습니다.

가능하다면 트리거 또는 pl / pgsql 스크립트를 피하는 것이 좋습니다. 현재 스크립트가 없으므로 설정하기가 어렵습니다. 그러나 "만약 이것을 할 수있는 유일한 방법은 트리거나 pl / pgsql을 사용하는 것"이라는 것을 알고 싶습니다.

답변:


17

트리거 또는 PL / pgSQL이 전혀 필요하지 않습니다.
당신은 제약 이 필요 하지 않습니다 DEFERRABLE.
또한 정보를 중복으로 저장할 필요가 없습니다.

users표에 활성 이메일의 ID를 포함시켜 상호 참조하십시오. 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()으로 아래 링크 대답 상세히 설명 .

열에 첨부 된 시퀀스의 이름을 모르는 경우 바꿀 수 있습니다.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이 이것에 대처할만큼 똑똑하지 않은 경우 서버 측 함수에서 SQL 명령을 캡슐화 할 수 있습니다.

충분한 설명과 밀접한 관련이 있습니다.

또한 관련 :

DEFERRABLE구속 조건 정보 :

소개 nextval()pg_get_serial_sequence():


이 관계를 1 대 1 관계에 적용 할 수 있습니까? 이 답변에 표시된 1 -1이 아닙니다.
CMCDragonkai

@CMCDragonkai : 그렇습니다. 사용자 당 정확히 하나의 활성 이메일이 시행됩니다. 같은 사용자에 대해 더 많은 (비활성) 전자 메일을 추가하지 않아도됩니다. 활성 이메일에 대한 특별한 역할을 원하지 않으면 트리거가 (엄격한) 대안이 될 것입니다. 그러나 모든 업데이트 및 삭제에주의를 기울여야합니다. 난 당신이 물어 건의 질문 이 필요합니다.
Erwin Brandstetter

사용하지 않고 사용자를 삭제하는 방법이 ON DELETE CASCADE있습니까? 그냥 궁금 해서요 (캐스 케이 딩은 현재 잘 작동하고 있습니다).
amoe

@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

a_horse_with_no_name의 도움으로 네이티브 SQL Server에서 번역

으로 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). 많은 ORM 등은 연기 가능한 제약 조건에 대처할 수 없습니다. 그래서이 경우 당신은 열을 주소로 사용자로부터 연기 FK를 추가 할 것 active_address_id, 대신 사용하는 active에 플래그를 address.


FK도 그렇습니다 DEFERRABLE.
Erwin Brandstetter
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.