ユーザーがいて、各ユーザーが複数のメールアドレスを持つことができるとします
CREATE TABLE emails (
user_id integer,
email_address text,
is_active boolean
)
一部のサンプル行
user_id | email_address | is_active
1 | [email protected] | t
1 | [email protected] | f
1 | [email protected] | f
2 | [email protected] | t
すべてのユーザーがアクティブなアドレスを1つだけ持つという制約を適用したいと思います。 Postgresでこれを行うにはどうすればよいですか?私はこれを行うことができます:
CREATE UNIQUE INDEX "user_email" ON emails(user_id) WHERE is_active=true;
これは、1つ以上のアクティブなアドレスを持つユーザーからは保護しますが、すべてのアドレスがfalseに設定されていることからは保護しないと思います。
可能であれば、トリガーやpl/pgsqlスクリプトは避けたいと思います。現時点ではそれらがなく、セットアップが難しいためです。しかし、それが事実である場合、「これを行う唯一の方法はトリガーまたはpl/pgsqlを使用することです」と知っていただければ幸いです。
トリガーやPL/pgSQLはまったく必要ありません。
あなたはneedDEFERRABLE
制約さえもしません。
また、情報を重複して保存する必要はありません。
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つのアクティブなメール」にする。 (ユーザーごとに複数のメールを保存できますが、「アクティブ」なメールはありません。)
あなたはcan _active_email_fkey
_ DEFERRABLE
を作成して、さらに余裕を持たせることができます(ユーザーと電子メールをsameトランザクションの個別のコマンドに挿入する)が、不要。
インデックスカバレッジを最適化するために、_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', '[email protected]') -- new users with *1* active email
, ('usr2', '[email protected]')
, ('usr3', '[email protected]')
)
, 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()
です 以下のリンクされた回答で詳細に説明されています 。
knowでない場合、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, '[email protected]')
RETURNING *
)
UPDATE users u
SET email_id = e.email_id
FROM e
WHERE u.user_id = e.user_id;
_
単純なORMがこれに対処するほど賢くない場合は、SQLコマンドをサーバー側の関数にカプセル化できます。
密接に関連しており、十分な説明があります:
関連もあります:
DEFERRABLE
制約について:
nextval()
およびpg_get_serial_sequence()
について:
テーブルに列を追加できる場合、次のスキームはほぼになります。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;
_
a_horse_with_no_name の助けを借りて、私のネイティブSQLサーバーから変換
ypercube がコメントで言及されているように、さらに先に進むこともできます。
UNIQUE INDEX ON emails (UserID) WHERE (EmailAddress = ActiveAddress)
を作成します効果は同じですが、間違いなく単純で端正です。
1 問題は、既存の制約が、別の行によって「アクティブ」と呼ばれる行existsのみを保証することであり、実際にアクティブであることではないことです。私は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';
_
この取り組みは、完全な電子メールアドレスを複製するのではなく、代理を使用することにより、元のものを少し改善します。
スキーマを変更せずにこれらのいずれかを行う唯一の方法は、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
フラグを使用すること。