1. 程式人生 > 實用技巧 >記SqlSugar ORM框架之找不到主鍵問題

記SqlSugar ORM框架之找不到主鍵問題

前端時間在.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是否有資料,如果有資料就不會再通過下面的兩種SysTableAttribute模式找主鍵;

這裡的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屬性賦值。

好了,下次大家如果遇到同樣的問題,可以直接升級到最新版本;當然,也可以參考下文章中說的幾種簡單的方案哦。