Search code examples
sql-servertriggersentity-framework-4rowcount

EF4 RowCount issue on instead of insert trigger while updating an other table


I have some trouble with entityFramework 4. Here is the thing : We have a SQL server database. Every table have 3 instead of triggers for insert, update and delete. We know EntityFramework has some issues to deal with theses triggers, that's why we added the following code at the end of triggers to force the rowCount :

for insert :

DECLARE @Identifier BIGINT;
SET @Identifier = scope_identity()
SELECT @Identifier AS Identifier

for update/delete :

CREATE TABLE #TempTable (temp INT PRIMARY KEY);
INSERT INTO #TempTable VALUES (1);
DROP TABLE #TempTable

It worked fine until now : From an instead of insert trigger (let's say table A), I try to update a field of an other table (table B)

I know my update code perfectly work since a manual insert does the work. The issue shows up only when I'm using Entity framework.


Solution

  • I have the solution now, let's make a school case of this with a full example. :)

    In this example, our application is an addressBook. We want to update the business Activity (IsActive column in Business) everytime we add, update or delete a contact on this business. The business is considered as active if at least one of the contact of the business is active. We record every state changements on the business in a table to have the full history.

    So, we have 3 tables :

    table Business (Identifier (PK Identity), Name, IsActive), table Contact (Identifier (PK Identity), Name, IsActive, IdentifierBusiness) table BusinessHistory (Identifier (PK Identity), IsActive, Date, IdentifierBusiness)

    Here's are the triggers one we are interested in :

    table Contact (trigger IoInsert):

    -- inserting the new rows
    INSERT INTO Contact
    (
         Name
        ,IsActive
        ,IdentifierBusiness
    )
    SELECT
        t0.Name
        ,t0.IsActive
        ,t0.IdentifierBusiness
    FROM
        inserted AS t0
    -- Updating the business
    
    UPDATE
        Business
    SET
        IsActive = CASE WHEN 
                (
                    (t0.IsActive = 1 AND Business.IsActive = 1)
                    OR
                    (t0.IsActive = 1 AND Business.IsActive = 0)
                ) THEN 1 ELSE 0
    FROM
        inserted AS t0
    WHERE
        Business.Identifier = t0.IdentifierBusiness
    AND
        t0.IsActive = 1
    AND
        Business.IsActive = 0   
    
    
    
    -- Forcing rowCount for EntityFramework 
    DECLARE @Identifier BIGINT;
    SET @Identifier = scope_identity()
    SELECT @Identifier AS Identifier
    

    Table Business (trigger IoUpdate)

    UPDATE
        Business
    SET
        IsActive = 1
    FROM
        Contact AS t0
    WHERE
        Business.Identifier = t0.IdentifierBusiness
    AND
        t0.IsActive = 1
    AND
        Business.IsActive = 0   
    
    
    ---- Updating BusinessHistory
    
    INSERT INTO BusinessHistory
    (
        Date
        ,IsActive
        ,IdentifierBusiness
    )
    SELECT
        DATE()
        ,t0.IsActive
        ,t0.Identifier
    FROM
        inserted AS t0
    INNER JOIN
        deleted AS t1 ON t0.Identifier = t1.Identifier
    WHERE   
        (t0.Identifier <> t1.Identifier)
    
    
    -- Forcing rowCount for EntityFramework 
    CREATE TABLE #TempTable (temp INT PRIMARY KEY);
    INSERT INTO #TempTable VALUES (1);
    DROP TABLE #TempTable   
    

    Table BusinessHistory :

    -- Updating the business
    
    UPDATE
        Business
    SET
        IsActive = CASE WHEN 
                (
                    (t0.IsActive = 1 AND Business.IsActive = 1)
                    OR
                    (t0.IsActive = 1 AND Business.IsActive = 0)
                ) THEN 1 ELSE 0
    FROM
        inserted AS t0
    WHERE
        Business.Identifier = t0.IdentifierBusiness
    AND
        t0.IsActive = 1
    AND
        Business.IsActive = 0   
    
    -- inserting the new rows
    INSERT INTO BusinessHistory
    (
        Date
        ,IsActive
        ,IdentifierBusiness
    )
    SELECT
        DATE()
        ,t0.IsActive
        ,t0.Identifier
    FROM
        inserted AS t0
    
    -- Forcing rowCount for EntityFramework 
    DECLARE @Identifier BIGINT;
    SET @Identifier = scope_identity()
    SELECT @Identifier AS Identifier
    

    So, in a nutshell, what happened ?

    We have 2 tables, Business and Contact. Contact is updating table Business on insert and update.

    When Business is updated, it does an insert into BusinessHistory, which is storing the history of updates of table Business ,when the field IsActive is updated.

    the thing is, even if I don't insert a new row in BusinessHistory, I launch an insert instruction and so, I go inside the instead of insert trigger of the table BusinessHistory. Of course, in the end of this one, there is a scope_identity(). You can use scope_identity only once, and it gives back the last identity inserted. So, since I did not inserted any BusinessHistory, it was consuming the scope_identity of my newly inserted contact : the scope_identity of the instead of insert of the contact table was empty !

    How to isolate the issue ?

    • Using the profiler, you figure out that there are insert instruction in BusinessHistory when it should not be any of them.

    • Using the debugging, you will eventually end in the an insert trigger your are not supposed to be in.

    How to fix it ?

    Several alternatives here. What I did was to surround in table Business the insert of BusinessHistory by an If condition : I want the insert to be inserted only if the statut "IsActive" has changed :

    IF EXISTS
    (
        SELECT
            1
        FROM
            inserted AS t0
        INNER JOIN
            deleted AS t1 ON t0.Identifier = t1.Identifier
        WHERE
            (t0.IsActive <> t1.IsActive)
    )
    BEGIN
        INSERT INTO BusinessHistory
        (
            Date
            ,IsActive
            ,IdentifierBusiness
        )
        SELECT
            DATE()
            ,t0.IsActive
            ,t0.Identifier
        FROM
            inserted AS t0
        INNER JOIN
            deleted AS t1 ON t0.Identifier = t1.Identifier
        WHERE   
            (t0.IsActive <> t1.IsActive)
    END
    

    An other possibility is, in the trigger instead of insert of the table BusinessHistory, to surround the whole trigger by an IF EXISTS condition

    IF EXISTS (SELECT 1 FROM inserted)
    BEGIN
        ----Trigger's code here ! 
    END
    

    How to avoid it ?

    • Well, use one of these fixes !
    • Avoiding scope_identity(), @@IDENTITY is more than enough in most of the cases ! In my company, we only use scope_identity because of EF 4 !

    I know my english is not perfect, I can edit if it's not good enough, or if someone want to add something on this subject !