为 LINQ 绑定的数据设置默认值






3.53/5 (8投票s)
如何阻止 LINQ 尝试将 null 数据插入您的数据库。

引言
对 Microsoft 新的 LINQ 技术所展现出的 ORM 行为印象深刻,我决定在下一个项目中使用它之前进行一个开发实验。尽管 LINQ 绝对是未来的方向,但似乎存在一些不足之处,需要一些变通。在这里,我将写下一种相当棘手的行为,其中 LINQ 对象的属性被设置为 null
值,导致数据库拒绝更新和插入不允许 null
数据的列。
Using the Code
在使用 ASP.NET DetailsView
控件和底层的 LINQ DataSource
时,我遇到了一个问题。问题是:当我使用 LINQ 加载对象时,而不是使用从数据库派生的属性的默认值,每个属性的默认值都是 null
。作为一个好的 DBA,我从不(没有充分理由)在数据库中留下 null
,所以我的表总是为每个列分配默认值。然而,当 LINQ 对象被发送回数据库时,操作会失败,因为数据库拒绝了 LINQ 层试图插入的 null
数据。
我认为这是设计上的一个缺陷。默认情况下,LINQ 对象应该使用映射回底层数据存储中使用的默认值的属性进行初始化。
无论如何,最好通过一个简单的例子来演示这个问题,然后展示一种变通方法……
我有一个简单的数据表……

CREATE TABLE [dbo].[Table1](
[Col1] [int] IDENTITY(1,1) NOT NULL,
[Col2] [nvarchar](50) NOT NULL CONSTRAINT [DF_Table1_Col2] DEFAULT (''),
[Col3] [int] NOT NULL CONSTRAINT [DF_Table1_col3] DEFAULT ((0)),
[Col4] [datetime] NOT NULL CONSTRAINT [DF_Table1_col4] DEFAULT (getdate()),
CONSTRAINT [PK_Table1] PRIMARY KEY CLUSTERED
(
[Col1] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = _
OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
……以及一个位于该表之上的 LINQ 类

接下来,我在项目的默认页面 (default.aspx) 中添加一个 LinqDataSource
,并设置一个基于底层数据主键的过滤参数,并告知 datasource
我也希望能够编辑数据。
<asp:LinqDataSource ID="LinqDataSource1" runat="server"
ContextTypeName="DataClassesDataContext" EnableUpdate="True"
TableName="Table1s" Where="Col1 == @Col1">
<WhereParameters>
<asp:QueryStringParameter Name="Col1" QueryStringField="Col1" Type="Int32" />
</WhereParameters>
</asp:LinqDataSource>
作为数据的一个 UI,我将一个 DetailsView
控件拖到页面上,并将其绑定到 LinqDataSource
。
<asp:DetailsView ID="DetailsView1" runat="server" AutoGenerateRows="False"
DataKeyNames="Col1" DataSourceID="LinqDataSource1" Height="50px" Width="125px">
<Fields>
<asp:BoundField DataField="Col1" HeaderText="Col1" InsertVisible="False"
ReadOnly="True" SortExpression="Col1" />
<asp:BoundField DataField="Col2" HeaderText="Col2" SortExpression="Col2" />
<asp:BoundField DataField="Col3" HeaderText="Col3" SortExpression="Col3" />
<asp:BoundField DataField="Col4" HeaderText="Col4" SortExpression="Col4" />
<asp:CommandField ShowEditButton="True" />
</Fields>
</asp:DetailsView>
我手动向我的数据表中添加一条记录,以便有一些数据可以编辑

请注意,Col2
是空的,但根据定义,它应该是一个空的 string
。
[Col2] [nvarchar](50) NOT NULL CONSTRAINT [DF_Table1_Col2] DEFAULT (''),
让我们尝试编辑这一行的数据(确保提供查询 string
参数 Col1=1
)

所以,我将 Col2
留空,然后点击更新链接。
Cannot insert the value NULL into column 'Col2', table 'TestDB.dbo.Table1';
column does not allow nulls. UPDATE fails.
The statement has been terminated.
Description: An unhandled exception occurred during the execution of the
current web request. Please review the stack trace for more information about
the error and where it originated in the code.
Exception Details: System.Data.SqlClient.SqlException:
Cannot insert the value NULL into column 'Col2', table 'TestDB.dbo.Table1';
column does not allow nulls. UPDATE fails.
The statement has been terminated.
Source Error:
An unhandled exception was generated during the execution of the current web request.
Information regarding the origin and location of the exception can be identified
using the exception stack trace below.
Stack Trace:
[SqlException (0x80131904): Cannot insert the value NULL into column 'Col2',
table 'TestDB.dbo.Table1'; column does not allow nulls. UPDATE fails.
The statement has been terminated.]
System.Data.SqlClient.SqlConnection.OnError_
(SqlException exception, Boolean breakConnection) +925466
System.Data.SqlClient.SqlInternalConnection.OnError_
(SqlException exception, Boolean breakConnection) +800118
System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning_
(TdsParserStateObject stateObj) +186
System.Data.SqlClient.TdsParser.Run_
(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, _
BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj) +1932
System.Data.SqlClient.SqlCommand.FinishExecuteReader_
(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString) +149
System.Data.SqlClient.SqlCommand.RunExecuteReaderTds_
(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, _
Boolean async) +1005
System.Data.SqlClient.SqlCommand.RunExecuteReader_
(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, _
String method, DbAsyncResult result) +132
System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery_
(DbAsyncResult result, String methodName, Boolean sendToPipe) +149
System.Data.SqlClient.SqlCommand.ExecuteNonQuery() +135
System.Data.Linq.SqlClient.SqlProvider.Execute_
(Expression query, QueryInfo queryInfo, IObjectReaderFactory factory, _
Object[] parentArgs, Object[] userArgs, ICompiledSubQuery[] subQueries, _
Object lastResult) +229
System.Data.Linq.SqlClient.SqlProvider.ExecuteAll_
(Expression query, QueryInfo[] queryInfos, IObjectReaderFactory factory, _
Object[] userArguments, ICompiledSubQuery[] subQueries) +129
System.Data.Linq.SqlClient.SqlProvider.System.Data.Linq.Provider.IProvider.Execute_
(Expression query) +878
System.Data.Linq.StandardChangeDirector.DynamicUpdate(TrackedObject item) +127
System.Data.Linq.StandardChangeDirector.Update(TrackedObject item) +168
System.Data.Linq.ChangeProcessor.SubmitChanges(ConflictMode failureMode) +433
System.Data.Linq.DataContext.SubmitChanges(ConflictMode failureMode) +1155
System.Data.Linq.DataContext.SubmitChanges() +82
System.Web.UI.WebControls.LinqToSqlWrapper.SubmitChanges(DataContext dataContext) +9
System.Web.UI.WebControls.LinqDataSourceView.UpdateDataObject_
(Object dataContext, Object table, Object oldDataObject, Object newDataObject) +106
System.Web.UI.WebControls.LinqDataSourceView.ExecuteUpdate_
(IDictionary keys, IDictionary values, IDictionary oldValues) +326
System.Web.UI.DataSourceView.Update_
(IDictionary keys, IDictionary values, IDictionary oldValues, _
DataSourceViewOperationCallback callback) +78
System.Web.UI.WebControls.DetailsView.HandleUpdate_
(String commandArg, Boolean causesValidation) +1152
System.Web.UI.WebControls.DetailsView.HandleEvent_
(EventArgs e, Boolean causesValidation, String validationGroup) +440
System.Web.UI.WebControls.DetailsView.OnBubbleEvent(Object source, EventArgs e) +95
System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +35
System.Web.UI.WebControls.DetailsViewRow.OnBubbleEvent_
(Object source, EventArgs e) +109
System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +35
System.Web.UI.WebControls.LinkButton.OnCommand(CommandEventArgs e) +115
System.Web.UI.WebControls.LinkButton.RaisePostBackEvent(String eventArgument) +132
System.Web.UI.WebControls.LinkButton.System.Web.UI.IPostBackEventHandler._
RaisePostBackEvent(String eventArgument) +7
System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, _
String eventArgument) +11
System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData) +177
System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, _
Boolean includeStagesAfterAsyncPoint) +1746
________________________________________
Version Information: Microsoft .NET Framework Version:2.0.50727.1433;
ASP.NET Version:2.0.50727.1433
很烦人,对吧?
首先,这不是一个新插入,所以当 LINQ 对象加载该行的数据时,Col2
应该被设置为从数据库中的 col2
获取的值,这是一个空的 string
。其次,如果是一个新插入,那么我希望使用表中定义中为每个列设置的默认值,而不是 null
。
所以,让我们尝试一个新的插入。我修改了 DataSource
和 DetailsView
以支持插入和更新
<asp:LinqDataSource ID="LinqDataSource1" runat="server"
ContextTypeName="DataClassesDataContext" EnableInsert="True"
EnableUpdate="True" TableName="Table1s" Where="Col1 == @Col1">
<WhereParameters>
<asp:QueryStringParameter Name="Col1" QueryStringField="Col1"
Type="Int32" />
</WhereParameters>
</asp:LinqDataSource>
<asp:DetailsView ID="DetailsView1" runat="server" AutoGenerateRows="False"
DataKeyNames="Col1" DataSourceID="LinqDataSource1"
Height="50px" Width="125px">
<Fields>
<asp:BoundField DataField="Col1" HeaderText="Col1" InsertVisible="False"
ReadOnly="True" SortExpression="Col1" />
<asp:BoundField DataField="Col2" HeaderText="Col2"
SortExpression="Col2" />
<asp:BoundField DataField="Col3" HeaderText="Col3"
SortExpression="Col3" />
<asp:BoundField DataField="Col4" HeaderText="Col4"
SortExpression="Col4" />
<asp:CommandField ShowEditButton="True" ShowInsertButton="True" />
</Fields>
</asp:DetailsView>
现在我尝试一个全新的插入,将 DetailsView
上的每个列都留空

然后点击插入链接
SqlDateTime overflow. Must be between 1/1/1753 12:00:00 AM and 12/31/9999 11:59:59 PM.
Description: An unhandled exception occurred during the execution of
the current web request. Please review the stack trace for more information
about the error and where it originated in the code.
Exception Details: System.Data.SqlTypes.SqlTypeException: SqlDateTime overflow.
Must be between 1/1/1753 12:00:00 AM and 12/31/9999 11:59:59 PM.
Source Error:
An unhandled exception was generated during the execution of the current web request.
Information regarding the origin and location of the exception can be identified
using the exception stack trace below.
Stack Trace:
[SqlTypeException: SqlDateTime overflow.
Must be between 1/1/1753 12:00:00 AM and 12/31/9999 11:59:59 PM.]
System.Data.SqlTypes.SqlDateTime.FromTimeSpan(TimeSpan value) +653497
System.Data.SqlTypes.SqlDateTime.FromDateTime(DateTime value) +150
System.Data.SqlTypes.SqlDateTime..ctor(DateTime value) +13
System.Data.SqlClient.MetaType.FromDateTime(DateTime dateTime, Byte cb) +56
System.Data.SqlClient.TdsParser.WriteValue_
(Object value, MetaType type, Byte scale, Int32 actualLength, _
Int32 encodingByteSize, Int32 offset, _
TdsParserStateObject stateObj) +519
System.Data.SqlClient.TdsParser.TdsExecuteRPC_
(_SqlRPC[] rpcArray, Int32 timeout, Boolean inSchema, _
SqlNotificationRequest notificationRequest, TdsParserStateObject stateObj, _
Boolean isCommandProc) +4163
System.Data.SqlClient.SqlCommand.RunExecuteReaderTds_
(CommandBehavior cmdBehavior, RunBehavior runBehavior, _
Boolean returnStream, Boolean async) +1005
System.Data.SqlClient.SqlCommand.RunExecuteReader_
(CommandBehavior cmdBehavior, RunBehavior runBehavior, _
Boolean returnStream, String method, DbAsyncResult result) +132
System.Data.SqlClient.SqlCommand.RunExecuteReader_
(CommandBehavior cmdBehavior, RunBehavior runBehavior, _
Boolean returnStream, String method) +32
System.Data.SqlClient.SqlCommand.ExecuteReader_
(CommandBehavior behavior, String method) +122
System.Data.SqlClient.SqlCommand.ExecuteDbDataReader(CommandBehavior behavior) +12
System.Data.Common.DbCommand.ExecuteReader() +9
System.Data.Linq.SqlClient.SqlProvider.Execute_
(Expression query, QueryInfo queryInfo, IObjectReaderFactory factory, _
Object[] parentArgs, Object[] userArgs, ICompiledSubQuery[] subQueries, _
Object lastResult) +903
System.Data.Linq.SqlClient.SqlProvider.ExecuteAll_
(Expression query, QueryInfo[] queryInfos, IObjectReaderFactory factory, _
Object[] userArguments, ICompiledSubQuery[] subQueries) +129
System.Data.Linq.SqlClient.SqlProvider.System.Data.Linq.Provider.IProvider.Execute_
(Expression query) +878
System.Data.Linq.StandardChangeDirector.DynamicInsert(TrackedObject item) +261
System.Data.Linq.StandardChangeDirector.Insert(TrackedObject item) +168
System.Data.Linq.ChangeProcessor.SubmitChanges(ConflictMode failureMode) +250
System.Data.Linq.DataContext.SubmitChanges(ConflictMode failureMode) +1155
System.Data.Linq.DataContext.SubmitChanges() +82
System.Web.UI.WebControls.LinqToSqlWrapper.SubmitChanges(DataContext dataContext) +9
System.Web.UI.WebControls.LinqDataSourceView.InsertDataObject_
(Object dataContext, Object table, Object newDataObject) +82
System.Web.UI.WebControls.LinqDataSourceView.ExecuteInsert(IDictionary values) +267
System.Web.UI.DataSourceView.Insert_
(IDictionary values, DataSourceViewOperationCallback callback) +72
System.Web.UI.WebControls.DetailsView.HandleInsert_
(String commandArg, Boolean causesValidation) +390
System.Web.UI.WebControls.DetailsView.HandleEvent_
(EventArgs e, Boolean causesValidation, String validationGroup) +602
System.Web.UI.WebControls.DetailsView.OnBubbleEvent(Object source, EventArgs e) +95
System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +35
System.Web.UI.WebControls.DetailsViewRow.OnBubbleEvent_
(Object source, EventArgs e) +109
System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +35
System.Web.UI.WebControls.LinkButton.OnCommand(CommandEventArgs e) +115
System.Web.UI.WebControls.LinkButton.RaisePostBackEvent(String eventArgument) +132
System.Web.UI.WebControls.LinkButton.System.Web.UI.IPostBackEventHandler._
RaisePostBackEvent(String eventArgument) +7
System.Web.UI.Page.RaisePostBackEvent_
(IPostBackEventHandler sourceControl, String eventArgument) +11
System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData) +177
System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, _
Boolean includeStagesAfterAsyncPoint) +1746
________________________________________
Version Information: Microsoft .NET Framework Version:2.0.50727.1433;
ASP.NET Version:2.0.50727.1433
嗯,我以为我在表定义中将该默认值设置为了 GetDate()
。
所以我在想,一定有什么方法可以告诉我的 LINQ 层让数据库处理默认值,停止搞砸事情。
如果我查看 LINQ 设计器代码隐藏文件,我可以看到在每个与底层数据库相关的属性索引器上方设置了 LINQ 属性,这里以 Col2
为例
[Column(Storage="_Col2", DbType="NVarChar(50) NOT NULL", CanBeNull=false)]
public string Col2
{
get
{
return this._Col2;
}
set
{
if ((this._Col2 != value))
{
this.OnCol2Changing(value);
this.SendPropertyChanging();
this._Col2 = value;
this.SendPropertyChanged("Col2");
this.OnCol2Changed();
}
}
}
所以它知道它不能是 null
,但其属性的默认值却是 null
。抱歉,这很不聪明。我查看了其他可以设置的标志,也许可以强制 LINQ 使用数据库默认值。智能感知给了我以下选项:

IsDbGenerated
看起来是一个相当不错的猜测,所以我尝试了一下
[Column(Storage="_Col2", DbType="NVarChar(50) NOT NULL", _
CanBeNull=false, IsDbGenerated=true)]
我再次尝试编辑我的数据,但在更新时出现以下错误
Value of member 'Col2' of an object of type 'Table1' changed.
A member that is computed or generated by the database cannot be changed.
Description: An unhandled exception occurred during the execution of the
current web request. Please review the stack trace for more information
about the error and where it originated in the code.
Exception Details: System.InvalidOperationException:
Value of member 'Col2' of an object of type 'Table1' changed.
A member that is computed or generated by the database cannot be changed.
Source Error:
An unhandled exception was generated during the execution of the current web request.
Information regarding the origin and location of the exception can be identified
using the exception stack trace below.
Stack Trace:
[InvalidOperationException: Value of member 'Col2' of an object of type 'Table1' changed.
A member that is computed or generated by the database cannot be changed.]
System.Data.Linq.ChangeProcessor.CheckForInvalidChanges(TrackedObject tracked) +291
System.Data.Linq.ChangeProcessor.SubmitChanges(ConflictMode failureMode) +415
System.Data.Linq.DataContext.SubmitChanges(ConflictMode failureMode) +1155
System.Data.Linq.DataContext.SubmitChanges() +82
System.Web.UI.WebControls.LinqToSqlWrapper.SubmitChanges(DataContext dataContext) +9
System.Web.UI.WebControls.LinqDataSourceView.UpdateDataObject_
(Object dataContext, Object table, Object oldDataObject, _
Object newDataObject) +106
System.Web.UI.WebControls.LinqDataSourceView.ExecuteUpdate_
(IDictionary keys, IDictionary values, IDictionary oldValues) +326
System.Web.UI.DataSourceView.Update(IDictionary keys, IDictionary values, _
IDictionary oldValues, DataSourceViewOperationCallback callback) +78
System.Web.UI.WebControls.DetailsView.HandleUpdate_
(String commandArg, Boolean causesValidation) +1152
System.Web.UI.WebControls.DetailsView.HandleEvent_
(EventArgs e, Boolean causesValidation, String validationGroup) +440
System.Web.UI.WebControls.DetailsView.OnBubbleEvent(Object source, EventArgs e) +95
System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +35
System.Web.UI.WebControls.DetailsViewRow.OnBubbleEvent_
(Object source, EventArgs e) +109
System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +35
System.Web.UI.WebControls.LinkButton.OnCommand(CommandEventArgs e) +115
System.Web.UI.WebControls.LinkButton.RaisePostBackEvent(String eventArgument) +132
System.Web.UI.WebControls.LinkButton.System.Web.UI.IPostBackEventHandler._
RaisePostBackEvent(String eventArgument) +7
System.Web.UI.Page.RaisePostBackEvent_
(IPostBackEventHandler sourceControl, String eventArgument) +11
System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData) +177
System.Web.UI.Page.ProcessRequestMain_
(Boolean includeStagesBeforeAsyncPoint, _
Boolean includeStagesAfterAsyncPoint) +1746
________________________________________
Version Information: Microsoft .NET Framework Version:2.0.50727.1433;
ASP.NET Version:2.0.50727.1433
现在,我真的开始感到沮丧了。我花了几个小时在这上面,并且在网上搜索解决方案,希望能找到一个不需要我编写代码的办法。到目前为止还没有成功。
所以,这是我的变通方法
我绝对不想编写任何显式绑定到任何表定义的代码,因为如果表发生更改,它肯定会成为维护问题。我也不想编写部分方法来检查 LINQ 属性在设置时的 null
值,因为这意味着更多的维护代码。所以,我认为最好的办法是挂钩 DetailsView
控件的更新事件,并确保在尝试将 LINQ 对象渗透回数据库之前正确初始化它。我修改了 LinqDataSource
控件,以便在 OnUpdating
和 OnInserting
方法触发时调用一个检查方法,如下所示:
<asp:LinqDataSource ID="LinqDataSource1" runat="server"
ContextTypeName="DataClassesDataContext" EnableInsert="True"
EnableUpdate="True" TableName="Table1s" Where="Col1 == _
@Col1" OnUpdating="SetDefaultUpdateValues" _
OnInserting="SetDefaultInsertValues">
<WhereParameters>
<asp:QueryStringParameter Name="Col1" QueryStringField="Col1" _
Type="Int32" />
</WhereParameters>
</asp:LinqDataSource>
后台代码如下:
using System;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using System.Reflection;
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
private void SetDefaults(object LinqObj)
{
// get the properties of the LINQ Object
PropertyInfo[] props = LinqObj.GetType().GetProperties();
// iterate through each property of the class
foreach (PropertyInfo prop in props)
{
// attempt to discover any metadata relating to underlying data columns
try
{
// get any column attributes created by the Linq designer
object[] customAttributes = prop.GetCustomAttributes
(typeof(System.Data.Linq.Mapping.ColumnAttribute), false);
// if the property has an attribute letting us know that
// the underlying column data cannot be null
if (((System.Data.Linq.Mapping.ColumnAttribute)
(customAttributes[0])).DbType.ToLower().IndexOf("not null") != -1)
{
// if the current property is null or Linq has set a date time
// to its default '01/01/0001 00:00:00'
if (prop.GetValue(LinqObj, null) == null || prop.GetValue(LinqObj,
null).ToString() == (new DateTime(1, 1, 1, 0, 0, 0)).ToString())
{
// set the default values here : could re-query the database,
// but would be expensive so just use defaults coded here
switch (prop.PropertyType.ToString())
{
// System.String / NVarchar
case "System.String":
prop.SetValue(LinqObj, String.Empty, null);
break;
case "System.Int32":
case "System.Int64":
case "System.Int16":
prop.SetValue(LinqObj, 0, null);
break;
case "System.DateTime":
prop.SetValue(LinqObj, DateTime.Now, null);
break;
}
}
}
}
catch
{
// could do something here ...
}
}
}
// set LINQ objects default values for new insert
protected void SetDefaultInsertValues(object sender, LinqDataSourceInsertEventArgs e)
{
SetDefaults(e.NewObject);
}
// set LINQ objects default values for update
protected void SetDefaultUpdateValues(object sender, LinqDataSourceUpdateEventArgs e)
{
SetDefaults(e.NewObject);
}
}
关注点
上述方法的不足之处在于,代码中设置的默认值不是从数据库派生的。这可以纠正,但成本很高。遗憾的是,LINQ 设计器在创建类时没有将默认列值作为元数据添加。我猜想 Microsoft 很快会找到一个解决方案。