No necesita disparadores o PL / pgSQL en absoluto.
Ni siquiera necesitas DEFERRABLE
restricciones.
Y no necesita almacenar ninguna información de forma redundante.
Incluya la ID del correo electrónico activo en la users
tabla, lo que resulta en referencias mutuas. Uno podría pensar que necesitamos una DEFERRABLE
restricción para resolver el problema del huevo y la gallina de insertar un usuario y su correo electrónico activo, pero usando CTE modificadores de datos ni siquiera necesitamos eso.
Esto aplica exactamente un correo electrónico activo por usuario en todo momento:
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);
Elimine la NOT NULL
restricción de users.email_id
para que sea "como máximo un correo electrónico activo". (Todavía puede almacenar varios correos electrónicos por usuario, pero ninguno de ellos está "activo").
Usted puede hacer active_email_fkey
DEFERRABLE
para permitir más libertad de acción (inserto de usuario y correo electrónico en comandos separados de la misma transacción), pero eso es innecesario .
Puse user_id
primero en la UNIQUE
restricción email_fk_uni
para optimizar la cobertura del índice. Detalles:
Vista opcional:
CREATE VIEW user_with_active_email AS
SELECT * FROM users JOIN email USING (user_id, email_id);
Así es como inserta nuevos usuarios con un correo electrónico activo (según sea necesario):
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);
La dificultad específica es que no tenemos user_id
ni email_id
para empezar. Ambos son números de serie proporcionados por los respectivos SEQUENCE
. No se puede resolver con una sola RETURNING
cláusula (otro problema de huevo y gallina). La solución es nextval()
como se explica en detalle en la siguiente respuesta vinculada .
Si no conoce el nombre de la secuencia adjunta para la serial
columna email.email_id
, puede reemplazarla:
nextval('email_email_id_seq'::regclass)
con
nextval(pg_get_serial_sequence('email', 'email_id'))
Así es como agrega un nuevo correo electrónico "activo":
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;
SQL Fiddle.
Puede encapsular los comandos SQL en funciones del lado del servidor si algún ORM de mente simple no es lo suficientemente inteligente como para hacer frente a esto.
Estrechamente relacionado, con amplia explicación:
También relacionado:
Sobre DEFERRABLE
restricciones:
Sobre nextval()
y pg_get_serial_sequence()
:
ON DELETE CASCADE
? Simplemente curioso (la conexión en cascada funciona bien por ahora).Si usted puede agregar una columna a la tabla, el siguiente esquema haría casi 1 de trabajo:
Test SQLFiddle
Traducido de mi SQL Server nativo, con la ayuda de a_horse_with_no_name
Como mencionó ypercube en un comentario, incluso podría ir más allá:
UNIQUE INDEX ON emails (UserID) WHERE (EmailAddress = ActiveAddress)
El efecto es el mismo, pero podría decirse que es más simple y ordenado.
1 El problema es que las limitaciones existentes sólo aseguran que una fila se denomina 'activa' por otra fila existe , no que también es realmente activa. No conozco Postgres lo suficientemente bien como para implementar la restricción adicional yo mismo (al menos no en este momento), pero en SQL Server, podría hacerse de esta manera:
Este esfuerzo mejora un poco en el original mediante el uso de un sustituto en lugar de duplicar la dirección de correo electrónico completa.
fuente
La única forma de hacer cualquiera de estos sin cambios de esquema es con un disparador PL / PgSQL.
Para el caso "exactamente uno", puede hacer que las referencias sean mutuas, con un solo ser
DEFERRABLE INITIALLY DEFERRED
. EntoncesA.b_id
(FK) referenciasB.b_id
(PK) yB.a_id
(FK) referenciasA.a_id
(PK). Sin embargo, muchos ORM, etc., no pueden hacer frente a restricciones diferibles. Entonces, en este caso, agregaría un FK diferido del usuario a la dirección en una columnaactive_address_id
, en lugar de usar unactive
indicadoraddress
.fuente
DEFERRABLE
.