Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SQL Server update does not get values updated by trigger. #6474

Closed
casperOne opened this issue Sep 5, 2016 · 1 comment
Closed

SQL Server update does not get values updated by trigger. #6474

casperOne opened this issue Sep 5, 2016 · 1 comment
Labels
closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. type-bug
Milestone

Comments

@casperOne
Copy link

casperOne commented Sep 5, 2016

I'm on 1.0.0 of Microsoft.EntityFramework.Core and Microsoft.EntityFramework.Core.SqlServer.

I have a table with two columns that are updated by an AFTER trigger (not an INSTEAD OF):

__lastModified (datetimeoffset)
__version (int)

There's also a __rowVersion column which is of type rowversion.

When I call SaveChanges, I don't get the values after the trigger fires, I get the values before the trigger is fired.

I logged the SQL, and this is what I get:

DECLARE @inserted0 TABLE ([__lastModified] datetimeoffset, [__rowVersion] varbinary(8), [__version] int);
UPDATE [stats].[EventSeries] SET [description] = @p0, [twitterScreenName] = @p1, [__lastModifiedByUserId] = @p2
OUTPUT INSERTED.[__lastModified], INSERTED.[__rowVersion], INSERTED.[__version]
INTO @inserted0
WHERE [eventSeriesId] = @p3 AND [__rowVersion] = @p4;
SELECT [__lastModified], [__rowVersion], [__version] FROM @inserted0;

I notice the OUTPUT statement in there (note, I've also taken a look at issue #1441 but that is an issue with EF not generating an OUTPUT INTO).

The issue is in the documentation for the OUTPUT statement, specifically the section on the INSERTED keyword (emphasis mine):

INSERTED is a column prefix that specifies the value added by the insert or update operation. Columns prefixed with INSERTED reflect the value after the UPDATE, INSERT, or MERGE statement is completed but before triggers are executed.

I've configured the __lastModified and __version columns with ValueGeneratedOnAddOrUpdate so the expectation is that upon return of an UPDATE/INSERT, those values would reflect what is in the database when the operation is complete.

As it stands now, the values that come back do not reflect what is in the database and the DbContext now stores the incorrect values.

The DbContext (and by extension, my application) is now inconsistent, and this is a HUGE problem.

There's a simple workaround, which is to refetch the entity, and that's what I'm doing now. But given that I'm wasting network bytes bringing something back from the database on a call to SaveAsync, why not bring back the correct values?

Also disturbing is that the __rowVersion column isn't brought back correctly; I now have a rowversion which doesn't exist in the database so if I wanted to try to modify the entity, I can't, because the rowversion for consistency checking will never match.

It seems the simple workaround is to start a transaction and just select out the values, like so:

BEGIN TRANSACTION;
UPDATE [stats].[EventSeries] SET [description] = @p0, [twitterScreenName] = @p1, [__lastModifiedByUserId] = @p2
WHERE [eventSeriesId] = @p3 AND [__rowVersion] = @p4;
SELECT [__lastModified], [__rowVersion], [__version] FROM [stats].[EventSeries] where [eventSeriesId] = @p3;
COMMIT TRANSACTION;

And only select out the values that have been indicated as being refreshed in the database.

@divega divega added this to the 1.1.0 milestone Sep 7, 2016
@divega divega added the type-bug label Sep 7, 2016
AndriySvyryd added a commit that referenced this issue Oct 4, 2016
…ow to SQL Server

For inserts inner join the INSERTED table instead of reading values directly from it.
For updates use the relationat implementation that doesn't use INSERTED table.

Fixes #6474
AndriySvyryd added a commit that referenced this issue Oct 4, 2016
…ow to SQL Server

For inserts inner join the INSERTED table instead of reading values directly from it.
For updates use the relationat implementation that doesn't use INSERTED table.

Fixes #6474
AndriySvyryd added a commit that referenced this issue Oct 4, 2016
…ow to SQL Server

For inserts inner join the INSERTED table instead of reading values directly from it.
For updates use the relationat implementation that doesn't use INSERTED table.

Fixes #6474
AndriySvyryd added a commit that referenced this issue Oct 4, 2016
…ow and computed columns using querying functions to SQL Server

For inserts inner join the INSERTED table instead of reading values directly from it.
For updates use the relational implementation that doesn't use INSERTED table.

Fixes #6044
Fixes #6474
AndriySvyryd added a commit that referenced this issue Oct 6, 2016
…ow and computed columns using querying functions to SQL Server

For inserts inner join the INSERTED table instead of reading values directly from it.
For updates use the relational implementation that doesn't use INSERTED table.

Fixes #6044
Fixes #6474
@AndriySvyryd AndriySvyryd added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Oct 6, 2016
@AndriySvyryd AndriySvyryd removed their assignment Oct 6, 2016
@casperOne
Copy link
Author

casperOne commented Oct 22, 2016

@AndriySvyryd @divega Would you make sure that the case where multiple new child entities are generated using a MERGE statement is covered?

For example, if I have a hierarchy of tables:

Event -> Tournament -> Phase

And I add 1 Event, 1 Tournament, 2 Phases at the same time, the following SQL is generated:

SET NOCOUNT ON;

INSERT INTO [stats].[Event] ([description], [end], [eventSeriesId], [locationId], [start], [_challongeId], [_eslId], [_evo2012Id], [_evo2013Id], [_evo2014Id], [_evo2015Id], [_evo2016Id], [_maxoplataId], [_smashggId], [__lastModifiedByUserId])
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14);
SELECT [eventId], [__lastModified], [__rowVersion], [__version]
FROM [stats].[Event]

SET NOCOUNT ON;
INSERT INTO [stats].[Tournament] ([ban], [description], [eventId], [exhibition], [gameId], [Invitational], [notes], [ratio], [_challongeId], [_eslId], [_evo2012Id], [_evo2013Id], [_evo2014Id], [_evo2015Id], [_evo2016Id], [_maxoplataId], [_smashggId], [__lastModifiedByUserId])
VALUES (@p15, @p16, @p17, @p18, @p19, @p20, @p21, @p22, @p23, @p24, @p25, @p26, @p27, @p28, @p29, @p30, @p31, @p32);
SELECT [tournamentId], [__lastModified], [__rowVersion], [__version]
FROM [stats].[Tournament]
WHERE @@ROWCOUNT = 1 AND [tournamentId] = scope_identity();

SET NOCOUNT ON;
DECLARE @toInsert1 TABLE ([bracketTypeId] int, [description] nvarchar(100), [losersAdvance] int, [ordinal] int, [tournamentId] int, [winnersAdvance] int, [_challongeId] int, [_eslId] int, [_evo2012Id] int, [_evo2013Id] int, [_evo2014Id] int, [_evo2015Id] int, [_evo2016Id] int, [_maxoplataId] int, [_smashggId] int, [__lastModifiedByUserId] int, [_Position] [int]);
INSERT INTO @toInsert1
VALUES (@p33, @p34, @p35, @p36, @p37, @p38, @p39, @p40, @p41, @p42, @p43, @p44, @p45, @p46, @p47, @p48, 0),
(@p49, @p50, @p51, @p52, @p53, @p54, @p55, @p56, @p57, @p58, @p59, @p60, @p61, @p62, @p63, @p64, 1);

DECLARE @inserted1 TABLE ([phaseId] int, [bracketTypeId] int, [description] nvarchar(100), [losersAdvance] int, [ordinal] int, [tournamentId] int, [winnersAdvance] int, [_challongeId] int, [_eslId] int, [_evo2012Id] int, [_evo2013Id] int, [_evo2014Id] int, [_evo2015Id] int, [_evo2016Id] int, [_maxoplataId] int, [_smashggId] int, [__lastModified] datetimeoffset, [__lastModifiedByUserId] int, [__rowVersion] varbinary(8), [__version] int, [_Position] [int]);
MERGE [stats].[Phase] USING @toInsert1 AS i ON 1=0
WHEN NOT MATCHED THEN
INSERT ([bracketTypeId], [description], [losersAdvance], [ordinal], [tournamentId], [winnersAdvance], [_challongeId], [_eslId], [_evo2012Id], [_evo2013Id], [_evo2014Id], [_evo2015Id], [_evo2016Id], [_maxoplataId], [_smashggId], [__lastModifiedByUserId])
VALUES (i.[bracketTypeId], i.[description], i.[losersAdvance], i.[ordinal], i.[tournamentId], i.[winnersAdvance], i.[_challongeId], i.[_eslId], i.[_evo2012Id], i.[_evo2013Id], i.[_evo2014Id], i.[_evo2015Id], i.[_evo2016Id], i.[_maxoplataId], i.[_smashggId], i.[__lastModifiedByUserId])
OUTPUT INSERTED.[phaseId], INSERTED.[bracketTypeId], INSERTED.[description], INSERTED.[losersAdvance], INSERTED.[ordinal], INSERTED.[tournamentId], INSERTED.[winnersAdvance], INSERTED.[_challongeId], INSERTED.[_eslId], INSERTED.[_evo2012Id], INSERTED.[_evo2013Id], INSERTED.[_evo2014Id], INSERTED.[_evo2015Id], INSERTED.[_evo2016Id], INSERTED.[_maxoplataId], INSERTED.[_smashggId], INSERTED.[__lastModified], INSERTED.[__lastModifiedByUserId], INSERTED.[__rowVersion], INSERTED.[__version], i._Position
INTO @inserted1;

SELECT [phaseId], [__lastModified], [__rowVersion], [__version] FROM @inserted1
ORDER BY _Position;

Note the insertion for the two Phase instances at the end; a MERGE is used and exhibits the same behavior as above.

When there's a single Phase child instance, then an insert is used (not sure why it isn't used when there are two child inserts, since that case can be detected by EF before the call is executed) and works as expected using the following SQL:

SET NOCOUNT ON;
INSERT INTO [stats].[Event] ([description], [end], [eventSeriesId], [locationId], [start], [_challongeId], [_eslId], [_evo2012Id], [_evo2013Id], [_evo2014Id], [_evo2015Id], [_evo2016Id], [_maxoplataId], [_smashggId], [__lastModifiedByUserId])
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14);
SELECT [eventId], [__lastModified], [__rowVersion], [__version]
FROM [stats].[Event]
WHERE @@ROWCOUNT = 1 AND [eventId] = scope_identity();
Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommandBuilderFactory:Information: Executed DbCommand (43ms) [Parameters=[@p15='?', @p16='?' (Size = 500), @p17='?', @p18='?', @p19='?', @p20='?', @p21='?' (Size = 500), @p22='?', @p23='?', @p24='?', @p25='?', @p26='?', @p27='?', @p28='?', @p29='?', @p30='?', @p31='?', @p32='?'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [stats].[Tournament] ([ban], [description], [eventId], [exhibition], [gameId], [Invitational], [notes], [ratio], [_challongeId], [_eslId], [_evo2012Id], [_evo2013Id], [_evo2014Id], [_evo2015Id], [_evo2016Id], [_maxoplataId], [_smashggId], [__lastModifiedByUserId])
VALUES (@p15, @p16, @p17, @p18, @p19, @p20, @p21, @p22, @p23, @p24, @p25, @p26, @p27, @p28, @p29, @p30, @p31, @p32);
SELECT [tournamentId], [__lastModified], [__rowVersion], [__version]
FROM [stats].[Tournament]
WHERE @@ROWCOUNT = 1 AND [tournamentId] = scope_identity();
Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommandBuilderFactory:Information: Executed DbCommand (56ms) [Parameters=[@p33='?', @p34='?' (Size = 100), @p35='?', @p36='?', @p37='?', @p38='?', @p39='?', @p40='?', @p41='?', @p42='?', @p43='?', @p44='?', @p45='?', @p46='?', @p47='?', @p48='?'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [stats].[Phase] ([bracketTypeId], [description], [losersAdvance], [ordinal], [tournamentId], [winnersAdvance], [_challongeId], [_eslId], [_evo2012Id], [_evo2013Id], [_evo2014Id], [_evo2015Id], [_evo2016Id], [_maxoplataId], [_smashggId], [__lastModifiedByUserId])
VALUES (@p33, @p34, @p35, @p36, @p37, @p38, @p39, @p40, @p41, @p42, @p43, @p44, @p45, @p46, @p47, @p48);
SELECT [phaseId], [__lastModified], [__rowVersion], [__version]
FROM [stats].[Phase]
WHERE @@ROWCOUNT = 1 AND [phaseId] = scope_identity();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. type-bug
Projects
None yet
Development

No branches or pull requests

4 participants