前端时间在.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属性赋值。

好了,下次大家如果遇到同样的问题,可以直接升级到最新版本;当然,也可以参考下文章中说的几种简单的方案哦。

07-16 17:53