On Insert Trigger breaks EF Add - c#

I added a SQL Server table ON INSERT trigger and it broke my API's Entity Framework insert code. It throws an error:
InnerException = "Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded.
I tried setting EntityState.Added; suggested by Ben here, but this didn't work.
I tried the suggestion from chamara here.
Added the following SQL Server trigger:
ALTER TRIGGER [dbo].[tr_TargetUserDel]
ON [dbo].[TargetUser]
AFTER INSERT
AS
BEGIN
DECLARE #UserId AS INT
DECLARE #TargetId AS INT
DECLARE #TargetUserId AS INT
SELECT #UserId = i.[UserId], #TargetId = i.[TargetId]
FROM inserted i
INSERT INTO [dbo].[TargetUser] ([UserId], [TargetId])
VALUES (#UserId, #TargetId)
SELECT #TargetUserId = CAST(##Identity AS INTEGER)
DELETE [dbo].[TargetUser]
WHERE TargetUserId <> #TargetUserId
AND TargetId = #TargetId
END
This caused the following EF insert to fail:
targetuser.UserId = interfaceUser.UserId;
targetuser.TargetId = interfaceUserDTO.TargetId;
db.InterfaceTargetUsers.Add(targetuser);
db.SaveChanges(); // <<Error thrown
The model object targetuser:
[Table("TargetUser")]
public partial class InterfaceTargetUser
{
[Key]
[Column(Order = 0)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int TargetUserId { get; set; }
public int UserId { get; set; }
public int TargetId { get; set; }
}
EF is having trouble with the insert, but if I run the insert directly in SQL Server, it runs fine:
INSERT INTO targetuser (TargetId, UserId)
VALUES (5276, 572)
Modifying the trigger as suggested by chamara here works, but am looking for a EF solution.
chamara's workaround is to add this to the end of the trigger:
SELECT * FROM deleted UNION ALL
SELECT * FROM inserted;
This works but it has no up votes. Why does this work? Seems like there should be an EF solution?

You have many problems there:
select #UserId = i.[UserId], #TargetId=i.[TargetId] from inserted i
Will works only when the pseudo INSERTED hase only 1 rows.
Also for the line
SELECT #TargetUserId =CAST(##Identity AS INTEGER)
Will only return the last generated identity value, not all of them.
Alter your trigger to be like
ALTER TRIGGER [dbo].[tr_TargetUserDel] ON [dbo].[TargetUser]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO [dbo].[TargetUser]([UserId], [TargetId])
SELECT UserId,
TargetId
FROM INSERTED;
DELETE T
FROM [dbo].[TargetUser] T INNER JOIN INSERTED TT
ON T.TargetId = TT.TargetId
AND
T.TargetUserId <> TT.TargetUserId;
SET NOCOUNT OFF;
END

Related

Instead Of Insert trigger doesn't work for my insert via C# code

I have the following trigger:
CREATE TRIGGER insert_or_update_AccountNews
ON AccountNews
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE
#AccountNumber bigint,
#NewsId int,
#TariffPlan nvarchar(1024)
SELECT #AccountNumber = INSERTED.AccountNumber,
#NewsId = INSERTED.NewsId,
#TariffPlan = INSERTED.TariffPlan
FROM INSERTED
IF EXISTS (SELECT NewsId FROM [AccountNews]
WHERE AccountNumber = #AccountNumber AND NewsId = #NewsId)
UPDATE [AccountNews]
SET TariffPlan = #TariffPlan
WHERE AccountNumber = #AccountNumber
AND NewsId = #NewsId
ELSE
INSERT INTO [AccountNews] (NewsId, AccountNumber, TariffPlan)
SELECT #NewsId, #AccountNumber, #TariffPlan
END;
And I have a table, as you can see, that is called AccountNews. It has the following columns:
AccountNumber, NewsId, TariffPlan
The idea is when I insert something into the table the trigger will determine if the data exists (I have unique constraint by AccountNumber and NewsId) or not exists. If the data not exists - the insert, otherwise - update.
And it works perfectly via the SQL console, like:
insert into AccountNews (NewsId, AccountId, TariffPlan)
values (12345, 777777, 'Hello world');
insert into AccountNews (NewsId, AccountId, TariffPlan)
values (12345, 777777, 'Hello world 2');
Next, I have this C# code to insert data:
DataTable table = await ReadAsStringAsync(file, newsId);
var connectionString = config.GetConnectionString("MyDbConnection");
using (SqlConnection connection = new SqlConnection(connectionString))
using (SqlBulkCopy bcp = new SqlBulkCopy(connection))
{
connection.Open();
bcp.BatchSize = 1000;
bcp.DestinationTableName = "[dbo].[AccountNews]";
bcp.ColumnMappings.Add("NewsId", "NewsId");
bcp.ColumnMappings.Add("AccountNumber", "AccountNumber");
bcp.ColumnMappings.Add("TariffPlan", "TariffPlan");
bcp.ColumnMappings.Add("Date", "Date");
await bcp.WriteToServerAsync(table);
}
In this case I don't see a result of my trigger. When I load some data that is already in my database I have a unique constraint exception.
To create an UPSERT-only table you can add a trigger like this:
use tempdb
go
drop table if exists AccountNews
create table AccountNews
(
AccountNumber bigint,
NewsId int,
TariffPlan nvarchar(1024),
constraint pk_AccountNews
primary key (AccountNumber, NewsId)
)
go
CREATE TRIGGER insert_or_update_AccountNews
ON AccountNews
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
merge AccountNews as target
using (select * from inserted) as source
on (target.AccountNumber = source.AccountNumber and target.NewsId = source.NewsId)
when matched then
update set TariffPlan = source.TariffPlan
when not matched then
insert (AccountNumber, NewsId, TariffPlan)
values (source.AccountNumber, source.NewsId, source.TariffPlan);
END;
or wihout MERGE (which doen't permit duplicates within a single batch):
CREATE TRIGGER insert_or_update_AccountNews
ON AccountNews
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
with q as
(
select a.*, i.TariffPlan NewTariffPlan
from AccountNews a
join inserted i
on a.AccountNumber = i.AccountNumber
and a.NewsId = i.NewsId
)
update q set TariffPlan = NewTariffPlan;
insert into AccountNews(AccountNumber,NewsId,TariffPlan)
select AccountNumber,NewsId,TariffPlan
from inserted i
where not exists
(
select *
from AccountNews a
where a.AccountNumber = i.AccountNumber
and a.NewsId = i.NewsId
);
END;
go
And you opt-in for triggers and constraint checking with SqlBulkCopyOptions, which you should normally do when bulk loading from an application because bypassing constraints or triggers requires ALTER TABLE privileges on the table.

The data reader is incompatible. A member of the type, 'RoleId', does not have a corresponding column in the data reader with the same name

Stored procedure works and deletes what I want but I still get this error after deleting:
The data reader is incompatible with the specified 'AMSIdentity.Models.RemoveRoleFromUserViewModel'. A member of the type, 'RoleId', does not have a corresponding column in the data reader with the same name.
I need to run the code without this error in the above
This code using ASP.NET MVC 5 and EF6 code first approach; I tried to use this code but always throws this error after delete.
This is the action method that I use
public ActionResult RemoveRoleFromUserConfirmed(string UserName, string RoleId)
{
if (UserName == null && RoleId == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
SqlParameter param1 = new SqlParameter("#RoleId", RoleId);
SqlParameter param2= new SqlParameter("#UserName", UserName);
var remove = Identitydb.Database.SqlQuery<RemoveRoleFromUserViewModel>("admin.sp_RemoveUserFromRole #RoleId, #UserName",
((ICloneable)param1).Clone(),
((ICloneable)param2).Clone()).ToArray().ToList().FirstOrDefault();
if (remove == null)
{
return HttpNotFound();
}
return RedirectToAction("Roles");
}
This is the view model that I use :
public class RemoveRoleFromUserViewModel
{
[Key]
[DisplayName("Role Id")]
public string RoleId { get; set; }
[DisplayName("Username")]
public string UserName { get; set; }
}
This is the stored procedure code:
ALTER PROCEDURE [Admin].[sp_RemoveUserFromRole]
#RoleId NVARCHAR(50),
#UserName NVARCHAR(50)
AS
BEGIN
DELETE FROM AspNetUserRoles
WHERE UserId = (SELECT Id
FROM AspNetUsers
WHERE UserName = #UserName)
AND RoleId = #RoleId
END
I expect that this code will delete role from the specific user.
When you perform a DELETE in the stored procedure, you need to "audit" what got deleted. Then perform a SELECT on that audit-table.
You are taking advantage of the OUTPUT feature of sql server.
see:
https://learn.microsoft.com/en-us/sql/t-sql/statements/delete-transact-sql?view=sql-server-2017
and/or
https://www.sqlservercentral.com/articles/the-output-clause-for-insert-and-delete-statements
Below is a generic example of the TSQL you need.
DROP TABLE IF EXISTS [dbo].[Patient]
GO
CREATE TABLE [dbo].[Patient]
(
[PatientKey] BIGINT NOT NULL IDENTITY(1, 1),
[PatientUniqueIdentifier] VARCHAR(256) NOT NULL,
[CreateDate] DATETIMEOFFSET NOT NULL,
CONSTRAINT [UC_Patient_PatientUniqueIdentifier] UNIQUE (PatientUniqueIdentifier)
)
/* now insert 3 sets of rows, with different create-dates */
INSERT INTO dbo.Patient (PatientUniqueIdentifier, [CreateDate]) SELECT TOP 10 NEWID() , '01/01/2001' from sys.objects
INSERT INTO dbo.Patient (PatientUniqueIdentifier, [CreateDate]) SELECT TOP 10 NEWID() , '02/02/2002' from sys.objects
INSERT INTO dbo.Patient (PatientUniqueIdentifier, [CreateDate]) SELECT TOP 10 NEWID() , '03/03/2003' from sys.objects
SELECT 'SeedDataResult' as Lable1, * FROM dbo.Patient
/* everything above is just setting up the example */
/* below would be the "guts"/implementation of your stored procedure */
DECLARE #PatientAffectedRowsCountUsingAtAtRowCount BIGINT
DECLARE #PatientAffectedRowsCountUsingCountOfOutputTable BIGINT
DECLARE #PatientCrudActivityAuditTable TABLE ( [PatientUniqueIdentifier] VARCHAR(256), DatabaseKey BIGINT , MyCrudLabelForKicks VARCHAR(16));
/* now delete a subset of all the patient rows , your delete will be whatever logic you implement */
DELETE FROM [dbo].[Patient]
OUTPUT deleted.PatientUniqueIdentifier , deleted.PatientKey , 'mydeletelabel'
INTO #PatientCrudActivityAuditTable ([PatientUniqueIdentifier] ,DatabaseKey , MyCrudLabelForKicks )
WHERE
CreateDate = '02/02/2002'
/*you don't need this update statement, but i'm showing the audit table can be used with delete and update and insert (update here) */
/*
UPDATE [dbo].[Patient]
SET CreateDate = '03/03/2003'
OUTPUT inserted.PatientUniqueIdentifier , inserted.PatientKey, 'myupdatelabel'
INTO #PatientCrudActivityAuditTable ([PatientUniqueIdentifier] ,DatabaseKey , MyCrudLabelForKicks)
FROM [dbo].[Patient] realTable
WHERE CreateDate != '03/03/2003'
*/
/* optionally, capture how many rows were deleted using ##ROWCOUNT */
SELECT #PatientAffectedRowsCountUsingAtAtRowCount = ##ROWCOUNT
/* or, capture how many rows were deleted using a simple count on the audit-table */
SELECT #PatientAffectedRowsCountUsingCountOfOutputTable = COUNT(*) FROM #PatientCrudActivityAuditTable
SELECT 'ResultSetOneForKicks' as Label1, 'Rows that I Deleted' as MyLabel_YouCanRemoveThisColumn, DatabaseKey , PatientUniqueIdentifier FROM #PatientCrudActivityAuditTable
/* if so inclined, you can also send back the delete-COUNTS to the caller. You'll have to code your IDataReader (ORM, whatever) to handle the multiple return result-sets */
/* most people will put the "counts" as the first result-set, and the rows themselves as the second result-set ... i have them in the opposite in this example */
SELECT 'ResultSetTwoForKicks' as Label1, #PatientAffectedRowsCountUsingAtAtRowCount as '#PatientAffectedRowsCountUsingAtAtRowCountCoolAliasName' , #PatientAffectedRowsCountUsingAtAtRowCount as '#PatientAffectedRowsCountUsingAtAtRowCountCoolAliasName'
In my example, you would write the dotNet serialize code...(whatever flavor you use, raw IDataReader, ORM tool, whatever) against the PatientKey and PatientUniqueIdentifier columns coming back from the #PatientSurrogateKeyAudit table.
Hi All,
I got the answer from #Jeroen Mostert, The solution is to use the
(ExecuteSqlCommand) rather than (SqlQuery) because I will never return
data, I only execute the stored procedure with two parameters.
This is the answer
SqlParameter param1 = new SqlParameter("#RoleId", RoleId);
SqlParameter param2= new SqlParameter("#UserName", UserName);
//I change this line from SqlQuery to ExecuteSqlCommand
var remove = Identitydb.Database.ExecuteSqlCommand("admin.sp_RemoveUserFromRole #RoleId, #UserName", param1, param2);
Thank you very much #Jeroen Mostert.
Regards,
Ali Mosaad
Software Developer

Using LINQ to insert data into a table that uses a sequence as primary key generator

I have a table which generates its primary key from a sequence (that just counts up from 0):
CREATE TABLE [dbo].[testTable](
[id] [int] NOT NULL,
[a] [int] NOT NULL,
CONSTRAINT [PK_testTable] PRIMARY KEY CLUSTERED ([id] ASC))
ALTER TABLE [dbo].[tblTestTable] ADD CONSTRAINT [DF_tblTestTable_id] DEFAULT (NEXT VALUE FOR [seq_PK_tblTestTable]) FOR [id]
I've used Visual Studio's O/R Designer to create the mapping files for the table; the id field is defined as:
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_id", DbType="Int NOT NULL", IsPrimaryKey=true)]
public int id {…}
and now I'm trying to insert data via LINQ.
var testTableRecord = new testTable()
{
a = 1,
};
db.Connection.Open();
db.testTables.InsertOnSubmit(testTableRecord);
db.SubmitChanges();
Console.WriteLine($"id: {testTableRecord.id}");
The problem I'm encountering is, that LINQ seems unable to handle the id generation via sequence as it sets the id implicitly to 0 when inserting.
When I set the id to CanBeNull, the insert fails because it tries to insert NULL into a non-nullable field.
When I set the id to IsDbGenerated, the insert works but it expects an IDENTITY field and tries to load the generated id with SELECT CONVERT(Int,SCOPE_IDENTITY()) AS [value]',N'#p0 int',#p0=1 and than sets the id in the object to null because SCOPE_IDENTITY() returns null…
I've been thinking about just using IsDbGenerated, destroying the LINQ object and querying the DB for the id, but I don't have anything unique to search for.
Unfortunately changing the id creation mechanism to IDENTITY is not an option.
Do I have to explicitly query the DB for the next sequence value and set the id manually?
Whats the best way to handle these inserts via LINQ?
PS: I need the id because I have to insert more data that uses the id as FK.
Looking at solutions from the raw sql perspective:
1.
INSERT INTO [dbo].[testTable] VALUES (NEXT VALUE FOR [dbo].[seq_PK_tblTestTable], 1)
Simply can't be done in LINQ to SQL as far as I can tell
2.
INSERT INTO [dbo].[testTable] (a) VALUES (1)
This can be achieved in LINQ to SQL by excluding the id property from the testTable entity.
If you need to retrieve ids from the table, you could create separate entities for inserting and querying:
public class testTableInsert {
[ColumnAttribute(...)]
public int a
}
public class testTableResult {
[ColumnAttribute(...)]
public int id
[ColumnAttribute(...)]
public int a
}
3.
DECLARE #nextId INT;
SELECT #nextId = NEXT VALUE FOR [dbo].[seq_PK_tblTestTable];
INSERT INTO [dbo].[testTable] VALUES (#nextId, 1)
As you mentioned, this can be essentially achieved by manually requesting the next id before each insert. If you go this route there are multiple ways to achieve it in your code, you can consider stored procedures or use the LINQ data context to manually execute the sql to retrieve the next sequence value.
Here's a code sample demonstrating how to extend the generated DataContext using partial methods.
public partial class MyDataContext : System.Data.Linq.DataContext
{
partial void InsertTestTable(TestTable instance)
{
using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = "SELECT NEXT VALUE FOR [dbo].[seq_PK_TestTable] as NextId";
cmd.Transaction = Transaction;
int nextId = (int) cmd.ExecuteScalar();
instance.id = nextId;
ExecuteDynamicInsert(instance);
}
}
}
Once the above is implemented, you can safely insert entities like this, and they will generate the correct sequence id.
TestTable entity = new TestTable { a = 2 };
dataContext.TestTables.InsertOnSubmit(entity);
dataContext.SubmitChanges();
Your only hope is a pretty profound refactoring and use a stored procedure to insert records. The stored procedure can be mapped to the class's Insert method in the data context designer.
Using your table definition, the stored is nothing but this:
CREATE PROCEDURE InsertTestTable
(
#id int OUTPUT,
#a AS int
)
AS
BEGIN
INSERT dbo.testTable (a) VALUES (#a);
SET #id = (SELECT CONVERT(int, current_value)
FROM sys.sequences WHERE name = 'seq_PK_tblTestTable')
END
You can import this stored procedure into the context by dragging it from the Sql Object Explorer onto the designer surface, which will then look like this:
The next step is to click the testTable class and click the ellipses button for the Insert method (which got enabled by adding the stored procedure to the context):
And customize it as follows:
That's all. Now LINQ-to-SQL will generate a stored procedure call to insert a record, for example:
declare #p3 int
set #p3=8
declare #p5 int
set #p5=0
exec sp_executesql N'EXEC #RETURN_VALUE = [dbo].[InsertTestTable] #id = #p0 OUTPUT,
#a = #p1',N'#p0 int output,#p1 int,#RETURN_VALUE int output',
#p0=#p3 output,#p1=123,#RETURN_VALUE=#p5 output
select #p3, #p5
Of course you may have to wonder how long you're going to hang on to LINQ-to-SQL. Entity Framework Core has sequence support out of the box. And much more.

Insert multiple records with values based on each other

Sorry for the bad title, I havent come up with a better one yet.
I am currently optimising a tool which basically does thousands of selects and inserts.
Assume the following relation
class A
{
public long ID; // This is an automatic key by the sqlserver
...Some other values
}
class B
{
public long RefID // Reference to A.ID;
... some other values...
}
class C
{
public long RefID // Reference to A.ID;
... some other values
}
What currently is happening is a SELECT to get ObjectA,
if it doesnt exist, create a new one. The Query returns the ID (OUTPUT INSERTED.ID)
Then it selects (inserts if not existant) the objects B and C.
Is there a way to compress this into a single SQL statement?
Im struggling at the part where the automatic generation of object A happens.
So it must do something like this:
IF NOT EXISTS(SELECT * FROM TableA WHERE someConditions)
INSERT... and get the ID
ELSE
REMEMBER THE ID?
IF NOT EXISTS(SELECT * FROM TableB WERE RefID = ourRememberedID)
INSERT...
IF NOT EXISTS(SELECT * FROM TableC WERE RefID = ourRememberedID)
INSERT...
Please note, stored procedures cannot be used.
A little help
You could issue these three in one statement
Use a DataReader NextResult
select ID from FROM TableA WHERE someConditions
select count(*) from TableB where refID = (select ID from FROM TableA WHERE someConditions)
select count(*) from TableC where refID = (select ID from FROM TableA WHERE someConditions)
But even then you take a risk the TableB or TableC had an insert before you got to it
If I could not use a stored procedure I think I would load the data into a #temp using a TVP
I think you could craft 4 statements in a transaction

EF6 auto created Stored Procedures for CRUD

Sorry long explanation but short question. For question you can directy look to 2 bold styled text except this.
I created my first EF6 CODE FIRST project to learn new features. I tried to implement new stored procedure based CRUD operations.
One of models entity for demonstration for here like this:
public partial class POCO
{
//identy
public int ID { get; set; }
// SQL Server Timestamp
// this.Property(t => t.RowVersion).IsFixedLength().HasMaxLength(8).IsRowVersion();
public byte[] RowVersion { get; set; }
public int MiscPropery { get; set; }
}
This entity uses SQL server's RowVersion as concurency token, which is "Database Generated Concurency" of course.
First: I created releted stored procedures on my own. I fallowed the EF6 documentation Stored Procedure Mapping
Then: EntityFramework 6 created database and stored procedures.
EntityFramework 6 created insert stored procedure:
CREATE PROCEDURE [dbo].[POCO_Update]
#ID [int],
#RowVersion_Original [rowversion],
#MiscPropery [int]
-- ------- I hope here: #RowsAffected int OUTPUT
AS
BEGIN
UPDATE [dbo].[POCOs]
SET [MiscPropery] = #MiscPropery
WHERE (([ID] = #ID) AND (([RowVersion] = #RowVersion_Original)
OR ([RowVersion] IS NULL AND #RowVersion_Original IS NULL)))
SELECT t0.[RowVersion], t0.[FaturaNo]
FROM [dbo].[POCOs] AS t0
WHERE ##ROWCOUNT > 0 AND t0.[ID] = #ID
END
EntityFramework 6 created delete stored procedure:
CREATE PROCEDURE [dbo].[POCO_Delete]
#ID [int],
#RowVersion_Original [rowversion]
-- ------- I hope here: #RowsAffected int OUTPUT
AS
BEGIN
DELETE [dbo].[POCOs]
WHERE (([ID] = #ID) AND (([RowVersion] = #RowVersion_Original)
OR ([RowVersion] IS NULL AND #RowVersion_Original IS NULL)))
END
My problems came at this point:
IN EntityFramework 6 documentation page, in "Concurrency Tokens" section says: (http://entityframework.codeplex.com/wikipage?title=Code%20First%20Insert%2fUpdate%2fDelete%20Stored%20Procedure%20Mapping)
Concurrency Tokens
Update and delete stored procedures may also need to deal with concurrency:
If the entity contains any concurrency tokens, the stored procedure should have an output parameter named RowsAffected that returns the number of rows updated/deleted.
And gives these samples on that documentation page:
// from documentation page
public class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
}
CREATE PROCEDURE [dbo].[Person_Update]
#PersonId int,
#Name nvarchar(max),
#Timestamp_Original rowversion,
#RowsAffected int OUTPUT -- ====> !!!!!
AS
BEGIN
UPDATE [dbo].[People]
SET [Name] = #Name
WHERE PersonId = #PersonId AND [Timestamp] = #Timestamp_Original
SET #RowsAffected = ##RowCount
END
But there is no output parameter named RowsAffected in stored procedures created by EF6.
Im a have to change autocreated stored procedures to follow documantation, or documentation is broken? Or everything goes under the curtains.

Categories

Resources