¿La restricción de clave externa puede causar ciclos o múltiples rutas en cascada?

176

Tengo un problema cuando intento agregar restricciones a mis tablas. Me sale el error:

La introducción de la restricción FOREIGN KEY 'FK74988DB24B3C886' en la tabla 'Empleado' puede causar ciclos o múltiples rutas en cascada. Especifique ON DELETE NO ACTION o ON UPDATE NO ACTION, o modifique otras restricciones de FOREIGN KEY.

Mi restricción es entre una Codemesa y una employeemesa. La Codetabla contiene Id, Name, FriendlyName, Typey Value. El employeetiene un número de campos que los códigos de referencia, por lo que no puede haber una referencia para cada tipo de código.

Necesito que los campos se establezcan como nulos si se elimina el código al que se hace referencia.

¿Alguna idea de cómo puedo hacer esto?

Ricardo Altamirano
fuente
Una de las soluciones está aquí
IsmailS

Respuestas:

180

SQL Server hace un recuento simple de rutas en cascada y, en lugar de tratar de determinar si realmente existen ciclos, asume lo peor y se niega a crear las acciones referenciales (CASCADE): puede y debe crear las restricciones sin las acciones referenciales. Si no puede alterar su diseño (o hacerlo comprometería las cosas), entonces debería considerar el uso de disparadores como último recurso.

FWIW resolver caminos en cascada es un problema complejo. Otros productos SQL simplemente ignorarán el problema y le permitirán crear ciclos, en cuyo caso será una carrera para ver cuál sobrescribirá el valor en último lugar, probablemente por ignorancia del diseñador (por ejemplo, ACE / Jet hace esto). Entiendo que algunos productos SQL intentarán resolver casos simples. El hecho es que SQL Server ni siquiera lo intenta, lo hace ultra seguro al no permitir más de una ruta y al menos así se lo dice.

Microsoft mismo aconseja el uso de disparadores en lugar de restricciones FK.

un día cuando
fuente
2
Una cosa que todavía no puedo entender es que, si este "problema" puede resolverse usando un disparador, ¿cómo es que un disparador no "causará ciclos o múltiples rutas en cascada ..."?
armen
55
@armen: debido a que su activador proporcionará explícitamente la lógica que el sistema no podría descubrir de manera implícita por sí mismo, por ejemplo, si hay múltiples rutas para una acción referencial de eliminación, entonces su código de activador definirá qué tablas se eliminarán y en qué orden.
cuando el
66
Y también el disparador se ejecuta después de que se completa la primera operación, por lo que no hay carrera en curso.
Bon
2
@dumbledad: Quiero decir, solo use disparadores cuando las restricciones (tal vez en combinación) no puedan hacer el trabajo. Las restricciones son declarativas y sus implementaciones son responsabilidad del sistema. Los disparadores son código de procedimiento y debe codificar (y depurar) la implementación y soportar sus desventajas (peor rendimiento, etc.).
cuando
1
El problema con esto es que el desencadenante solo funciona siempre que elimine la restricción de clave externa, lo que significa que no tiene una comprobación de integridad referencial en las inserciones de la base de datos y, por lo tanto, necesita aún más desencadenantes para manejar eso. La solución de activación es un agujero de conejo que conduce a un diseño de base de datos degenerado.
Neutrino
99

Una situación típica con múltiples rutas en cascada será esta: una tabla maestra con dos detalles, digamos "Maestro" y "Detalle1" y "Detalle2". Ambos detalles son borrados en cascada. Hasta el momento no hay problemas. Pero, ¿qué pasa si ambos detalles tienen una relación de uno a muchos con alguna otra tabla (diga "SomeOtherTable")? SomeOtherTable tiene una columna Detail1ID Y una columna Detail2ID.

Master { ID, masterfields }

Detail1 { ID, MasterID, detail1fields }

Detail2 { ID, MasterID, detail2fields }

SomeOtherTable {ID, Detail1ID, Detail2ID, someothertablefields }

En otras palabras: algunos de los registros en SomeOtherTable están vinculados con registros Detail1 y algunos de los registros en SomeOtherTable están vinculados con registros Detail2. Incluso si se garantiza que los registros SomeOtherTable nunca pertenezcan a ambos Detalles, ahora es imposible hacer que la cascada de registros de SomeOhterTable se elimine para ambos detalles, porque hay múltiples rutas en cascada desde Master a SomeOtherTable (una a través de Detail1 y otra a través de Detail2). Ahora puede que ya hayas entendido esto. Aquí hay una posible solución:

Master { ID, masterfields }

DetailMain { ID, MasterID }

Detail1 { DetailMainID, detail1fields }

Detail2 { DetailMainID, detail2fields }

SomeOtherTable {ID, DetailMainID, someothertablefields }

Todos los campos de ID son campos clave e incremento automático. El quid se encuentra en los campos DetailMainId de las tablas Detail. Estos campos son clave y referencia referencial. Ahora es posible eliminar todo en cascada eliminando solo los registros maestros. La desventaja es que para cada registro detail1 Y para cada registro detail2, también debe haber un registro DetailMain (que en realidad se crea primero para obtener la identificación correcta y única).

Hans Riesebos
fuente
1
Su comentario me ayudó mucho a entender el problema que estoy enfrentando. ¡Gracias! Preferiría desactivar la eliminación en cascada para una de las rutas, luego manejar la eliminación de otros registros de otras maneras (procedimientos almacenados; disparadores; por código, etc.). Pero tengo en cuenta su solución (agrupación en una ruta) para posibles aplicaciones diferentes del mismo problema ...
libre albedrío
1
Uno para usar la palabra crux (y también para explicar)
masterwok
¿Es esto mejor que escribir disparadores? Parece extraño agregar una tabla adicional solo para que la cascada funcione.
dumbledad
Cualquier cosa es mejor que escribir disparadores. Su lógica es opaca y son ineficientes en comparación con cualquier otra cosa. Romper tablas grandes en tablas más pequeñas para un control más fino es solo una consecuencia natural de una mejor base de datos normalizada y no es en sí mismo algo de lo que preocuparse.
Neutrino
12

Señalaría que (funcionalmente) hay una GRAN diferencia entre ciclos y / o múltiples rutas en el ESQUEMA y los DATOS. Si bien los ciclos y quizás las trayectorias múltiples en los DATOS ciertamente podrían complicar el procesamiento y causar problemas de rendimiento (costo de manejo "adecuado"), el costo de estas características en el esquema debería ser cercano a cero.

Dado que la mayoría de los ciclos aparentes en los RDB ocurren en estructuras jerárquicas (organigrama, parte, subparte, etc.) es lamentable que SQL Server asuma lo peor; es decir, ciclo de esquema == ciclo de datos. De hecho, si estás usando restricciones de RI, ¡no puedes construir un ciclo en los datos!

Sospecho que el problema de trayectos múltiples es similar; es decir, múltiples rutas en el esquema no necesariamente implican múltiples rutas en los datos, pero tengo menos experiencia con el problema de múltiples rutas.

Por supuesto, si SQL Server no permite ciclos que aún estaría sujeta a una profundidad de 32, pero eso es probablemente adecuado para la mayoría de los casos. (¡Lástima que no sea una configuración de base de datos, sin embargo!)

Los disparadores "En lugar de Eliminar" tampoco funcionan. La segunda vez que se visita una tabla, se ignora el activador. Entonces, si realmente quieres simular una cascada, tendrás que usar procedimientos almacenados en presencia de ciclos. Sin embargo, el disparador en lugar de eliminar funcionaría para casos de múltiples rutas.

Celko sugiere una "mejor" forma de representar las jerarquías que no introduce ciclos, pero hay compensaciones.

Bill Cohagan
fuente
"¡Si estás usando restricciones de RI, no puedes construir un ciclo en los datos!" -- ¡buen punto!
cuando el
Claro que puede construir circularidad de datos, pero con MSSQL solo usando ACTUALIZACIÓN. Otros RDBM admiten restricciones diferidas (integridad asegurada en el momento de la confirmación, no en el momento de la inserción / actualización / eliminación).
Carl Krig
3

Según parece, tiene una acción OnDelete / OnUpdate en una de sus claves externas existentes, que modificará su tabla de códigos.

Entonces, al crear esta clave externa, crearía un problema cíclico,

Por ejemplo, actualizar empleados, hace que los códigos cambien por una acción de actualización, hace que los empleados cambien por una acción de actualización ... etc ...

Si publica sus definiciones de tabla para ambas tablas y sus definiciones de clave externa / restricción, deberíamos poder decirle dónde está el problema ...

Eoin Campbell
fuente
1
Son bastante largos, así que no creo que pueda publicarlos aquí, pero agradecería mucho su ayuda. ¿No sabe si hay alguna forma de enviárselos? Lo intentaré y lo describiré: las únicas restricciones que existen son de 3 tablas que tienen campos que hacen referencia a los códigos mediante una simple clave INT Id. El problema parece ser que Employee tiene varios campos que hacen referencia a la tabla de códigos y que quiero que todos se conecten en cascada a SET NULL. Todo lo que necesito es que cuando se eliminen los códigos, las referencias a ellos deben establecerse como nulas en todas partes.
publíquelos de todos modos ... No creo que a nadie le importe, y la ventana de código los formateará correctamente en un bloque de desplazamiento :)
Eoin Campbell
2

Esto se debe a que Emplyee podría tener una Colección de otra entidad, por ejemplo, Calificaciones y Calificación podría tener algunas otras universidades de colección, por ejemplo

public class Employee{
public virtual ICollection<Qualification> Qualifications {get;set;}

}

public class Qualification{

public Employee Employee {get;set;}

public virtual ICollection<University> Universities {get;set;}

}

public class University{

public Qualification Qualification {get;set;}

}

En DataContext podría ser como a continuación

protected override void OnModelCreating(DbModelBuilder modelBuilder){

modelBuilder.Entity<Qualification>().HasRequired(x=> x.Employee).WithMany(e => e.Qualifications);
modelBuilder.Entity<University>.HasRequired(x => x.Qualification).WithMany(e => e.Universities);

}

en este caso hay una cadena de empleados a calificación y de calificación a universidades. Entonces me estaba lanzando la misma excepción.

Funcionó para mí cuando cambié

    modelBuilder.Entity<Qualification>().**HasRequired**(x=> x.Employee).WithMany(e => e.Qualifications); 

A

    modelBuilder.Entity<Qualification>().**HasOptional**(x=> x.Employee).WithMany(e => e.Qualifications);
Rajnikant
fuente
1

Trigger es la solución para este problema:

IF OBJECT_ID('dbo.fktest2', 'U') IS NOT NULL
    drop table fktest2
IF OBJECT_ID('dbo.fktest1', 'U') IS NOT NULL
    drop table fktest1
IF EXISTS (SELECT name FROM sysobjects WHERE name = 'fkTest1Trigger' AND type = 'TR')
    DROP TRIGGER dbo.fkTest1Trigger
go
create table fktest1 (id int primary key, anQId int identity)
go  
    create table fktest2 (id1 int, id2 int, anQId int identity,
        FOREIGN KEY (id1) REFERENCES fktest1 (id)
            ON DELETE CASCADE
            ON UPDATE CASCADE/*,    
        FOREIGN KEY (id2) REFERENCES fktest1 (id) this causes compile error so we have to use triggers
            ON DELETE CASCADE
            ON UPDATE CASCADE*/ 
            )
go

CREATE TRIGGER fkTest1Trigger
ON fkTest1
AFTER INSERT, UPDATE, DELETE
AS
    if @@ROWCOUNT = 0
        return
    set nocount on

    -- This code is replacement for foreign key cascade (auto update of field in destination table when its referenced primary key in source table changes.
    -- Compiler complains only when you use multiple cascased. It throws this compile error:
    -- Rrigger Introducing FOREIGN KEY constraint on table may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, 
    -- or modify other FOREIGN KEY constraints.
    IF ((UPDATE (id) and exists(select 1 from fktest1 A join deleted B on B.anqid = A.anqid where B.id <> A.id)))
    begin       
        update fktest2 set id2 = i.id
            from deleted d
            join fktest2 on d.id = fktest2.id2
            join inserted i on i.anqid = d.anqid        
    end         
    if exists (select 1 from deleted)       
        DELETE one FROM fktest2 one LEFT JOIN fktest1 two ON two.id = one.id2 where two.id is null -- drop all from dest table which are not in source table
GO

insert into fktest1 (id) values (1)
insert into fktest1 (id) values (2)
insert into fktest1 (id) values (3)

insert into fktest2 (id1, id2) values (1,1)
insert into fktest2 (id1, id2) values (2,2)
insert into fktest2 (id1, id2) values (1,3)

select * from fktest1
select * from fktest2

update fktest1 set id=11 where id=1
update fktest1 set id=22 where id=2
update fktest1 set id=33 where id=3
delete from fktest1 where id > 22

select * from fktest1
select * from fktest2
Tono Škoda
fuente
0

Este es un error de las políticas de activación de bases de datos de tipo. Un activador es código y puede agregar algunas inteligencias o condiciones a una relación de Cascade como Cascade Deletion. Es posible que deba especializar las opciones de tablas relacionadas alrededor de esto, como desactivar CascadeOnDelete :

protected override void OnModelCreating( DbModelBuilder modelBuilder )
{
    modelBuilder.Entity<TableName>().HasMany(i => i.Member).WithRequired().WillCascadeOnDelete(false);
}

O apague esta función por completo:

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
Amirhossein Mehrvarzi
fuente
-2

Mi solución a este problema encontrado usando ASP.NET Core 2.0 y EF Core 2.0 fue realizar lo siguiente en orden:

  1. Ejecute el update-databasecomando en Package Management Console (PMC) para crear la base de datos (esto da como resultado el error "Introducción de la CLAVE EXTRANJERA ... puede causar ciclos o múltiples rutas en cascada". Error)

  2. Ejecute el script-migration -Idempotentcomando en PMC para crear un script que se pueda ejecutar independientemente de las tablas / restricciones existentes

  3. Tome la secuencia de comandos resultante y busque ON DELETE CASCADEy reemplace conON DELETE NO ACTION

  4. Ejecute el SQL modificado en la base de datos

Ahora, sus migraciones deberían estar actualizadas y las eliminaciones en cascada no deberían ocurrir.

Lástima que no pude encontrar ninguna manera de hacer esto en Entity Framework Core 2.0.

¡Buena suerte!

usuario1477388
fuente
Puede cambiar su archivo de migración para hacerlo (sin cambiar la secuencia de comandos sql), es decir, en su archivo de migración puede establecer la acción Eliminar para restringir desde Cascade
Rushi Soni
Es mejor especificar esto usando anotaciones fluidas para que no tenga que acordarse de hacer esto si termina eliminando y recreando su carpeta de migraciones.
Allen Wang
En mi experiencia, las anotaciones fluidas se pueden usar y se deben usar (las uso), pero a menudo son bastante defectuosas. Simplemente especificarlos en el código no siempre funciona produce el resultado esperado.
user1477388