problema de violación de restricción de clave externa

10

He identificado 3 situaciones.

  1. Un estudiante sin matrículas.
  2. Un estudiante con matrículas pero sin calificaciones.
  3. Un estudiante con matrículas y calificaciones.

Hay un disparador en la tabla de inscripciones para calcular el GPA. Si un estudiante tiene calificaciones, actualizará o insertará una entrada en la tabla de GPA; sin calificaciones, sin entrada en la tabla de GPA.

Puedo eliminar a un estudiante sin inscripciones (# 1). Puedo eliminar a un estudiante con inscripciones y calificaciones (# 3 arriba). Pero no puedo eliminar a un estudiante con matrículas pero sin calificaciones (# 2). Me sale una violación de restricción de referencia.

La instrucción DELETE está en conflicto con la restricción REFERENCE "FK_dbo.GPA_dbo.Student_StudentID". El conflicto ocurrió en la base de datos "", tabla "dbo.GPA", columna 'StudentID'.

Si no pudiera eliminar a un nuevo estudiante sin inscripciones (y sin ingreso de GPA), entonces entendería la violación de la restricción, pero puedo eliminar a ese estudiante. Es un estudiante con inscripciones y sin calificaciones (y aún sin ingreso de GPA) que no puedo eliminar.

He parchado mi gatillo para poder seguir adelante. Ahora, si tiene inscripciones, el disparador lo inserta en la tabla de GPA sin importar qué. Pero no entiendo el problema subyacente. Cualquier explicación sería muy apreciada.

Por lo que vale:

  1. Visual Studio 2013 Profesional.
  2. IIS express (interno a VS2013).
  3. Aplicación web ASP.NET con EntityFramework 6.1.1.
  4. MS SQL Server 2014 Enterprise.
  5. GPA.Value es anulable.
  6. Enrollment.GradeID es anulable.

Aquí hay un fragmento de la base de datos:

imagen de base de datos

- EDITAR -

Todas las tablas son creadas por EntityFramework, utilicé SQL Server Management Studio para producirlas.

Aquí están las instrucciones de crear tabla con restricciones .:

GPA mesa:

CREATE TABLE [dbo].[GPA](
    [StudentID] [int] NOT NULL,
    [Value] [float] NULL,
  CONSTRAINT [PK_dbo.GPA] PRIMARY KEY CLUSTERED 
  (
    [StudentID] ASC
  )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, 
         ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [dbo].[GPA]  WITH CHECK 
  ADD  CONSTRAINT [FK_dbo.GPA_dbo.Student_StudentID] 
  FOREIGN KEY([StudentID])
  REFERENCES [dbo].[Student] ([ID])

ALTER TABLE [dbo].[GPA] 
  CHECK CONSTRAINT [FK_dbo.GPA_dbo.Student_StudentID]

Enrollment mesa:

CREATE TABLE [dbo].[Enrollment](
    [EnrollmentID] [int] IDENTITY(1,1) NOT NULL,
    [CourseID] [int] NOT NULL,
    [StudentID] [int] NOT NULL,
    [GradeID] [int] NULL,
  CONSTRAINT [PK_dbo.Enrollment] PRIMARY KEY CLUSTERED 
  (
    [EnrollmentID] ASC
  )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, 
         ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [dbo].[Enrollment]  WITH CHECK 
  ADD  CONSTRAINT [FK_dbo.Enrollment_dbo.Course_CourseID] 
  FOREIGN KEY([CourseID])
  REFERENCES [dbo].[Course] ([CourseID])
  ON DELETE CASCADE

ALTER TABLE [dbo].[Enrollment] 
  CHECK CONSTRAINT [FK_dbo.Enrollment_dbo.Course_CourseID]

ALTER TABLE [dbo].[Enrollment]  WITH CHECK 
  ADD  CONSTRAINT [FK_dbo.Enrollment_dbo.Grade_GradeID] 
  FOREIGN KEY([GradeID])
  REFERENCES [dbo].[Grade] ([GradeID])

ALTER TABLE [dbo].[Enrollment] 
  CHECK CONSTRAINT [FK_dbo.Enrollment_dbo.Grade_GradeID]

ALTER TABLE [dbo].[Enrollment]  WITH CHECK 
  ADD  CONSTRAINT [FK_dbo.Enrollment_dbo.Student_StudentID] 
  FOREIGN KEY([StudentID])
  REFERENCES [dbo].[Student] ([ID])
  ON DELETE CASCADE

ALTER TABLE [dbo].[Enrollment] 
  CHECK CONSTRAINT [FK_dbo.Enrollment_dbo.Student_StudentID]

Student mesa:

CREATE TABLE [dbo].[Student](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [EnrollmentDate] [datetime] NOT NULL,
    [LastName] [nvarchar](50) NOT NULL,
    [FirstName] [nvarchar](50) NOT NULL,
  CONSTRAINT [PK_dbo.Student] PRIMARY KEY CLUSTERED 
  (
    [ID] ASC
  )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, 
         ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

Aquí están los desencadenantes :

CREATE TRIGGER UpdateGPAFromUpdateDelete
ON Enrollment
AFTER UPDATE, DELETE AS
BEGIN
    DECLARE @UpdatedStudentID AS int
    SELECT @UpdatedStudentID = StudentID FROM DELETED
    EXEC MergeGPA @UpdatedStudentID
END

CREATE TRIGGER UpdateGPAFromInsert
ON Enrollment
AFTER INSERT AS
--DECLARE @InsertedGradeID AS int
--SELECT @InsertedGradeID = GradeID FROM INSERTED
--IF @InsertedGradeID IS NOT NULL
    BEGIN
        DECLARE @InsertedStudentID AS int
        SELECT @InsertedStudentID = StudentID FROM INSERTED
        EXEC MergeGPA @InsertedStudentID
    END

El parche para avanzar era comentar esas líneas en el AFTER INSERTgatillo.

Aquí está el procedimiento almacenado :

CREATE PROCEDURE MergeGPA @StudentID int AS
MERGE GPA AS TARGET
USING (SELECT @StudentID) as SOURCE (StudentID)
ON (TARGET.StudentID = SOURCE.StudentID)
WHEN MATCHED THEN
    UPDATE
        SET Value = (SELECT Value FROM GetGPA(@StudentID))
WHEN NOT MATCHED THEN
INSERT (StudentID, Value)
    VALUES(SOURCE.StudentID, (SELECT Value FROM GetGPA(@StudentID)));

Aquí está la función de base de datos :

CREATE FUNCTION GetGPA (@StudentID int) 
RETURNS TABLE
AS RETURN
SELECT ROUND(SUM (StudentTotal.TotalCredits) / SUM (StudentTotal.Credits), 2) Value
    FROM (
        SELECT 
            CAST(Credits as float) Credits
            , CAST(SUM(Value * Credits) as float) TotalCredits
        FROM 
            Enrollment e 
            JOIN Course c ON c.CourseID = e.CourseID
            JOIN Grade g  ON e.GradeID = g.GradeID
        WHERE
            e.StudentID = @StudentID AND
            e.GradeID IS NOT NULL
        GROUP BY
            StudentID
            , Value
            , e.courseID
            , Credits
    ) StudentTotal

Aquí está la salida de depuración del método de eliminación del controlador, la instrucción select es el método que consulta qué eliminar. Este estudiante tiene 3 inscripciones, el REFERENCEproblema de restricción ocurre cuando se elimina la tercera inscripción. Supongo que EF está utilizando una transacción porque las inscripciones no se eliminan.

iisexpress.exe Information: 0 : Component:SQL Database;Method:SchoolInterceptor.ReaderExecuted;Timespan:00:00:00.0004945;Properties:
Command: SELECT 
    [Project2].[StudentID] AS [StudentID], 
    [Project2].[ID] AS [ID], 
    [Project2].[EnrollmentDate] AS [EnrollmentDate], 
    [Project2].[LastName] AS [LastName], 
    [Project2].[FirstName] AS [FirstName], 
    [Project2].[Value] AS [Value], 
    [Project2].[C1] AS [C1], 
    [Project2].[EnrollmentID] AS [EnrollmentID], 
    [Project2].[CourseID] AS [CourseID], 
    [Project2].[StudentID1] AS [StudentID1], 
    [Project2].[GradeID] AS [GradeID]
    FROM ( SELECT 
        [Limit1].[ID] AS [ID], 
        [Limit1].[EnrollmentDate] AS [EnrollmentDate], 
        [Limit1].[LastName] AS [LastName], 
        [Limit1].[FirstName] AS [FirstName], 
        [Limit1].[StudentID] AS [StudentID], 
        [Limit1].[Value] AS [Value], 
        [Extent3].[EnrollmentID] AS [EnrollmentID], 
        [Extent3].[CourseID] AS [CourseID], 
        [Extent3].[StudentID] AS [StudentID1], 
        [Extent3].[GradeID] AS [GradeID], 
        CASE WHEN ([Extent3].[EnrollmentID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
        FROM   (SELECT TOP (2) 
            [Extent1].[ID] AS [ID], 
            [Extent1].[EnrollmentDate] AS [EnrollmentDate], 
            [Extent1].[LastName] AS [LastName], 
            [Extent1].[FirstName] AS [FirstName], 
            [Extent2].[StudentID] AS [StudentID], 
            [Extent2].[Value] AS [Value]
            FROM  [dbo].[Student] AS [Extent1]
            LEFT OUTER JOIN [dbo].[GPA] AS [Extent2] ON [Extent1].[ID] = [Extent2].[StudentID]
            WHERE [Extent1].[ID] = @p__linq__0 ) AS [Limit1]
        LEFT OUTER JOIN [dbo].[Enrollment] AS [Extent3] ON [Limit1].[ID] = [Extent3].[StudentID]
    )  AS [Project2]
    ORDER BY [Project2].[StudentID] ASC, [Project2].[ID] ASC, [Project2].[C1] ASC: 
iisexpress.exe Information: 0 : Component:SQL Database;Method:SchoolInterceptor.NonQueryExecuted;Timespan:00:00:00.0012696;Properties:
Command: DELETE [dbo].[Enrollment]
WHERE ([EnrollmentID] = @0): 
iisexpress.exe Information: 0 : Component:SQL Database;Method:SchoolInterceptor.NonQueryExecuted;Timespan:00:00:00.0002634;Properties:
Command: DELETE [dbo].[Enrollment]
WHERE ([EnrollmentID] = @0): 
iisexpress.exe Information: 0 : Component:SQL Database;Method:SchoolInterceptor.NonQueryExecuted;Timespan:00:00:00.0002512;Properties:
Command: DELETE [dbo].[Enrollment]
WHERE ([EnrollmentID] = @0): 
iisexpress.exe Error: 0 : Error executing command: DELETE [dbo].[Student]
WHERE ([ID] = @0) Exception: System.Data.SqlClient.SqlException (0x80131904): The DELETE statement conflicted with the REFERENCE constraint "FK_dbo.GPA_dbo.Student_StudentID". The conflict occurred in database "<databasename>", table "dbo.GPA", column 'StudentID'.
The statement has been terminated.
DowntownHippie
fuente

Respuestas:

7

Es una cuestión de tiempo. Considere eliminar StudentID # 1:

  1. La fila se elimina de la Studenttabla.
  2. La eliminación en cascada elimina las filas correspondientes de Enrollment
  3. La relación de clave externa GPA-> Studentestá marcada
  4. El gatillo dispara, llamando MergeGPA

En este punto, MergeGPAverifica si hay una entrada para el Estudiante # 1 en la GPAtabla. No existe (de lo contrario, la verificación FK en el paso 3 habría provocado un error).

Entonces, la WHEN NOT MATCHEDcláusula en los MergeGPAintentos de INSERTuna fila GPApara StudentID # 1. Este intento falla (con el error FK) porque StudentID # 1 ya se ha eliminado de la Studenttabla (en el paso 1).

Paul White 9
fuente
1
Creo que estás haciendo algo. Cuando un estudiante se crea con inscripciones, pero no se han asignado calificaciones, ese estudiante no tiene entrada en la tabla de GPA. Cuando la base de datos va a eliminar a ese estudiante, mira la base de datos, ve las inscripciones para eliminar pero no hay entrada de GPA. Por lo tanto, se trata de eliminar las inscripciones, lo que hace que se active un disparador que crea la entrada de GPA, lo que luego causa la violación de la restricción. Entonces, la solución es crear una entrada de GPA cuando creo un estudiante. Entonces mi disparador de inserción no necesitará un condicional, y mi procedimiento almacenado no necesitará ser una fusión, solo una actualización.
DowntownHippie
-1

Sin leer todo, solo del diagrama: tiene una entrada en Inscripción o una en GPA que apunta al Estudiante que desea eliminar.

Las entradas con las claves foráneas deben eliminarse primero (o las claves deben configurarse como nulas, pero eso es una mala práctica) antes de que pueda eliminar la entrada del Estudiante.

Además, algunas bases de datos tienen ON DELETE CASCADE, que eliminará cualquier entrada con claves externas a la que desea eliminar.

Otra forma es no declararlas como claves foráneas y solo usar el valor de la clave, pero eso tampoco se recomienda.

usuario44286
fuente
En los casos en que está fallando, hay una entrada en la Inscripción pero no una en el GPA.
DowntownHippie
tiene algunas restricciones con ON DELETE CASCADE y algunas sin. intente agregar esa línea a todas las restricciones. después de eso intentaría deshabilitar todos los disparadores y después de esa prueba con una configuración mínima. buena suerte
user44286
Veo esas ON DELETE CASCADEdeclaraciones. Ninguna de estas declaraciones de creación de tablas, ni las declaraciones de eliminación están escritas a mano, todas son generadas por el marco de la entidad. Las cascadas se deben a que la inscripción tiene claves externas que no son su clave principal; La restricción de clave externa de GPA es su clave principal, por lo que no debería necesitar una cascada. He probado esto, si eliminas a un estudiante con una entrada de tabla de GPA, la entrada se elimina. El único problema es un estudiante con matrículas pero sin gpa.
DowntownHippie