前端时间在.NetCore项目中使用SqlSugar ORM框架(引用sqlSugarCore依赖包)的时候遇到了一个奇葩问题:对表进行数据更新操作的时候,报错 “ You cannot have no primary key and no conditions ”,即没有主键和条件的报错。
由于当时采用的更新方式是UpdateColumns()+WhereColumns(),所以排除了没有条件的问题,定位问题:缺少主键。
SqlSugar框架在初始化DB对象时,为我们提供了两种获取主键的方式:
SqlSugarClient属性:InitKeyType
1.SysTable 表示通过数据库系统表查询表主键,这种需要数据库最高权限,并且数据库表需有主键或能获取到主键。
2.Attribute 表示通过实体 [SugarColumn(IsPrimaryKey = true)]标签获取主键,而无需通过数据库表。
在项目中我们并未指定InitKeyType属性值,也就是默认使用了 SysTable模式。
于是,首先查看数据表设计结构,发现表结构是有主键的,当时就懵逼了。。。赶紧去看了一下SqlSugar源码,发现ORM框架中是通过如下方式获取主键列的。
protected override string GetColumnInfosByTableNameSql { get { string sql = @"SELECT sysobjects.name AS TableName, syscolumns.Id AS TableId, syscolumns.name AS DbColumnName, systypes.name AS DataType, syscolumns.length AS [Length], sys.extended_properties.[value] AS [ColumnDescription], syscomments.text AS DefaultValue, syscolumns.isnullable AS IsNullable, columnproperty(syscolumns.id,syscolumns.name,'IsIdentity')as IsIdentity, (CASE WHEN EXISTS ( select 1 from sysindexes i join sysindexkeys k on i.id = k.id and i.indid = k.indid join sysobjects o on i.id = o.id join syscolumns c on i.id=c.id and k.colid = c.colid where o.xtype = 'U' and exists(select 1 from sysobjects where xtype = 'PK' and name = i.name) and o.name=sysobjects.name and c.name=syscolumns.name ) THEN 1 ELSE 0 END) AS IsPrimaryKey FROM syscolumns INNER JOIN systypes ON syscolumns.xtype = systypes.xtype LEFT JOIN sysobjects ON syscolumns.id = sysobjects.id LEFT OUTER JOIN sys.extended_properties ON (sys.extended_properties.minor_id = syscolumns.colid AND sys.extended_properties.major_id = syscolumns.id) LEFT OUTER JOIN syscomments ON syscolumns.cdefault = syscomments.id WHERE syscolumns.id IN (SELECT id FROM sysobjects WHERE xtype IN('u', 'v') ) AND (systypes.name <> 'sysname') AND sysobjects.name='实际传入参数:表名称' AND systypes.name<>'geometry' AND systypes.name<>'geography' ORDER BY syscolumns.colid"; return sql; } }
数据库中直接查询如上sql语句,发现并未找到主键列(IsPrimaryKey=1),神奇了。。。表结构明明有主键却查不到,折腾了一会儿才知道,项目中用的数据表竟然是同义词!!!
同义词是指向其他数据库表的别名,在当前数据库根本无法获取同义词对应的表结构属性,到这里也就恍然大悟了,原来是同义词导致框架SysTable模式下获取不到表主键。
执行框架中的如上Sql,还有其他几种情况会导致无法获取到主键:
1)表没建主键
2)表使用的同义词
3)当前数据库用户没有查询系统表sys级别权限
4)数据库排序规则:_CI(CS) 是否区分大小写,CI不区分,CS区分
如果设置的区分大小写,则上面sql中的如下部分将受影响,导致找不到数据。数据库实际存的值是大写的U,V
SELECT id FROM sysobjects WHERE xtype IN('u', 'v')
解决方案:
1)SqlSugar也支持手写sql语句,直接手写更新sql即可。
2)初始化SqlSugarClient DB对象时,选择InitKeyType.Attribute模式,通过实体特性获取主键。
后续:
项目中问题虽然解决了,但是后来还是比较好奇:使用了WhereColumns()条件列,为什么还要去找主键呢?直接根据条件列更新不可以么???于是决定一探究竟。。。
定位到获取主键的方法:
private List<string> GetPrimaryKeys() { if (this.WhereColumnList.HasValue()) { return this.WhereColumnList; } if (this.Context.IsSystemTablesConfig) { return this.Context.DbMaintenance.GetPrimaries(this.Context.EntityMaintenance.GetTableName(this.EntityInfo.EntityName)); } else { return this.EntityInfo.Columns.Where(it => it.IsPrimarykey).Select(it => it.DbColumnName).ToList(); } }
我们会发现方法中,首先判断的是this.WhereColumnList是否有数据,如果有数据就不会再通过下面的两种SysTable和Attribute模式找主键;
这里的WhereColumnList数据来自我们的更新条件WhereColumns()
public IUpdateable<T> WhereColumns(string[] columnNames) { if (this.WhereColumnList == null) this.WhereColumnList = new List<string>(); foreach (var columnName in columnNames) { this.WhereColumnList.Add(columnName); } return this; }
显然当前对象的WhereColumnList并没有获取成功,于是继续向上层探索
定位到异步执行方法ExecuteCommandAsync:
public Task<int> ExecuteCommandAsync() { Task<int> result = new Task<int>(() => { IUpdateable<T> asyncUpdateable = CopyUpdateable(); return asyncUpdateable.ExecuteCommand(); }); TaskStart(result); return result; }
到这里我们发现实际上异步的时候重新复制了一下当前Updateable对象,即CopyUpdateable(),接下来我们去看看这个方法到底做了什么操作?
private IUpdateable<T> CopyUpdateable() { var asyncContext = this.Context.Utilities.CopyContext(true); asyncContext.CurrentConnectionConfig.IsAutoCloseConnection = true; asyncContext.IsAsyncMethod = true; var asyncUpdateable = asyncContext.Updateable<T>(this.UpdateObjs); var asyncUpdateableBuilder = asyncUpdateable.UpdateBuilder; asyncUpdateableBuilder.DbColumnInfoList = this.UpdateBuilder.DbColumnInfoList; asyncUpdateableBuilder.IsNoUpdateNull = this.UpdateBuilder.IsNoUpdateNull; asyncUpdateableBuilder.Parameters = this.UpdateBuilder.Parameters; asyncUpdateableBuilder.sql = this.UpdateBuilder.sql; asyncUpdateableBuilder.WhereValues = this.UpdateBuilder.WhereValues; asyncUpdateableBuilder.TableWithString = this.UpdateBuilder.TableWithString; asyncUpdateableBuilder.TableName = this.UpdateBuilder.TableName; asyncUpdateableBuilder.PrimaryKeys = this.UpdateBuilder.PrimaryKeys; asyncUpdateableBuilder.IsOffIdentity = this.UpdateBuilder.IsOffIdentity; asyncUpdateableBuilder.SetValues = this.UpdateBuilder.SetValues; if (this.RemoveCacheFunc != null) { asyncUpdateable.RemoveDataCache(); } return asyncUpdateable; }
从上面这个方法中,我们能够看出异步的更新对象asyncUpdateable并没有绑定WhereColumnList,所以在调用asyncUpdateable.ExecuteCommand()时,GetPrimaryKeys方法中当前对象的WhereColumnList没有值
public virtual int ExecuteCommand() { PreToSql(); AutoRemoveDataCache(); Check.Exception(UpdateBuilder.WhereValues.IsNullOrEmpty() && GetPrimaryKeys().IsNullOrEmpty(), "You cannot have no primary key and no conditions"); string sql = UpdateBuilder.ToSqlString(); ValidateVersion(); RestoreMapping(); Before(sql); var result = this.Ado.ExecuteCommand(sql, UpdateBuilder.Parameters == null ? null : UpdateBuilder.Parameters.ToArray()); After(sql); return result; }
针对如上框架问题,我们可以:
1)IUpdateable接口添加属性:List<string> WhereColumnList { get; set; }
2)CopyUpdateable()方法中,给异步更新对象asyncUpdateable绑定WhereColumnList。
private IUpdateable<T> CopyUpdateable() { var asyncContext = this.Context.Utilities.CopyContext(true); asyncContext.CurrentConnectionConfig.IsAutoCloseConnection = true; asyncContext.IsAsyncMethod = true; var asyncUpdateable = asyncContext.Updateable<T>(this.UpdateObjs); var asyncUpdateableBuilder = asyncUpdateable.UpdateBuilder; asyncUpdateableBuilder.DbColumnInfoList = this.UpdateBuilder.DbColumnInfoList; asyncUpdateableBuilder.IsNoUpdateNull = this.UpdateBuilder.IsNoUpdateNull; asyncUpdateableBuilder.Parameters = this.UpdateBuilder.Parameters; asyncUpdateableBuilder.sql = this.UpdateBuilder.sql; asyncUpdateableBuilder.WhereValues = this.UpdateBuilder.WhereValues; asyncUpdateableBuilder.TableWithString = this.UpdateBuilder.TableWithString; asyncUpdateableBuilder.TableName = this.UpdateBuilder.TableName; asyncUpdateableBuilder.PrimaryKeys = this.UpdateBuilder.PrimaryKeys; asyncUpdateableBuilder.IsOffIdentity = this.UpdateBuilder.IsOffIdentity; asyncUpdateableBuilder.SetValues = this.UpdateBuilder.SetValues; asyncUpdateable.WhereColumnList = this.WhereColumnList; if (this.RemoveCacheFunc != null) { asyncUpdateable.RemoveDataCache(); } return asyncUpdateable; }
至此,关于框架中主键的问题终于搞清楚来龙去脉了。。。
还没结束,哈哈!!!
既然我们都发现了这个问题,你说人家框架发布者能没发现么。。。于是,在sqlSugarCore4.9.9.9版本之后,作者对异步更新操作ExecuteCommandAsync这一问题进行了修复。
我们简单来看看是怎么优化的:
public async Task<int> ExecuteCommandAsync() { string sql = _ExecuteCommand(); var result =await this.Ado.ExecuteCommandAsync(sql, UpdateBuilder.Parameters == null ? null : UpdateBuilder.Parameters.ToArray()); After(sql); return result; }
private string _ExecuteCommand() { PreToSql(); AutoRemoveDataCache(); Check.Exception(UpdateBuilder.WhereValues.IsNullOrEmpty() && GetPrimaryKeys().IsNullOrEmpty(), "You cannot have no primary key and no conditions"); string sql = UpdateBuilder.ToSqlString(); ValidateVersion(); RestoreMapping(); Before(sql); return sql; }
我们会发现ExecuteCommandAsync异步方法中,不再通过复制异步更新对象的方式,而是直接采用了当前调用的更新对象UpdateableProvider,该对象已实现WhereColumnList属性赋值。
好了,下次大家如果遇到同样的问题,可以直接升级到最新版本;当然,也可以参考下文章中说的几种简单的方案哦。