• 记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属性赋值。

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

  • 相关阅读:
    Alpha版本冲刺(一)
    福大软工 · 第七次作业
    福大软工 · 第八次作业(课堂实战)- 项目UML设计(团队)
    福大软工1816 · 第六次作业
    福大软工1816 · 第五次作业
    福大软工1816
    福大软工1816 · 第一次作业
    Python学习
    整理一下软工实践这门课的一些链接
    个人作业——软件工程实践总结作业
  • 原文地址:https://www.cnblogs.com/chenxf1117/p/13299626.html
Copyright © 2020-2023  润新知