Migrate Nhibernate Castle.ActiveRecord to EFCore HiLo - c#

We're currently migrating our old OR Mapper to EF Core. Till now we used the
http://www.castleproject.org/projects/activerecord
or mapper with the HiLo algorithm. The explanations is:
https://github.com/castleproject-deprecated/ActiveRecord/blob/master/docs/primary-key-mapping.md
Now we want to switch to EF Core and will try to use the same algorithm. But there isn't much explanation how the HiLo algorithm exactly works in Nhibernate/ActiveRecord. And I try to avoid Id collision.
As far as I see, the Hi value is configured in a Database:
select next_hi from hibernate_unique_key
with the value: 746708
I think the maxLow Value is Int16.MaxValue
In that case the Sequence for EFCore should be:
CREATE SEQUENCE [dbo].[DBSequenceHiLo]
AS [bigint]
START WITH (select next_hi from hibernate_unique_key + Int16.MaxValue)
INCREMENT BY Int16.MaxValue
MINVALUE -9223372036854775808
MAXVALUE 9223372036854775807
CACHE
GO
How does the ActiveRecord HiLo Algorithm exactly works? What is the Increment by value? What is the start with value? The migration will take some time, is it possible to run it parallel with the same HiLo algorithm?

As far as I know. It's not possible to use the exact same algorithm for ActiveRecord and EF Core. One works with Sequence and the other works with a table. So you can't use both OR Mapper parallel. But you can create a Sequence for EF Core without ID collision, you just can't use ActiveRecord afterwards.
To get the INCREMENT BY value just start the current app. Create a DB Entry with the App. Stop it. Start it again and create a second entry. Because you stopped the app, the Lo/cache is empty and it gets the next hi value. The difference between those two ID's is the "INCREMENT BY" value of Active Record. It was 2^17 in my case. Default should be 2^15 I think, but I haven't seen any Infos about it.
To get a start value I created a SQL script, to get the highest Id of the database. Here is my script (It works only if your PK is named Id and it only works with sql.)
DECLARE #tables TABLE(tablename nvarchar(max) NOT NULL);
DECLARE #name nvarchar(max)
DECLARE #maxid bigint
DECLARE #currentid bigint
DECLARE #query nvarchar(max);
DECLARE #sSQL nvarchar(500);
DECLARE #ParmDefinition nvarchar(500);
set #maxid = 0
insert into #tables
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_CATALOG = 'BB_Vision'
While(Select Count(*) From #tables) > 0
Begin
Select Top 1 #name = tablename From #tables
IF EXISTS(SELECT 1 FROM sys.columns
WHERE Name = N'Id'
AND Object_ID = Object_ID(#name))
BEGIN
SELECT #sSQL = N'SELECT #retvalOUT = MAX(ID) FROM ' + #name;
SET #ParmDefinition = N'#retvalOUT bigint OUTPUT';
EXEC sp_executesql #sSQL, #ParmDefinition, #retvalOUT=#currentid OUTPUT;
IF #currentid > #maxid
BEGIN
set #maxid = #currentid
END
END
Delete #tables Where #name = tablename
End
select #maxid+1
Now you can create your EF Core Sequence. Here is an explanation how to use it:
https://www.talkingdotnet.com/use-hilo-to-generate-keys-with-entity-framework-core/
After that you shouldn't use ActiveRecord anymore or you have to create your sequence again with a higher start value.
Because the migration takes some time and you will mostly still create some Features/Bugfix for the current OR mapper, it's a good idea to set your ActiveRecord Hi value to a larger value on your local Database. So you can work with both on the same Database. But I wouldn't use it in production
update hibernate_unique_key set next_hi = next_hi + next_hi

Related

How to get custom SQL Server type into Entity Framework generated code

There is a TABLE Type defined in SQL server:
CREATE TYPE RealtySearchResult AS TABLE
(
realtyId int not null,
OwnerId int not null,
...)
And stored procedure:
CREATE PROCEDURE [dbo].[SearchRealty]
(#fulltext nvarchar(200) null,
#skipRows int,
#pageCount int,
....
)
AS
BEGIN
DECLARE #SQL nvarchar(max)
DECLARE #result RealtySearchResult
CREATE TABLE #TEMP
(
realtyId int not null,
OwnerId int not null,
...
)
set #SQL = N'
INSERT INTO #TEMP
SELECT
realty.Id AS realtyId,
realty.OwnerId,
....join with fulltext catalog.... WHERE....#pageCount .....#skipRows'
-- sp_executesql cannot write to local table variable #result,
-- that is why it reads to temp table and then to #result
exec sp_executesql #SQL, N'#skipRows int, #pageCount int', #skipRows, #pageCount
INSERT INTO #result SELECT * FROM #TEMP
SELECT * FROM #result
END
And then in Visual Studio I update the model from database and a new method (wrapper for store procedure SearchRealty) is generated, but it does not contains generated code for returning complex type.
I would expect that EntityFramework should be able to recognize that the store procedure returns defined table type RealtySearchResult and should generate wrapper for it. I am too lazy to write the complex return type by myself in C# again (I just wrote it in SQL). It is really needed?
Can I just generate wrapper for RealtySearchResult type in EntityFramework somehow?
I use Visual Studio 2017 and EntityFramework 6.
It sounds as duplicate as Stored procedure in Entity Framework database first approach but once I click the button Get Column Information I got message "The selected stored procedure or function returns no columns".
Analysis
Based on link Entity Framework not getting column info on a different schema provided by kirsten I realize that EntityFramework execute stored procedure with mode
SET FMTONLY ON
It means it strips all condition and dynamic SQL. This result in empty temporary table and procedure failing during receiving metadata from EntityFramework.
Solution
To help the designer to get metadata without dynamic SQL. Count with that conditions are removed. Following code does a job:
DECLARE #result RealtySearchResult
IF 0=1
BEGIN
SELECT * FROM #result
RETURN
END
During execution of store procedure by EntityFramework (in order to get metadata), condition 0=1 is removed and empty table of Table type is returned which is enough to get metadata. This code is never trigerred in production because of impossible condition.

Generate unique sequential numbers using SQL in multi-user environment

I want to generate some unique sequential counters in tbl1 table, so I wrote following code:
using (var dbConnection = CommonDA.GetDbConnection())
{
var command = dbConnection.CreateCommand();
dbConnection.Open();
command.CommandText = "insert into tbl1(Counter) select Max(Counter)+1 from tbl1;";
command.ExecuteNonQuery();
}
but sometimes the generated counters are duplicate(because multiple users run the code), is there any mechanism that I generate unique sequential counters?
You can try to use SEQUENCE like this
Source:
Step 1 :
In this step, we need to create a sample table and a sequence to
demonstrate it.
-- This script is compatibile with SQL Server 2012 and above.
-- CREATE TABLE
USE tempdb
GO
CREATE TABLE dbo.tbl_sample
( [ID] VARCHAR(8) ,
[Name] varchar(50)
CONSTRAINT PK_Employee_EmployeeID
PRIMARY KEY CLUSTERED ([ID] ASC) )
GO
--CREATE SEQUENCE
USE tempdb
GO
CREATE SEQUENCE dbo.Sample_Seq AS
INT START WITH 1
INCREMENT BY 1 ;
GO
Step 2 :
In this step, we need to create a default value for the [ID] column of
the above table, and the default value of the [ID] column should be
SEQUENCE and add a custom script to make it varchar.
Given below is the script.
-- This script is compatibile with SQL Server 2012 and above.
-- CREATE DEFAULT VALUE OF SEQUENCE
USE tempdb
GO
ALTER TABLE dbo.tbl_sample
ADD CONSTRAINT Const_Sample_Seq
DEFAULT FORMAT((NEXT VALUE FOR dbo.Sample_Seq),'CUS0000#') FOR [ID];
GO
Step 3 :
Lets insert few records into the table.
-- This script is compatibile with SQL Server 2012 and above.
-- Insert records into the table
USE tempdb
GO
INSERT INTO dbo.tbl_sample ([Name])
VALUES ('Imran'),('Bob'),('Sandra')
GO
Step 4 :
Once the data has been inserted, you can browse the table and view the
[ID] column data that it is either a number or varchar only.
--Browse Table
USE tempdb
GO
SELECT * FROM tbl_sample
GO
--OUTPUT
In SQL Server 2008 there is no sequence object, but that doesn't mean you can't mimic one, including all the options of a sequence object, except cashing.
However, You must take into consideration the fact that there maybe gaps in the sequence (i.e. 1, 2, 5, 6, 8...)
To do that, you start by creating a table with a single column, that is specified as identity:
CREATE TABLE tblSequence
(
Number int identity(1, 1)
)
Then, create a stored procedure that will get you the next number:
CREATE PROCEDURE stp_NextSequenceNumber
(
#NextNumber int output
)
AS
BEGIN
INSERT INTO tblSequence DEFAULT VALUES
SELECT #NextNumber = SCOPE_IDENTITY()
END
GO
Now all you have to do to get the next number is simply execute the stored procedure:
DECLARE #NextNumber int
EXEC stp_NextSequenceNumber #NextNumber output
It's also possible to further develop that procedure to handle recycling of the sequence using truncate table, that will delete all values from the table and reset the identity column to it's seed:
CREATE PROCEDURE stp_NextSequenceNumber
(
#NextNumber int output
)
AS
BEGIN
DECLARE #CurrentNumber int
SELECT #CurrentNumber = MAX(Number)
FROM tblSequence
IF #CurrentNumber >= 10 -- In this example, 10 is the max value of the sequence
TRUNCATE TABLE tblSequence
INSERT INTO tblSequence DEFAULT VALUES
SELECT #NextNumber = SCOPE_IDENTITY()
END
GO
Yes. You can use NEWSEQUENTIALID via SQL Server. It is guaranteed to be unique across space and time.

C# MSSQL alter table then modify values

While the following work fine in SQL Server Management Studio, it just won't work in C#:
DECLARE #PeriodID AS bigint;
SELECT TOP 1 #PeriodID = PeriodID FROM Periods ORDER BY PeriodID DESC;
IF NOT EXISTS(SELECT * FROM information_schema.columns WHERE COLUMN_NAME = N'PeriodID' AND TABLE_NAME = N'MobilePlans')
BEGIN
BEGIN
ALTER TABLE MobilePlans ADD PeriodId bigint NULL
END
BEGIN
UPDATE MobilePlans SET PeriodID = #PeriodID
END
BEGIN
ALTER TABLE MobilePlans ALTER COLUMN PeriodID bigint NOT NULL
END
END
In C#, it keeps telling me Invalid column name 'PeriodID'. and after spending a couple of hours searching, I thought I'd ask here.
While searching, I came across http://bytes.com/topic/c-sharp/answers/258520-problem-sqlcommand-t-sql-transaction, but I couldn't really translate my conditional query to that.
Why can't C# do the same as the Management studio?
Is there another way to do what the query does, that works in C#? I need to perform this on 400 databases, so I'd really like a script to do it for me.
Thanks in advance!
SQL server version is 2008.
Manager version is 2008 (10.0.2531).
.NET framework version is 2.0.
I get "invalid column name 'PeriodID'" running it in Management Studio, if the table doesn't already have the PeriodID column.
Repro:
create table Periods (
PeriodID bigint not null
)
go
insert into Periods(PeriodID) select 1
go
create table MobilePLans (
BLah int not null
)
go
insert into MobilePLans(BLah) select 2
go
DECLARE #PeriodID AS bigint;
SELECT TOP 1 #PeriodID = PeriodID FROM Periods ORDER BY PeriodID DESC;
IF NOT EXISTS(SELECT * FROM information_schema.columns WHERE COLUMN_NAME = N'PeriodID' AND TABLE_NAME = N'MobilePlans')
BEGIN
BEGIN
ALTER TABLE MobilePlans ADD PeriodId bigint NULL
END
BEGIN
UPDATE MobilePlans SET PeriodID = #PeriodID
END
BEGIN
ALTER TABLE MobilePlans ALTER COLUMN PeriodID bigint NOT NULL
END
END
The reason is simple - SQL Server tries to compile each batch completely. If the column already exists, then the UPDATE statement can be compiled. If not, you get the error.
If you put the update inside an Exec:
EXEC('UPDATE MobilePlans SET PeriodID = ' + #PeriodID)
Then it will be compiled at the point that the column does exist.
I guess you have a spelling mistake in your c# code.
The error is saying PeriodeID whereas your column name is PeriodID.
Clarification:
1 - You would like to push this script it against many databases?
2 - Invalid column name 'PeriodeID', I cannot see a column called "PeriodeId", but "PeriodId", is this a typo?
3 - Can you try the same block without main BEGIN/END block?
Is the server collation Case Insensitive ? Because you're adding PeriodId and then updating PeriodID

Linq to SQL SP is returning multiple recordsets when I only want one!

Hi I have the following SP, however when I use LINQ to SQL it generates 2 multiple recordsets. For my sanity I am trying to fathom out what it is in the stored procedure that is doing this and would like to only return a single recordset... Can any help?
ALTER PROCEDURE [dbo].[CheckToken]
#LoginId int
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #failures INT
SET #failures = (SELECT COUNT(loginid) FROM auditerrorcode WHERE
errorid = 1012 AND loginid = #loginid
AND datecreated > DATEADD(hh, -1, getdate())
)
IF #failures > 10 UPDATE [login] SET [IsDisabled]=1 WHERE loginid = #loginid
SELECT * FROM [Login] WHERE LoginId = #LoginId
END
Execute your procedure stand alone and rule out your not getting two rows because there are two rows being returned for the ID you are passing in. Do this in SQL Managment Studio with a
EXEC dbo.CheckToken 999
Make sure to use the same #LoginID that you are calling from your .NET code.
Sorry Guys....
I looked again in the DBML file generated and deleted the CheckToken method which had multiple result sets defined. I then regenerated and now I get what I expected, one recordset
Looks like the mods I made to the SP has worked.

How do I get Linq to SQL to recognize the result set of a dynamic Stored Procedure?

I'm using Linq-to-SQL with a SQL Server backend (of course) as an ORM for a project. I need to get the result set from a stored procedure that returns from a dynamically-created table. Here's what the proc looks like:
CREATE procedure [RetailAdmin].[TitleSearch] (
#isbn varchar(50), #author varchar(50),
#title varchar(50))
as
declare #L_isbn varchar(50)
declare #l_author varchar(50)
declare #l_title varchar(50)
declare #sql nvarchar(4000)
set #L_isbn = rtrim(ltrim(#isbn))
set #l_author = rtrim(ltrim(#author))
set #l_title = rtrim(ltrim(#title))
CREATE TABLE #mytemp(
[storeid] int not NULL,
[Author] [varchar](100) NULL,
[Title] [varchar](400) NULL,
[ISBN] [varchar](50) NULL,
[Imprint] [varchar](255) NULL,
[Edition] [varchar](255) NULL,
[Copyright] [varchar](100) NULL,
[stockonhand] [int] NULL
)
set #sql = 'select a.storeid, Author,Title, thirteendigitisbn ISBN,
Imprint,Edition,Copyright ,b.stockonhand from ods.items a join ods.inventory b on
a.itemkey = b.itemkey where b.stockonhand <> 0 '
if len(#l_author) > 0
set #sql = #sql + ' and author like ''%'+#L_author+'%'''
if len(#l_title) > 0
set #sql = #sql + ' and title like ''%'+#l_title+'%'''
if len(#L_isbn) > 0
set #sql = #sql + ' and thirteendigitisbn like ''%'+#L_isbn+'%'''
print #sql
if len(#l_author) <> 0 or len(#l_title) <> 0 or len(#L_isbn) <> 0
begin
insert into #mytemp
EXECUTE sp_executesql #sql
end
select * from #mytemp
drop table #mytemp
I didn't write this procedure, but may be able to influence a change if there's a really serious problem.
My present problem is that when I add this procedure to my model, the designer generates this function:
[Function(Name="RetailAdmin.TitleSearch")]
public int TitleSearch([Parameter(DbType="VarChar(50)")] string isbn,
[Parameter(DbType="VarChar(50)")] string author,
[Parameter(DbType="VarChar(50)")] string title)
{
IExecuteResult result = this.ExecuteMethodCall(this,
((MethodInfo)(MethodInfo.GetCurrentMethod())), isbn, author, title);
return ((int)(result.ReturnValue));
}
which doesn't look anything like the result set I get when I run the proc manually:
Can anybody tell me what's going wrong here?
This is basically the same problem as this question but due to the poor phrasing from the OP it was never really answered.
Thanks Marc for your reply. I will see about making the changes you suggested.
The problem was the temp table. Linq to Sql just doesn't know what to do with them. This was particularly difficult to diagnose, because Visual Studio caches information about stored procs, so when it initially failed to find a result set it set the return as a default integer type and didn't update when I made changes to the stored proc. Getting VS to recognize a change requires you to:
Delete proc from the dbml
delete the server connection from Server Explorer
save the dbml to force a recompile
close the project and restart VS
recreate the server connection and import the proc
You might not have to do every one of those steps, but that's what worked for me. What you need to do, if you must use a temp table, is to create a barebones proc that simply returns the correct schema, and then alter it to do what you want after you've imported it into the OR Designer.
First - IMPORTANT - your SQL is vulnerable to injection; the inner command should be parameterized:
if len(#l_author) > 0
set #sql = #sql + ' and author like ''%''+#author+''%'''
EXECUTE sp_executesql #sql, N'#author varchar(100)', #L_author
This passes the value of #L_author in as the #author parameter in the dynamic command - preventing injection attacks.
Second - you don't really need the temp table. It isn't doing anything for you... you just INSERT and SELECT. Perhaps just EXEC and let the results flow to the caller naturally?
In other circumstances a table-variable would be more appropriate, but this doesn't work with INSERT/EXEC.
Are the columns the same for every call? If so, either write the dbml manually, or use a temp SP (just with "WHERE 1=0" or something) so that the SET FMT_ONLY ON can work.
If not (different columns per usage), then there isn't an easy answer. Perhaps use regular ADO.NET in this case (ExecuteReader/IDataReader - and perhaps even DataTable.Fill).
Of course, you could let LINQ take the strain... (C#):
...
if(!string.IsNullOrEmpty(author)) {
query = query.Where(row => row.Author.Contains(author));
}
...
etc
There's no real easy way to do this. I've had the same problem in the past. I think the issue is that Linq to Sql has no way of "figuring out" which type will be returned since you're building up the SELECT statement at execution time. What I did to get around this, was in the stored proc, I did just a select and selected all the columns that I possibly needed. Then, I had Linq to Sql generate the function based on that. Then, I went back to SQL and changed the stored proc back to the way it's supposed to be. The trick here is not to regenerate your DBML.

Categories

Resources