前端時間在.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屬性賦值。
好了,下次大家如果遇到同樣的問題,可以直接升級到最新版本;當然,也可以參考下文章中說的幾種簡單的方案哦。