Open jsacapdev opened 6 years ago
@jsacapdev EF Core currently expects the rows affected returned to match what is expected to happen in the database based on the model and conceptual operation being performed. In cases like this where a trigger is being used and the conceptual operation is being changed it would be useful to be able to opt out of this checking. For Adds specifically, see also #7038
@jsacapdev does it work if you follow the recommendation to add SET NOCOUNT ON
in the trigger?
e.g.
CREATE TRIGGER RemoveTestPosts
ON [dbo].[Posts]
FOR INSERT
AS
SET NOCOUNT ON;
DELETE FROM [dbo].[Posts] WHERE Content = 'test1';
GO
@Suchiman setting nocount on does not work
@ajcvickers would you be open to accepting a PR to get this fixed? I ask as you may need to consider the impact of the change required. If you are open to accepting a PR, we can start to discuss your thoughts on a design moving forward?
@jsacapdev Yes, we would certainly consider a PR for this. I'll get back to you on a potential approach.
We discussed this and came to the following initial conclusions:
As of now, we haven't settled on the API questions, so marking this issue as needs-design.
@jsacapdev If you only need this for inserts, then I would suggest tackling #7038 since this doesn't require API changes. If you need this for updates and deletes as well, then we're going to have to do more design work, and this may take some time. Also, we are surprised that using SET NOCOUNT ON does not work--can you provide any more details on why this doesn't work?
@ajcvickers What I have found is that with both triggers (with and without NOCOUNT) I dont get a PostId returned when either query is generated and executed on the database. So the logic in ConsumeResultSetWithPropagation() that checks for a populated data reader fails and it throws back with the same behaviour.
INSERT INTO [Posts] ([BlogId], [Content], [Title])
VALUES (1, 'test1', 'test1');
SELECT [PostId]
FROM [Posts]
WHERE @@ROWCOUNT = 1 AND [PostId] = scope_identity();
@jsacapdev Can you explain more about what the scenarios are for using the triggers? (If, for example, the result of doing the insert is that the record doesn't exist after the insert, then this is going to be very hard for EF to reason about. However, it seems likely that the real reason for using the trigger is something different, so we'd like to understand more.
@ajcvickers if i am honest, its not a very general use case, and its just as simple as understanding why once a triggered has been fired do i get an exception throw back. i appreciate your time looking into this and the information you have provided. and i have learnt a little bit more about the product.
@ajcvickers I am having the same issue with my "UPSERT" trigger. Weirdly enough, my error
An unhandled exception occurred while processing the request.
DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities
Odd note, inserting test data from VS code returns 1 row affected, with the trigger catching and updating it.
Trigger code:
IF EXISTS (SELECT * FROM sysobjects WHERE TYPE = 'TR'
AND NAME = 'VISITOR_IF_EXISTS_UPDATE_TIME')
DROP TRIGGER VISITOR_IF_EXISTS_UPDATE_TIME
GO
CREATE TRIGGER VISITOR_IF_EXISTS_UPDATE_TIME
ON Visitor
INSTEAD OF INSERT
AS
BEGIN
IF NOT EXISTS (SELECT V.IP_ADDRESS from Visitor V, INSERTED I
WHERE V.IP_ADDRESS = I.IP_ADDRESS)
INSERT INTO VISITOR (IP_ADDRESS, COUNTRY_NAME, REGION_NAME, CITY, LATITUDE, LONGITUDE)
SELECT IP_ADDRESS, COUNTRY_NAME, REGION_NAME, CITY, LATITUDE, LONGITUDE
FROM INSERTED
ELSE
UPDATE Visitor
SET Visitor.UPDATE_DATE = (SYSDATETIME()),
Visitor.UPDATE_COUNT = V.UPDATE_COUNT + 1
FROM Visitor V, INSERTED I
WHERE V.IP_ADDRESS = I.IP_ADDRESS
END
@AndriySvyryd @divega Should we document what the update pipeline expects back for the various cases? For example, if EF is doing an insert in batch mode and there are store-generated values coming back.
The issue I'm having with INSTEAD OF INSERT triggers is that Entity Framework is batching the insert statement with a select statement using scope_identity()
... since the INSTEAD OF trigger has its own scope, the select statement fails.
Perhaps an option to have inserts use @@IDENTITY
instead of scope_identity()
would allow users to use INSTEAD OF triggers without resorting to returning result sets, which is deprecated in SQL Server per https://docs.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?view=sql-server-2017#returning-results. That option could be added to the SqlServerDbContextOptionsBuilder
class.
The issue I'm having with INSTEAD OF INSERT triggers is that Entity Framework is batching the insert statement with a select statement using
scope_identity()
... since the INSTEAD OF trigger has its own scope, the select statement fails.Perhaps an option to have inserts use
@@IDENTITY
instead ofscope_identity()
would allow users to use INSTEAD OF triggers without resorting to returning result sets, which is deprecated in SQL Server per https://docs.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?view=sql-server-2017#returning-results. That option could be added to theSqlServerDbContextOptionsBuilder
class.
I have INSTEAD OF INSERT trigger too - just collecting some third-party ad-hoc data and in the instead-of-trigger having logic avoiding duplications via unique key doing actually a data merge - insert or update.
In fact there is no transactional concurrency - third-party data are collected just-in-time - storing only needed ones, not PRE-IMPORTING all the third-party entities before running an application.
I thought using ALTERNATE (composed) KEY would be the way, but not: there is still
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
@@ROWCOUNT is 1, but scope_identity is NULL.
May be using alternate key in the way:
WHERE @@ROWCOUNT = 1 AND ([Id] = scope_identity() OR (ALTERNATE_KEY_COLUMN1 = xyz AND ALTERNATE_KEY_COLUMN2 = 123456789))
could be easy to implement?
Any update about this issue? I"m currently getting the same exception when using EF Core 5.0.11 with SQL triggers.
We have an INSTEAD OF INSERT trigger into our database, which changes the fields for the insert query but because EF includes the SCOPE_IDENTITY()
and the trigger has another scope this leads to the exception.
@xwellingtonx This issue is in the Backlog milestone. This means that it is not planned for the next release (EF Core 7.0). We will re-assess the backlog following the this release and consider this item at that time. However, keep in mind that there are many other high priority features with which it will be competing for resources. Make sure to vote (👍) for this issue if it is important to you.
@ajcvickers Thank you for the fast reply, I've voted now.
Note discussion specifically around deletes, where concurrency checks rarely make sense (even if there are scenarios where they do): #27562
Tentatively bringing this into 7.0 but as low-priority, since it's required for #27532.
Note an important perf aspect here: optimistic concurrency checks are a blocker for #27532; we can't insert the commit into the batch, since we need to throw and roll back if there's a concurrency exception, but we only know that on the client after the transaction gets committed. So we must do an additional roundtrip, which has a cost.
See Twitter poll specifically about the deletion question (also discussed in #27562).
Some reason why concurrency checks around deletion can be useful:
We've had various discussions around this recently, here's a summary.
Thanks @divega for valuable conversation (here)!
Another good reason to opt out of EF's optimistic concurrency is if you're using repeatable read transactions (or serializable/snapshot).
Repeatable read transactions are somewhat similar to optimistic concurrency, but managed by the database instead of by EF: the unit-of-work only succeeds if there was no conflict, otherwise an error is raised and the application has to retry. Serializable/snapshot transactions have overhead costs which vary across databases (e.g. may introduce additional locking), and also provide more than just checking that rows haven't changed (e.g. they guarantee a stable view of the database throughout the transaction).
edited - wrong issue.
@DaleMckeown the newly-released EF Core 7.0 no longer generates INSERT+SELECt, but rather INSERT with an OUTPUT clause; can you give that a try?
@roji Ah, that would probably help. We're going to be on EF core 6 for a while, unfortunately. Only just got our 30 apps from 3.1 to 6.0 a month ago.
OK - I'd be interesting in knowing whether the new OUTPUT-based behavior fixes your problem, in case you have time to give that a test.
By the way, note that the SELECT you posted above isn't a result of optimistic concurrency/rows affected (which is what this issue is about) - it's about bringing back the database-generated ID, so that it can be populated into the .NET entity instance. Opting out of that would be #9118 rather than this issue.
In any case, we still think it's a good idea to provide a opt-outs for both things.
@roji I'll see what I can do :)
Thanks for the clarification on the issue, I will amend my post and move it to the relevant issue.
indeed, being able to opt-out of these things would be beneficial.
Thanks for the assistance.
Another use case that is affected by this DbUpdateConcurrencyException is updating a disconnected entity that does not exist. i.e. Update method receives a disconnected entity with a primary key and attaches it to the context. The primary key shouldnt have been modified but if it was (very possible in disconnected scenario (api/web)), the call to SaveChanges will result in 0 rows updated and it will through the DbUpdateConcurrencyException.
Opting out of this behavior and reading the rows updated would be a much better solution than catching every DbUpdateConcurrencyException.
A trigger is created on the database to delete an row if it meets some condition.
An entity is added to the table that matches that condition.
The call to SaveChanges() throws.
Is this by design? Is there a workaround? (other than using a third party library)
Steps to reproduce
Ran the program to create a Blog.
Ran the program to create a post.
Manually created the trigger.
Ran the program again to create a post.
Further technical details
EF Core version: Microsoft.AspNetCore.All 2.0.3 Microsoft.EntityFrameworkCore 2.0.1
Database Provider: Microsoft.EntityFrameworkCore.SqlServer SQL Server is running in docker
Operating system: Visual Studio Code
Dotnet: PS C:\playground\AlrightTrigger\AlrightTriggerConsole> dotnet --info .NET Command Line Tools (2.0.3)
Product Information: Version: 2.0.3 Commit SHA-1 hash: 12f0c7efcc
Runtime Environment: OS Name: Windows OS Version: 10.0.14393 OS Platform: Windows RID: win10-x64 Base Path: C:\Program Files\dotnet\sdk\2.0.3\
Microsoft .NET Core Shared Framework Host
Version : 2.0.3 Build : a9190d4a75f4a982ae4b4fa8d1a24526566c69df