データベースで「少なくとも1つ」または「正確に1つ」を強制する制約


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

すべてのユーザーがアクティブなアドレスを1つだけ持つという制約を施行したい。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を使用すれば、それさえ必要ありません。

これにより、ユーザーごとに常に1つのアクティブな電子メールが常に強制されます。

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「最大で1つのアクティブな電子メール」にします。(ユーザーごとに複数のメールを保存できますが、いずれも「アクティブ」ではありません。)

あなたは可能作る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;

SQLフィドル。

単純なORMがこれに対処するのに十分にスマートでない場合、サーバー側の関数にSQLコマンドをカプセル化できます。

密接な関係があり、十分な説明があります:

関連するもの:

DEFERRABLE制約について:

nextval()pg_get_serial_sequence()


これは、少なくとも1対1の関係に適用できますか?この回答に示されているように、1 -1ではありません。
CMCDragonkai

@CMCDragonkai:はい。ユーザーごとに厳密に1つのアクティブな電子メールが適用されます。同じユーザーに(非アクティブな)メールを追加することを妨げるものはありません。アクティブな電子メールに特別な役割が必要ない場合は、トリガーが(厳密ではない)代替手段になります。ただし、すべての更新と削除に注意する必要があります。これが必要な場合は質問することをお勧めします。
アーウィンブランドステッター

使用せずにユーザーを削除する方法はあります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

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トリガーを使用することです。

「1つだけ」の場合、参照を相互に作成できます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
アーウィンブランドステッター
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.