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

跳躍的鍵盤手發表於2020-07-16

前端時間在.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屬性賦值。

 

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

相關文章