I am new to EF (its my first week), but not new to databases or to programming. Others have asked similar questions, but I don't feel that it has been asked with the right detail or explained quite as it needs to be explained, so here I go.
Question: How do I get Entity Framework to properly deal with columns in a database that have a DEFAULT CONSTRAINT defined when performing an INSERT? Meaning, if I do not supply a value in my model during an insert operation, how do I get EF to exclude that column from its generated TSQL INSERT command, so that the database-defined DEFAULT CONSTRAINT will work?
Background
I have a simple table I created, just to test Entity Framework 6 (EF6) and its interaction with the columns SQL Server is capable of updating. This utilizes IDENTITY, TIMESTAMP, COMPUTED, and a few columns with a DEFAULT CONSTRAINT applied.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[DBUpdateTest](
[RowID] [int] IDENTITY(200,1) NOT NULL,
[UserValue] [int] NOT NULL,
[DefValue1] [int] NOT NULL,
[DefValue2null] [int] NULL,
[DefSecond] [int] NOT NULL,
[CalcValue] AS
(((([rowid]+[uservalue])+[defvalue1])+[defvalue2null])*[defsecond]),
[RowTimestamp] [timestamp] NULL,
CONSTRAINT [PK_DBUpdateTest] PRIMARY KEY CLUSTERED
(
[RowID] ASC
)
WITH
(PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,
ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
)
GO
ALTER TABLE [dbo].[DBUpdateTest]
ADD CONSTRAINT [DF_DBUpdateTest_DefValue1]
DEFAULT ((200)) FOR [DefValue1]
GO
ALTER TABLE [dbo].[DBUpdateTest]
ADD CONSTRAINT [DF_DBUpdateTest_DefValue2null]
DEFAULT ((30)) FOR [DefValue2null]
GO
ALTER TABLE [dbo].[DBUpdateTest]
ADD CONSTRAINT [DF_DBUpdateTest_DefSecond]
DEFAULT (datepart(second,getdate())) FOR [DefSecond]
GO
EF6 handles the IDENTITY, TIMESTAMP, and COMPUTED columns perfectly, meaning after INSERT or UPDATE (via context.SaveChanges()
) EF reads the new values back into the entity object for immediate use.
However, this does not happen for the columns with the DEFAULT CONSTRAINT. And from what I can tell, this is because when EF generates the TSQL to perform the INSERT, it supplies the common default value for nullable or non-nullable types, just as if that column had no DEFAULT CONSTRAINT defined on it. So it seems clear that EF completely ignores the possibility of a DEFAULT CONSTRAINT.
Here is my EF code to INSERT a DBUpdateTest record (and I only update a single column):
DBUpdateTest myVal = new DBUpdateTest();
myVal.UserValue = RND.Next(20, 90);
DB.DBUpdateTests.Add(myVal);
DB.SaveChanges();
Here is the EF generated SQL during an INSERT to DBUpdateTest (which dutifully updates all possible columns):
exec sp_executesql
N'INSERT [dbo].[DBUpdateTest]([UserValue], [DefValue1], [DefValue2null],
[DefSecond])
VALUES (@0, @1, NULL, @2)
SELECT [RowID], [CalcValue], [RowTimestamp]
FROM [dbo].[DBUpdateTest]
WHERE @@ROWCOUNT > 0 AND [RowID] = scope_identity()',
N'@0 int,@1 int,@2 int',@0=86,@1=0,@2=54
Note that it is very clearly supplying what would be the default value for an INT NOT NULL (0) and an INT NULL (null), which completely overcomes the DEFAULT CONSTRAINT.
This is what happens when the EF INSERT command executes, where it supplies a NULL for the nullable column and a ZERO for the INT column
RowID UserValue DefValue1 DefValue2null DefSecond CalcValue
=========================================================================
211 100 200 NULL 0 NULL
If, on the other hand, I execute this statement:
insert into DBUpdateTest (UserValue) values (100)
I will get a record like so
RowID UserValue DefValue1 DefValue2null DefSecond CalcValue
=========================================================================
211 100 200 30 7 3787
This works as expected for one reason: the TSQL INSERT command did not provide values for any columns with a defined DEFAULT CONSTRAINT.
What I am trying to do, therefore, is to get EF to exclude the DEFAULT CONSTRAINT columns from the INSERT TSQL if I do not explcitly set values for them in the model object.
Things I Already Tried
1. Recognize Default Constraint? SO: How to get EF to handle a Default Constraint
In the OnModelCreating()
method of my DbContext
class, it was recommended that I could tell EF that a column with the DEFAULT CONSTRAINT is a COMPUTED field, which it is not. However, I wanted to see if it would get EF to at least read the value back after the INSERT (never mind that it would also likely keep me from being able to assign a value to that column, which is just more of what I do not want):
modelBuilder.Entity<DBUpdateTest>()
.Property(e => e.DefValue1)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);
This does not work, and in fact appears to do nothing different at all (ED: actually it does work, see point 2). EF still generates the same TSQL, providing defaults for the columns and defeating the database in the process.
Is there a flag I am missing, a configuration item I am forgetting to set, a function attribute I can use, some inherited class code I can create, to get EF to "handle DEFAULT CONSTRAINT columns correctly?"
2. Get OnModelCreating() to execute? SO: OnModelCreating not called
Janesh (below) showed me that EF will eliminate parameters from its generated TSQL INSERT command if the column is marked with DatabaseGeneratedOption.Computed
. It just wasn't working for me because apparently I was using the wrong kind of connect string (!!!).
Here's my App.config, and here's the <connectionStrings>
section where I show the "bad" and "good" connect string:
<connectionStrings>
<add name="TEST_EF6Entities_NO_WORKY" providerName="System.Data.EntityClient" connectionString="metadata=res://*/TCXModel.csdl|res://*/TCXModel.ssdl|res://*/TCXModel.msl;provider=System.Data.SqlClient;provider connection string="data source=...ConnectStringHere...;App=EntityFramework"" />
<add name="TEST_EF6Entities_IT_WORKS" providerName="System.Data.SqlClient" connectionString="data source=...ConnectStringHere...;App=EntityFramework;" />
</connectionStrings>
The difference: The one that works uses ProviderName of System.Data.SqlClient
, the one that does not work uses System.Data.EntityClient
. Apparently the SqlClient provider allows the OnModelCreating()
method to be called, which allows my use of DatabaseGeneratedOption.Computed
to have an effect.
========== NOT YET SOLVED ==========
The purpose of DEFAULT CONSTRAINTS on columns is to allow me to supply (or not supply) a value, and still end up with a valid value on the database side. I don't have to know that SQL Server is doing this, nor do I have to know what the default value is or should be. This happens completely outside my control or knowledge.
The point is, I have the option of not supplying the value. I can supply it, or I can fail to supply it, and I can do that differently for each INSERT if needed.
Using DatabaseGeneratedOption.Computed
is really not a valid option for this case because it forces a choice: "you can ALWAYS provide a value (and therefore NEVER utilize the database default mechanism), or you can NEVER provide a value (and therefore ALWAYS utilize the database default mechanism)".
Plus, that option is clearly intended to be used only on actual Computed Columns, and not for columns with DEFAULT CONSTRAINTs, because once applied, the model property effectively becomes READ-ONLY for purposes of INSERT and UPDATE - because that's how a real Computed Column would work. Obviously this stands in the way of my choice to supply or not supply a value to the database.
So, I still ask: How can I get EF to work "correctly" with database columns that have a DEFAULT CONSTRAINT defined?
This bit is the key to your question:
What I am trying to do, therefore, is to get EF NOT to include the DEFAULT CONSTRAINT columns in its INSERT TSQL if I do not explcitly set values for them in the object.
Entity Framework won't do that for you. Fields are either always computed or always included in inserts and updates. But you CAN write the classes to behave in the way that you describe. You have to set the fields (explicitly) to the default values in the constructor, or using backing fields.
public class DBUpdateTest
/* public partial class DBUpdateTest*/ //version for database first
{
private _DefValue1 = 200;
private _DefValue2 = 30;
public DbUpdateTest()
{
DefSecond = DateTime.Second;
}
public DefSecond { get; set; }
public DefValue1
{
get { return _DefValue1; }
set { _DefValue1 = value; }
}
public DefValue2
{
get { return _DefValue2; }
set { _DefValue2 = value; }
}
}
If you always insert using these classes, then you probably don't need to set the default in the database, but if you insert using sql from elsewhere, then you will have to add the default constraint to the database too
I strongly disagree that DatabaseGeneratedOption.Computed
does not help to stop sending the field in insert sql command. I tried that with minimal small example to verify and it worked.
Note: Once you applied DatabaseGeneratedOption.Computed
to any property then you want be able to specify any value from EF. i.e. you can not specify any value while inserting or updating records.
Model
public class Person
{
public int Id { get; set; }
public int SomeId { get; set; }
public string Name { get; set; }
}
Context
public class Context : DbContext
{
public DbSet<Person> People { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>().HasKey(d => d.Id);
modelBuilder.Entity<Person>()
.Property(d => d.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Person>()
.Property(d => d.SomeId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);
}
}
Migration
public partial class Initial : DbMigration
{
public override void Up()
{
CreateTable(
"dbo.People",
c => new
{
Id = c.Int(nullable: false, identity: true),
SomeId = c.Int(nullable: false, defaultValue:3), //I edited it mannually to assign default value 3.
Name = c.String(),
})
.PrimaryKey(t => t.Id);
}
public override void Down()
{
DropTable("dbo.People");
}
}
Note: I edited default value 3 to SomeId manually.
MainProgram:
static void Main(string[] args)
{
using (Context c = new Context())
{
Person p = new Person();
p.Name = "Jenish";
c.People.Add(p);
c.Database.Log = Console.WriteLine;
c.SaveChanges();
}
}
I got following query logged to my console:
Opened connection at 04/15/2015 11:32:19 AM +05:30
Started transaction at 04/15/2015 11:32:19 AM +05:30
INSERT [dbo].[People]([Name])
VALUES (@0)
SELECT [Id], [SomeId]
FROM [dbo].[People]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'Jenish' (Type = String, Size = -1)
-- Executing at 04/15/2015 11:32:20 AM +05:30
-- Completed in 3 ms with result: SqlDataReader
Committed transaction at 04/15/2015 11:32:20 AM +05:30
Closed connection at 04/15/2015 11:32:20 AM +05:30
Notice SomeId have not been passed to Insert command instead it is being selected in select command.
Someone answered this question back in 2013/2014, I believe :). The project was using EF5, ok, mind that! He told me:
- Right-click on your
.edmx
, choose "Open With", then "XML (Text) Editor", and find your columns that you set as Default Constraint inside the<!-- SSDL content -->
section of the file. - Then add
StoreGeneratedPattern="Computed"
at the end of these fields.
Done, then it worked.
Then I moved to EF6 and it STILL works, but WITHOUT the addition made before.
Now, I face a very interesting problem, using EF6, version 6.1.3.
At this very moment, I have 2 tables with a Default Constraint to set a boolean field to 1
, whether the record is ACTIVE
or INACTIVE
. Simple as that.
In one table it works, meaning: I don't have to add this StoreGeneratedPattern="Computed"
to the column inside .edmx
.
But, in the other table, IT DOESN'T; I DO have to add this StoreGeneratedPattern="Computed"
to the very same column as the table 1.
来源:https://stackoverflow.com/questions/29642068/how-to-get-ef-6-to-handle-default-constraint-on-a-database-during-insert