How to run a subquery when inserting into a table in EFCore - postgresql

I have an orderid integer column in a Postgres table called Orders and it's not the primary key. And there's a logic to automatically increment it considering the max value of the orderid and adding 1 to it.
One solution to this is creating a function in the database layer and set it as the default value to this column. But since that couples us to the database layer, we are thinking of something that's related to EF. We can't pass a SQL as the default value to orderid because postgres gives an error. We can also make it an auto-increment value but we don't wanna do that because the logic could change.
What we want is a way to run this subquery and automatically generate the value to orderid.

I presume what you want to achieve here is to maintain a separate non-primary key, auto-incremented unique id which is number. You can this by using a sequence.
1) Create the sequence via fluent API
2) Set the column to use the next value from the sequence
3) Make sure to create and run migrations before testing.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasSequence<int>("OrderNumbers")
.StartsAt(1000)
.IncrementsBy(1);
modelBuilder.Entity<Order>()
.Property(o => o.OrderNo)
.HasDefaultValueSql("nextval('\"OrderNumbers\"')");
}

Related

How do you add a Unique case insensitive Index to a PostgresSQL table using EF Fluent API

I have recently been using PostgreSQL rather than SQL, so finding a lot of little nuances between the two.
I want to be able to make a string value unique in a table, so using the EF code first fluent API, I have this code.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MyTable>()
.HasIndex(u => u.UniqueValue)
.IsUnique();
base.OnModelCreating(modelBuilder);
}
When creating a migration, it will generate this.
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_MyTable_UniqueValue",
table: "MyTable",
column: "UniqueValue",
unique: true);
}
This will then add the index to the PostgreSQL table, and work when the word is of the same case.
e.g. Try and insert "Hello" twice, and it will not work.
It does allow for variations of the word though, so I can insert "Hello", "HEllo", "HELlo", etc...
It looks like it is possible to force the case on the index in PostgreSQL using something like
CREATE UNIQUE INDEX UniqueValue ON MyTable (UPPER(UniqueValue));
However I am not sure how to do this via EF Fluent API and create the migration from that?
It seems like for now you'll have to set up a raw SQL migration. Support for that still hasn't been added (yet). You could also set up a generated (computed) column that holds the result of upper(UniqueValue), then add a unique index on that.
There's no way to add an expression-based unique constraint or to add the expression to an existing unique index key. You can build a new index using your definition as is:
CREATE UNIQUE INDEX UniqueValue ON MyTable (UPPER(UniqueValue));
Or add an exclusion constraint which ends up doing pretty much the same (as of now, quite far from being supported in EF):
create table test(txt text);
alter table test
add constraint case_insensitive_unique
exclude using gist(upper(txt) with =);
insert into test(txt) select 'hello';
--INSERT 0 1
insert into test(txt) select 'Hello';
--ERROR: conflicting key value violates exclusion constraint "case_insensitive_unique"
--DETAIL: Key (upper(txt))=(HELLO) conflicts with existing key (upper(txt))=(HELLO).
Demo

EF Core Entity with Primary Key but without Clustered Index

For Entity Framework, when we have a primary Key on the Entity, EF always generates a clustered index.
public class TestEntity
{
[key]
public int NaturalKeyId;
public string Field2;
public string Field3;
}
The NaturalKeyId field will form a clustered index.
Is there a way to retain it as the (unique) primary key, but not have it as a clustered index? Before I get trolled, I agree that it is generally better to have a clustered index on a monotonously increasing (surrogate) Id field. However in a specific case, all update operations are always using the NaturalKey (there is no convenient Id passed in for the Update. Updates are always triggered by external systems that only know the Natural Key) and I want to avoid (if possible) the overhead of having to first read the Entity via the natural key before being able to update / delete it.
I cannot convert the Natural Key itself as the clustered Index as the key values are essentially random and I will face Page Splits for Inserts.
Edit: I already have a working solution where after running the migration, I manually recreated the table in the DB with a unique index on NaturalKeyId instead of Clustered Index.
For Microsoft SQL Server, you can use the fluent API to configure the primary key as non-clustered:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<TestEntity>().HasKey(e => e.NaturalKeyId).IsClustered(false);
}
Microsoft SQL Server Database Provider - Indexes - EF Core | Microsoft Docs

How to change entry values for DbContext.SaveChanges()

We are trying to implement a multi tenant architecture in our Web API based application. We are using RLS in SQL Server, and Subscription_Id is, what is given to each subscriber. We have set the default value for Subscription_Id in SQL Server, so while I am calling db.SaveChanges(), I just want to ignore the Subscription_Id going to the SQL Server from the API.
I tried setting the value of Subscription_Id in the SaveChanges() override method but got stuck here.
public override int SaveChanges()
{
var objectType = selectedEntity.CurrentValues.ToObject();
Guid value = new Guid("54E720FC-616B-44C6-8485-5F2185FD7B4C");
PropertyInfo propertyInfo =
objectType.GetType().GetProperty("Subscription_Id");
ChangeTracker.Entries().FirstOrDefault()
.CurrentValues.ToObject().GetType()
.GetProperty("Subscription_Id")
.SetValue(objectType, Convert.ChangeType(value, propertyInfo.PropertyType), null);
return base.SaveChanges();
}
My advice is that you shouldn't modify your SaveChanges() code for this.
A recommended way of using RLS is making the TenantId columns transparent to your EF model and your code, so you don't need to define Tenant ID or navigation properties in your entities. This way you don't need to change your SaveChanges() code, or to explicitly manage and set Subscription_Id values anywhere in your code other that when opening the DB connection.
What you need to do is manually setting a default value constraint in the Subscription_Id columns in your database, with a default value based on the current session Subscription_Id parameter. The value will be set when inserting the records, and implicitly used to filter any subsequent queries and commands at database level.
In case of a new column:
ALTER TABLE SomeEntityTable ADD Subscription_Id nvarchar(128)
DEFAULT CAST(SESSION_CONTEXT(N'UserId') AS nvarchar(128))
In case of an existing column:
ALTER TABLE SomeEntityTable
ADD DEFAULT CAST(SESSION_CONTEXT(N'UserId') AS nvarchar(128)
FOR Subscription_Id
If the column had a previous different DEFAULT value it would be good to also delete its associated obsolete DEFAULT constraint. More info about updating default values in existing columns can be found here.
These columns should not be included in your model. You should not have properties for them in your entity classes. If you are using Database First you should make sure you exclude/ignore these columns when updating your model from your database.
How to do this if you are using EF Code First: you can manually include AlterColumn (or CreateColumn) instructions in a code migration after you generate it with Add-Migration. Do it for every entity table:
public override void Up()
{
AlterColumn("dbo.SomeEntityTable", "Subscription_Id",
c => c.String(
nullable: false,
maxLength: 128,
defaultValueSql: "CAST(SESSION_CONTEXT(N'UserId') AS nvarchar(128))"));
}
(It would be good to add also a Down() method removing the column.)
Warning: Be careful when running this migration if you already have existing records in the tables with an empty Subscription_Id column value (or if you are adding a new Subscription_Id column to a table that already have records). The empty column will be filled with the value of the Subscription_Id in the connection that is executing the migration, which probably will be wrong (you probably don't want all the existing records to be associated to that specific subscription). In that case you may want to include explicit UPDATE instructions with the right Subscription_Id values in your Up() method, with the Sql() method. Something like this:
Sql("UPDATE SomeEntitiesTable SET Subscription_Id= '19bc9b0d-28dd-4510-bd5e-d6b6d445f511' WHERE Id IN (1, 2, 5)");
With Code First you should also remove the Subscription_Id properties from your model classes. If you can't, at least add explicit Ignore() instructions in your configuration code for the Subscription_Id columns, you don't want them in your EF mappings.
Note: I'm assuming here that you created a RLS policy in your DB that uses UserId parameter in SESSION_CONTEXT, and that your application code is setting that value when opening the DB connection, via a DbConnectionInterceptor or something similar.
This page contains more info.

Entity Framework database defaults on inserts allow updates

Is there a way to have entity framework use a SQL default value on an insert and yet allow updating to the field. We have an instance where a SQL table has an identity column "id" and another column which is set to ident_current("table"). The only way that I know of to get the field inserted with the default value is to set the field as DatabaseGenerated(DatabaseGeneratedOption.Computed) so that it is ignored on the insert. However by having that attribute then we cannot perform an update to the column. Also it's a self referencing foreign key so we can't do an insert then immediate update to get around the issue. Don't ask me why the table is designed this way - just the way it was set up before so we're kind of stuck with it for now. A simple diagram of our setup is below:
DomainClass:
Class1 {
public int id {get;set;}
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public int id2 {get;set;}
public string Name {get;set;}
}
SQL (pseudo):
Table (
id INT which is an identity(1,1) column,
id2 INT NOT NULL with a default value of ident_current("table")
Name nvarchar(50)
)
We would want the insert statement generated by EF to be:
INSERT INTO Table(Name) VALUES('Name')
and the update to be:
UPDATE table
SET id2 = *somenumber*, name = 'Name'
Thanks a lot for all the help. We are using EF 4.3.1.0 if that's needed as well.
There is no way AFAIK. See this and that.
The first link points to a suggestion about using sequences as primary keys, which seems like something you might want to do instead given your example code.
The second link points to a suggestion about generic handling of default values, which is currently not supported either, but would be another potential starting point toward adding support for what you need.

How should I annotate CreatedOn and ModifiedOn columns with EF 4.1?

In our database, every table has two DateTime columns, CreatedOn and ModifiedOn, set via triggers in SQL Server. CreatedOn is set on INSERT, and ModifiedOn is set on INSERT and UPDATE.
I am trying to use Entity Framework 4.1. How should I annotate/configure the two properties?
I think it involves the annotation [DatabaseGenerated(DatabaseGeneratedOption.Computed)], but should I use that annotation for both, or should I set [DatabaseGenerated(DatabaseGeneratedOption.Identity)] on the CreatedOn field?
According to MSDN Identity simply implies that The database generates a value when a row is inserted., which seems true here.
Also, should I use [Timestamp]?
Use Identity for CreatedOn and Computed for ModifiedOn. Identity means that value is set only during insert and returned back to application. Computed is set during each modification (including insert) and value is returned back to the application after each executed insert or update.
Just be aware that neither of these properties can be set in the application. Computed columns also can't be part of primary key or foreign key (it will not be your case).
This will only work with existing database. When using code-first Computed can be set only for timestamp or rowversion.
Timestamp is used for optimistic concurrency. If you mark a column as timestamp each update will contain condition WHERE timestampColum = #lastKnownValue. It will update the record only if last known value is the same as current value. If the value is different you will get an exception. It is usually used with timestamp SQL type. Using it with datatime would require some tests. Value of SQL datatime is not the same as value in .NET.