Veritabanında "en az bir" veya "tam olarak bir" zorlama kısıtlaması


24

Diyelim ki kullanıcılarımız var ve her kullanıcı birden fazla e-posta adresine sahip olabilir

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

Bazı örnek satırlar

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

Her kullanıcının tam olarak bir aktif adrese sahip olduğu konusunda bir kısıtlama uygulamak istiyorum. Postgres'te bunu nasıl yapabilirim? Bunu yapabilirim:

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

Bu, birden fazla aktif adrese sahip bir kullanıcıya karşı koruma sağlar, ancak tüm adreslerinin yanlış olarak ayarlanmasına karşı koruma sağlayacağına inanmıyorum.

Mümkünse, şu anda bunlardan hiçbirine sahip olmadığımız için bir tetikleyiciden veya bir pl / pgsql komut dosyasından kaçınmayı tercih ederim. Ancak, "Bunu yapmanın tek yolu bir tetikleyici veya pl / pgsql ile" olduğunu bilmek hoşuma giderdi, eğer öyleyse.

Yanıtlar:


17

Hiç tetikleyiciye veya PL / pgSQL'e ihtiyacınız yok. Sınırlamalara
bile ihtiyacınız yok DEFERRABLE.
Ve herhangi bir bilgiyi gereksiz yere saklamanıza gerek yoktur.

usersKarşılıklı başvurulara neden olan etkin e-postanın kimliğini tabloya ekleyin . Bir DEFERRABLEkullanıcı ve onun aktif e-postasını eklemenin tavuk ve yumurta problemini çözmek için bir kısıtlamaya ihtiyacımız olduğunu düşünebiliriz , ancak verileri değiştiren CTE'leri kullanarak buna ihtiyacımız bile yok.

Bu , her zaman kullanıcı başına tam olarak bir aktif e-postayı zorlar :

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

"En fazla bir aktif e-postaya" yapmak için NOT NULLkısıtlamayı kaldırın users.email_id. (Kullanıcı başına hala birden fazla e-posta saklayabilirsiniz, ancak hiçbiri "etkin" değildir.)

Sen edebilirsiniz yapmak active_email_fkey DEFERRABLE(ayrı komutlar kullanıcı ve e-posta eki daha fazla gecikmeyi sağlamak için aynı işlem), ama bu gerekli değildir .

Endeks kapsamını optimize etmek için user_idilk önce UNIQUEkısıtlamaya girdim email_fk_uni. Detaylar:

İsteğe bağlı görünüm:

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

Yeni kullanıcıları aktif bir e-posta ile nasıl ekleyeceğiniz (gerektiği gibi):

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

Özel zorluk, bizim ne başlamamız ne user_idde bizim olmamızdır email_id. Her ikisi de ilgili tarafından sağlanan seri numaralarıdır SEQUENCE. Tek bir RETURNINGmaddeyle çözülemez (başka bir tavuk-yumurta problemi). Çözüm, aşağıdaki bağlantılı cevapta ayrıntılınextval() olarak açıklanmaktadır .

Eğer yoksa biliyoruz için ekteki dizinin adını serialsütununda email.email_idsen değiştirebilirsiniz:

nextval('email_email_id_seq'::regclass)

ile

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

Yeni bir "etkin" e-postayı şu şekilde eklersiniz:

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 Fiddle.

Bazı basit fikirli ORM bununla başa çıkmak için yeterince akıllı değilse, SQL komutlarını sunucu tarafı işlevlerinde saklayabilirsiniz.

Yakından ilgili, yeterince açıklama ile:

Ayrıca ilgili:

Hakkında DEFERRABLEkısıtlamalar:

Hakkında nextval()ve pg_get_serial_sequence():


Bu, en az bir taneye kadar ilişkilere uygulanabilir mi? Bu cevapta gösterildiği gibi 1 -1 değil.
CMCDragonkai,

@ CMCDragonkai: Evet. Kullanıcı başına tam olarak bir aktif e-posta zorlanır. Hiçbir şey, aynı kullanıcı için daha fazla (etkin olmayan) e-posta eklemenizi engelleyemez. Etkin e-posta için özel bir rol istemiyorsanız, tetikleyiciler (daha az katı) bir alternatif olacaktır. Ancak tüm güncellemeleri ve silmeleri örtmek için dikkatli olmalısınız. Buna ihtiyacın olursa bir soru sormanı öneririm .
Erwin Brandstetter,

Kullanıcıları kullanmadan silmenin bir yolu var mı ON DELETE CASCADE? Sadece merak ediyorum (kademeli kullanım şu an için iyi çalışıyor).
amoe

@amoe: Çeşitli yollar var. Verileri değiştiren CTE'ler, tetikleyiciler, kurallar, aynı işlemdeki çoklu ifadeler, hepsi kesin gereksinimlere bağlıdır. Bir cevaba ihtiyacınız olursa, özelliklerinizle yeni bir soru sorun. Bağlam için her zaman buna bağlantı kurabilirsiniz.
Erwin Brandstetter

5

Tabloya bir sütun ekleyebilirseniz, aşağıdaki şema neredeyse 1 çalışma olacaktır:

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

Yerel SQL Sunucumdan a_horse_with_no_name yardımı ile çevrildi

Bir açıklamada ypercube'ün dediği gibi , daha da ileri gidebilirsiniz:

  • Boolean sütunu bırakın; ve
  • Oluşturun UNIQUE INDEX ON emails (UserID) WHERE (EmailAddress = ActiveAddress)

Etkisi aynı, ama tartışmasız daha basit ve düzenli.


1 sorun mevcut kısıtlamaları sadece bir satır başka satırla 'etkin' olarak anılacaktır sağlamak olduğunu var o da aslında aktif değil. Postgres'i fazladan kısıtlamayı kendim uygulayacak kadar iyi tanımıyorum (en azından şu anda değil), ancak SQL Server'da bu şekilde yapılabilir:

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

Bu çaba, tam e-posta adresini kopyalamak yerine, bir vekil kullanarak orijinali biraz geliştirir.


4

Bunlardan herhangi birini şema değişikliği olmadan yapmanın tek yolu PL / PgSQL tetikleyicisidir.

"Tam olarak bir" durum için referansları karşılıklı olarak karşılıklı yapabilirsiniz DEFERRABLE INITIALLY DEFERRED. Yani A.b_id(FK) referansları B.b_id(PK) ve B.a_id(FK) referansları A.a_id(PK). Birçok ORM vb. Olsa ertelenebilir kısıtlamalarla baş edemez. Bu durumda active_address_id, bayrak kullanmak yerine , bir sütunda adresleme yapmak için kullanıcıdan ertelenen bir FK ekleyeceksiniz .activeaddress


FK olmak zorunda bile değil DEFERRABLE.
Erwin Brandstetter
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.