EF Core 5 code-first computed column using Function - sql-server

What is the recommended approach to use functions within computed columns in EF Core 5.0?
We are using SQL Server and Migrate() to create and update the database.
For other purposes we had a .sql script that runs after database creation to take heed of creating functions and stored procedures.
The last migration inserts a computed column. Obviously we need this functions before adding the column but after creating schema. Our current solution is to add the functions as direct call migrationBuider.Sql().
migrationBuilder.Sql(#"
CREATE FUNCTION [sid].[hasCreditor]
(
#contactId uniqueidentifier = null
)
RETURNS bit
AS
BEGIN
DECLARE #result bit
IF(#contactId IS NULL)
SET #result = 0
ELSE
BEGIN
DECLARE #creditorCount int
SELECT #creditorCount = COUNT(*) FROM sid.Creditor WHERE ContactId = #contactId
SET #result = IIF(#creditorCount > 0, 1, 0)
END
RETURN #result
END");
migrationBuilder.AddColumn<bool>(
name: "HasCreditor",
schema: "sid",
table: "Contact",
type: "bit",
nullable: true,
computedColumnSql: "sid.hasCreditor(Id)");
The solution is working but if sometimes we need to consolidate the migrations and create a new base migration this part will be lost and we need to insert it manually again.
Is this the best way or is there a better approach for inserting functions? Also taking into account that we use a special SQL schema "sid".

Related

Converting Postgresql Function to SQL Server function

Our company is in planning to move a Postgres database to SQL Server. This includes all tables, functions and stored procedures etc.
Here is the Postgresql syntax:
CREATE or REPLACE FUNCTION ex.on_update_integrity_reset()
RETURNS trigger
LANGUAGE plpgsql
AS
$$
BEGIN
new."integrity_error_id" := 0 ;
new."requires_processing" := True ;
new."datetime_amended" := now();
return new ;
END;
$$;
I have tried the following conversion BUT no luck I am afraid. I am hoping that the solution is quite straight forward. Any assistance will be gratefully received.
My T-SQL syntax, which isn't working:
CREATE FUNCTION ex.on_update_integrity_reset()
RETURNS TABLE
WITH SCHEMABINDING
AS
BEGIN atomic
new."integrity_error_id" := 0 ;
new."requires_processing" := True ;
new."datetime_amended" := GETDATE();
return new ;
END;
There are a huge amount of differences between Postgres' pgSQL and SQL Server's T-SQL, triggers not the least of them. You must learn the differences from the documentation, rather than dumping in code and expecting it to just "work".
The key things to note about triggers in SQL Server:
Triggers are directly defined on the table, rather than being separate functions which you can call.
Triggers are run per-statement not per-row, and the pseudo-tables inserted and deleted may contain multiple or zero rows.
SET NOCOUNT ON is ideal, due to problems with certain client drivers.
Do not return resultsets from triggers, instead make any updates, inserts or deletes you wish, taking into account the pseudo-tables.
Here is an example for your use case:
CREATE TRIGGER dbo.on_update_integrity_reset ON dbo.YourTable
AFTER INSERT -- what about updates???
AS
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM inserted)
RETURN; -- early bail-out
UPDATE t
SET
integrity_error_id = 0,
requires_processing = 1, -- boolean type is not supported
datetime_amended = GETDATE()
FROM dbo.YourTable t
JOIN inserted i ON i.SomePrimaryKey = t.SomePrimaryKey;
Be that as it may, you probably don't actually want a trigger. Instead, you probably need DEFAULT constraints. Although there are other methods for an auto-updating datetime column.
ALTER TABLE dbo.YourTable
ADD DEFAULT 0 FOR integrity_error_id;
ALTER TABLE dbo.YourTable
ADD DEFAULT 1 FOR requires_processing;
ALTER TABLE dbo.YourTable
ADD DEFAULT GETDATE() FOR datetime_amended;

How to Update or Insert a record using SQL Merge

I have a windows forms application that needs to edit an existing record if it already exists and create it if it does not. I'm using SQL Server 2008 R2. My application reads data from various tables which includes an ID field for the output table if a record already exists.
The ID field is blank if a new record is being created. The ID field is the primary key and an Identity (auto increment) field for the destination table.
I have created a stored procedure using MERGE that I hope will create a new record or update the existing one. The update part is working but I can't figure out what to do with the ID field when creating.
When doing an update I pass in an ID Parameter and the existing record is located. Obviously if it is a new record I won't have an ID yet but I can't then leave that Parameter out or I get an unassigned variable error as you would expect.
Here is my stored procedure. Am I just barking up the wrong tree here
somewhere?
Should I just create two stored procedures and call Update if I have and ID and Call Create if I don't have and ID?
Thanks for any assistance.
USE [Insurance]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[CreateModifyValuation]
-- Add the parameters for the stored procedure here
#ValuationID int,
#OwnersCorporationID int,
#ValDate datetime,
#ValuerID int,
#Amount money,
#Printed bit,
#Approved bit,
#Notes varchar(max),
#MultiplierValue money,
#MultiplierClass char(10),
#Adjustment money,
#SubmittedDate datetime
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
Merge Valuation as Target
USING (Select
#ValuationID,
#OwnersCorporationID,
#ValDate,
#ValuerID,
#Amount,
#Printed,
#Approved,
#Notes,
#MultiplierValue,
#MultiplierClass,
#Adjustment,
#SubmittedDate
)
As Source(
ValuationID,
OwnersCorporationID,
ValDate,
ValuerID,
Amount,
Printed,
Approved,
Notes,
MultiplierValue,
MultiplierClass,
Adjustment,
SubmittedDate
)
ON Source.ValuationID = Target.ValuationID
WHEN MATCHED THEN
UPDATE SET
Target.OwnersCorporationID = Source.OwnersCorporationID,
Target.ValDate = Source.ValDate,
Target.ValuerID = Source.ValuerID,
Target.Amount = Source.Amount,
Target.Printed = Source.Printed,
Target.Approved = Source.Approved,
Target.Notes = Source.Notes,
Target.MultiplierValue = Source.MultiplierValue,
Target.MultiplierClass = Source.MultiplierClass,
Target.Adjustment = Source.Adjustment,
Target.SubmittedDate = Source.SubmittedDate
WHEN NOT MATCHED BY Target THEN
INSERT (
OwnerscorporationID,
ValDate,
ValuerID,
Amount,
Printed,
Approved,
Notes,
MultiplierValue,
MultiplierClass,
Adjustment,
SubmittedDate
)
VALUES (
Source.OwnersCorporationID,
Source.ValDate,
Source.ValuerID,
Source.Amount,
Source.Printed,
Source.Approved,
Source.Notes,
Source.MultiplierValue,
Source.MultiplierClass,
Source.Adjustment,
Source.SubmittedDate
);
END
I feel like I cheated but it's only one line so how bad can it be :)
In My SQL I added this line before the "SET NOCOUNT ON;"
(Funny if I put it after SET NOCOUNT ON I get a syntax error)
if (#ValuationID = 0) set #ValuationID = null
Then in my C# code I set the ID to 0 for a new record and it seems to work after a couple of tests. There may be a better way to do this but like most things in life you stop looking once it works.
Thanks again to those who commented.
David

Migrating stored procedure from LINQ to SQL to Entity Framework 6

I have migrated the my project from Linq-to-SQL to Entity Framework 6. After resolving so many problems, I finally came to one I am not sure what to do with it. After conversion about one third of my unit tests are failing because stored procedures returning scalar values just work different way.
In Linq-to-SQL, the stored procedure return the returned value. In EF they return number of rows affected. There are workarounds which requires changing the stored procedure (generally from RETURN #retvalue to SELECT #retvalue). But this requires changes in T-SQL. The project still contains some legacy code from old days of ASP.NET like aspnet_Membership_CreateUser and so on. It means that I cannot change these stored procedures, because there are partly used by ASP.NET ADO.NET Membership provider, partly by the Linq-to-SQL code. The solution I consider is to make T-SQL wrappers of these parts legacy code.
Another reason why I would like to keep the stored procedure unchanged is the possibility of reverting to previous commit if the upgrade to EF is not successful. A don't want to change database forth and back again. But this reason is not so strong, it is only about my laziness.
I know that membership provider is old fashioned but I cannot rewrite entire application in one commit, I need to keep it stable.
The best way to finish the migration would be to call the stored procedure from EF like Linq-to-SQL, but I didn't found a way how to it. The EF disappointed me in this. How is possible that after years of development EF does not support returning scalar value from stored procedure?
How would you solved this issue?
Hi Qub1n,
// Create the gender parameter var param1 = new SqlParameter {
ParameterName = "#userId",
Value = _userId };
// Create the return code var returnValue = new SqlParameter {
ParameterName = "#ReturnCode",
SqlDbType = System.Data.SqlDbType.Int,
Direction = System.Data.ParameterDirection.Output };
// Gender is an int column in the database.
_context
.Database
.ExecuteSqlCommand("exec #ReturnCode = GetGenderOfUser #userId",
param1, returnValue);
return (int)returnValue.Value;
This works for me. My stored proc is like this:
CREATE PROCEDURE [dbo].[GetGenderOfUser]
-- Add the parameters for the stored procedure here
#userId uniqueidentifier
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
declare #retval int
-- Insert statements for procedure here
set #retval = (select top(1) Gender from Users where Guid = #userId)
return #retval
END
Let me know if this works!
Finnaly I end up with wrappers, which preserve the original functionality of database (for ASP.NET MembershipProvider) and create version of stored procedures for EntityFramework:
CREATE PROCEDURE [dbo].[aspnet_UsersInRoles_AddUsersToRolesEF]
#ApplicationName nvarchar(256),
#UserNames nvarchar(4000),
#RoleNames nvarchar(4000),
#CurrentTimeUtc datetime
AS
BEGIN
DECLARE #return_value int
EXEC #return_value = [dbo].[aspnet_UsersInRoles_AddUsersToRoles]
#ApplicationName,
#UserNames,
#RoleNames,
#CurrentTimeUtc
SELECT #return_value
END
GO
Now I can use the compiler to check number and types of parameters in store procedure.

SQL IDENTITY autoincrement when 0 but still allow explicit inserts

I am dealing with this problem: I would like to have autogenerated identity in my table which is of type int
But, I would like to be able to explicitly set the identity. Now the real challenge is that this stuff is going through Entity Framework. I have my database with a IDENTITY(1,1) column, and IDENTITY_INSERT set to ON.
And whenever the Id is 0 (not specified) in newly created object, it inserts the very same 0. Any help appreciated, except offers to reconsider architecture (I will do that in any other case if this attempt fails).
And all this must work either on SQL CE, and SQL Server.
If you tell EF the primary key is database generated then it will not pass the id to the insert sql. You need to pass the ID so go with DatabaseGenerated.None.
But you want it to be an IDENTITY, so make it one in a migration script. You could change the CREATETABLE statement, adding identity: true to the column specification, or you can modify the table by running sql using the Sql() method
Now you need to modify the actual sql run during insert. The only way to do that is configure your model to use stored procedures then modify the sql generated in the Up migration for the insert procedures:
CREATE PROCEDURE [dbo].[My_Insert]
#Id int,
--ETC
AS
BEGIN
IF(Id > 0) SET IDENTITY_INSERT ON
INSERT --ETC
IF(Id > 0) THEN BEGIN
SET IDENTITY_INSERT OFF
SELECT Id
ELSE
SELECT SCOPE_IDENTITY() AS Id
END
END

EF4 - The selected stored procedure returns no columns

I have query in a stored procedure that calls some linked servers with some dynamic SQL. I understand that EF doesn't like that, so I specifically listed all the columns that would be returned. Yet, it still doesn't like that. What am I doing wrong here? I just want EF to be able to detect the columns returned from the stored procedure so I can create the classes I need.
Please see the following code that makes up the last lines of my stored procedure:
SELECT
#TempMain.ID,
#TempMain.Class_Data,
#TempMain.Web_Store_Class1,
#TempMain.Web_Store_Class2,
#TempMain.Web_Store_Status,
#TempMain.Cur_1pc_Cat51_Price,
#TempMain.Cur_1pc_Cat52_Price,
#TempMain.Cur_1pc_Cat61_Price,
#TempMain.Cur_1pc_Cat62_Price,
#TempMain.Cur_1pc_Cat63_Price,
#TempMain.Flat_Length,
#TempMain.Flat_Width,
#TempMain.Item_Height,
#TempMain.Item_Weight,
#TempMain.Um,
#TempMain.Lead_Time_Code,
#TempMain.Wp_Image_Nme,
#TempMain.Wp_Mod_Dte,
#TempMain.Catalog_Price_Chg_Dt,
#TempMain.Description,
#TempMain.Supersede_Ctl,
#TempMain.Supersede_Pn,
TempDesc.Cust_Desc,
TempMfgr.Mfgr_Item_Nbr,
TempMfgr.Mfgr_Name,
TempMfgr.Vendor_ID
FROM
#TempMain
LEFT JOIN TempDesc ON #TempMain.ID = TempDesc.ID
LEFT JOIN TempMfgr ON #TempMain.ID = TempMfgr.ID
EF doesn't support importing stored procedures which build result set from:
Dynamic queries
Temporary tables
The reason is that to import the procedure EF must execute it. Such operation can be dangerous because it can trigger some changes in the database. Because of that EF uses special SQL command before it executes the stored procedure:
SET FMTONLY ON
By executing this command stored procedure will return only "metadata" about columns in its result set and it will not execute its logic. But because the logic wasn't executed there is no temporary table (or built dynamic query) so metadata contains nothing.
You have two choices (except the one which requires re-writing your stored procedure to not use these features):
Define the returned complex type manually (I guess it should work)
Use a hack and just for adding the stored procedure put at its beginning SET FMTONLY OFF. This will allow rest of your SP's code to execute in normal way. Just make sure that your SP doesn't modify any data because these modifications will be executed during import! After successful import remove that hack.
Adding this Non-Logical block of code solved the problem. Even though it will never Hit
IF 1=0 BEGIN
SET FMTONLY OFF
END
Why does my typed dataset not like temporary tables?
http://social.msdn.microsoft.com/Forums/en-US/adodotnetdataset/thread/fe76d511-64a8-436d-9c16-6d09ecf436ea/
Or you can create a User-Defined Table Type and return that.
CREATE TYPE T1 AS TABLE
( ID bigint NOT NULL
,Field1 varchar(max) COLLATE Latin1_General_CI_AI NOT NULL
,Field2 bit NOT NULL
,Field3 varchar(500) NOT NULL
);
GO
Then in the procedure:
DECLARE #tempTable dbo.T1
INSERT #tempTable (ID, Field1, Field2, Field3)
SELECT .....
....
SELECT * FROM #tempTable
Now EF should be able to recognize the returned columns type.
As some others have noted, make sure the procedure actually runs. In particular, in my case, I was running the procedure happily without error in SQL Server Management Studio completely forgetting that I was logged in with admin rights. As soon as I tried running the procedure using my application's principal user I found there was a table in the query that that user did not have permission to access.
Interesting side note: Had the same problem which I first solved by using Table Variables, rather than Temp Tables (just for the import). That wasn't particularly intuitive to me, and threw me off when initially observing my two SProcs: one using Temp tables and one with Table Variables.
(SET FMTONLY OFF never worked for me, so I just changed my SProcs temporarily to get the column info, rather than bothering with the hack on the EF side just as an FYI.)
My best option was really just manually creating the complex type and mapping the function import to it. Worked great, and the only difference ended up being that an additional FactoryMethod to create the properties was included in the Designer.
What I would add is:
That the import also fails if the stored procedures has parameters and returns no result set for the default parameter values.
My stored procedure had 2 float parameters and would not return anything when both parameters are 0.
So in order to add this stored procedure to the entity model, I set the value of these parameters in the stored procedure so that it is guaranteed to return some rows, no matter what the parameters actually are.
Then after adding this stored procedure to the entity model I undid the changes.
both solutions :
1- Define the returned complex type manually (I guess it should work)
2- Use a hack and just for adding the stored procedure put at its beginning SET FMTONLY OFF.
not working with me in some procedure however it worked with other one!
my procedure ends with this line:
SELECT machineId, production [AProduction]
, (select production FROM #ShiftBFinalProd WHERE machineId = #ShiftAFinalProd.machineId) [BProduction]
, (select production FROM #ShiftCFinalProd WHERE machineId = #ShiftAFinalProd.machineId) [CProduction]
FROM #ShiftAFinalProd
ORDER BY machineId
Thanks
In addition to what #tmanthley said, be sure that your stored procedure actually works by running it first in SSMS. I had imported some stored procedures and forgot about a couple dependent scalar functions, which caused EF to determine that the procedure returned no columns. Seems like a mistake I should have caught earlier on, but EF doesn't give you an error message in that case.
Entity Framework will try to get the columns by executing your stored procedure, passing NULL for every argument.
Please make sure that the stored procedure will return something under all the circumstances. Note it may have been smarter for Entity Framework to execute the stored proc with default values for the arguments, as opposed to NULLs.
ER does the following to get the metadata of the table:
SET FMTONLY ON
This will break your stored procedure in various circumstances, in particular, if it uses a temporary table.
So to get a result as complex type; please try by adding
SET FMTONLY OFF;
This worked for me - hope it works for you too.
Referred from https://social.msdn.microsoft.com/Forums/en-US/e7f598a2-6827-4b27-a09d-aefe733b48e6/entity-model-add-function-import-stored-procedure-returns-no-columns?forum=adodotnetentityframework
In my case adding SET NOCOUNT ON; at the top of the procedure fixed the problem. It's best practice anyway.
In my case SET FMTONLY OFF did not work. The method I followed is, I took backup of original stored procedure and replace with only column name like the below query.
Select Convert(max,'') as Id,Convert(max,'') as Name
After this change, create new function import, complex type in entity framework.
Once the function import and complex type is created, replace the above query with your original stored procedure.
SET FMTONLY OFF
worked for me for one of the procedure but failed for other procedure. Following steps helps me to resolve my problem
Within a stored procedure, I have created temporary table with the same column type and inserted all the data returned by dynamic query to temp table.
and selected the temp table data.
Create table #temp
(
-- columns with same types as dynamic query
)
EXEC sp_executeSQL #sql
insert into #temp
Select * from #temp
drop table #temp
Deleted existing complex type, import function and stored procedure instance for old stored procedure and updated entity model for current new procedure.
Edit the imported Function in entity modal for desired complex type, you will get all the column information there which is not getting for previous stored procedure.
once you have done with the type creation you can delete the temporary table from stored procedure and then refresh Entity Framework.
In Entity framework, while getting column information the sql executes the procedure with passing null values in parameter. So I handled null case differently by creating a temp table with all the required columns and returning all the columns with no value when null is passed to the procedure.
In my procedure there was dynamic query, something like
declare #category_id int
set #category_id = (SELECT CATEGORY_ID FROM CORE_USER where USER_ID = #USER_ID)
declare #tableName varchar(15)
declare #sql VARCHAR(max)
declare #USER_IDT varchar(100)
declare #SESSION_IDT varchar(10)
IF (#category_id = 3)
set #tableName = 'STUD_STUDENT'
else if(#category_id = 4)
set #tableName = 'STUD_GUARDIAN'
if isnull(#tableName,'')<>''
begin
set #sql = 'SELECT [USER_ID], [FIRST_NAME], SCHOOL_NAME, SOCIETY_NAME, SCHOOL_ID,
SESSION_ID, [START_DATE], [END_DATE]
from #tableName
....
EXECUTE (#sql)
END
ELSE
BEGIN
SELECT * from #UserPrfTemp
END
I was not getting the column information in
my case after using the set FMTONLY OFF trick.
This is temp table I created to get the blank data.
Now I am getting the column info
Create table #UserPrfTemp
(
[USER_ID] bigint,
[FIRST_NAME] nvarchar(60),
SCHOOL_NAME nvarchar(60),
SOCIETY_NAME nvarchar(200)
.....
}
I solved this problem creating a table variable and then returning from it.
DECLARE #VarTable TABLE (
NeededColumn1 VARCHAR(100),
NeededColumn2 INT,
NeededColumn3 VARCHAR(100)
)
...
--Fetch Data from Linked server here
...
INSERT INTO #VarTable (NeededColumn1,NeededColumn2,NeededColumn3)
SELECT Column1, Column2, Column3
FROM #TempTable
SELECT * FROM #VarTable.
In that manner, your the SP result will be bounded to the table variable, which EF has access to.
I discovered a method that should help most people out whatever's happening.
Pull up your favourite SQL client and run the proc that you're trying to update with every parameter = null. Visual Studio is literally trying to do this when SET FMTONLY ON. Run a trace. You'll see.
You'll probably get an error, or unexpected data out. Fix that and your issue is fixed.
In my case the function read in JSON and failed because the JSON string was empty.
I just put something like
IF(#FooJSON IS NULL)
BEGIN
SELECT 1 VAR1, 2 VAR2;
END
ELSE
--OTHER LOGIC
That's probably an ugly solution, but I inherited this mess and we don't go into Ravenholm.
Change #Temp tables with WITH SQL EXPRESSION

Resources