使用 MySqlCommand 执行存储过程时出现 SqlNullValueException

Posted

技术标签:

【中文标题】使用 MySqlCommand 执行存储过程时出现 SqlNullValueException【英文标题】:SqlNullValueException when executing a stored procedure with MySqlCommand 【发布时间】:2020-04-02 23:27:34 【问题描述】:

我正在编写一个 C# 应用程序来从 mysql 数据库中检索食谱,使用 Dapper for ORM。到目前为止,我已经用 C# 编写了带有直接查询的 DAL(我知道这是不安全的),而且效果很好。我现在已经开始过渡到带有参数的存储过程,以更好地保护数据库免受 SQL 注入,并且尽可能地使用接近最佳实践。

但是,当我使用 Dapper 的 QueryAsync<T>(这也适用于 Query<T>)和 DynamicParameters 时,我收到一个异常消息“数据为空。无法调用此方法或属性空值。”

但是,如果我将查询作为字符串文字 SQL 语句执行,或者使用字符串文字来调用存储过程,它就可以正常工作。我知道数据在那里,而不是空,因为当它直接在 MySQL 中使用我知道存在的设置 ID 号运行时它可以工作。我还尝试使用我知道存在的 id 在 C# 中运行下面列出的方法,其中一些工作正常,其中一些返回所述错误。

一旦我拨打QueryAsync<Recipe>("...") 电话,我就不知道哪里出了问题。我不知道我提供给该方法的参数是否没有传递到存储过程中,或者该过程是否返回 null,或者如果出错了其他什么。

非常感谢您在解决该呼叫可能失败的地方提供任何帮助。 我已经在底部包含了堆栈跟踪,我还无法理解。我仍然需要学习理解堆栈跟踪。

编辑: 我在 SQL Server 中重新创建了 MySql 数据库,并创建了一个新的 DAL 连接器。所有这些都完全反映了 MySql 结构和 DAL。 GetRecipeByIdAsync1(int id) 与 SQL Server 的工作方式完全一样。所以 Dapper/DynamicParameters/MySql.Data 与 MySQL 中的存储过程交互的方式肯定有一些问题

我的食谱课:

public class Recipe


        [Description("id")]
        public int Id  get; set; 

        [Description("name")]
        public string Title  get; set; 

        [Description("description")]
        public string Description  get; set; 

        [Description("source_site")]
        public string SourceSite  get; set; 

这是我在 MySQL 中的recipes 表:

recipes
=============
id (pk)     | INT          | Not Null   | Auto-Increment
name        | VARCHAR(45)  | Not Null   |
description | VARCHAR(250) | Allow Null |
source_site | VARCAHR(200) | Allow Null |

这是我用来设置自定义映射的辅助类,因此我的列不需要匹配属性名称:

public class Helper

    public static void SetTypeMaps()
    
        var recipeMap = new CustomPropertyTypeMap(typeof(Recipe),
            (type, columnName) => type.GetProperties().FirstOrDefault(prop => GetDescriptionFromAttribute(prop) == columnName));

        SqlMapper.SetTypeMap(typeof(Recipe), recipeMap);

        // Other custom mappers omitted
    

我正在使用的存储过程:

PROCEDURE `sp_recipes_GetByRecipeId`(IN RecipeId INT)
BEGIN
    SELECT r.*
    FROM recipes r
    WHERE r.id = RecipeId;
END

现在对于我在 DAL 中使用的各种版本的方法(为方便起见,我在此处对其进行了编号):

/// This does not work
public async Task<Recipe> GetRecipeByIdAsync1(int id)

    using (IDbConnection db = new MySqlConnection(GlobalConfig.CnnString("CookbookTest1")))
    
        var p = new DynamicParameters();
        p.Add("RecipeId", id, dbType: DbType.Int32, direction: ParameterDirection.Input);

        // This is the line where the exception occurs
        var result = await db.QueryAsync<Recipe>("sp_recipes_GetByRecipeId", p, commandType: CommandType.StoredProcedure); 

        return result.FirstOrDefault();
    



// This also does not work
public async Task<Recipe> GetRecipeByIdAsync2(int id)

    using (IDbConnection db = new MySqlConnection(GlobalConfig.CnnString("CookbookTest1")))
    
        // This is the line where the exception occurs
        var result = await db.QueryAsync<Recipe>("sp_recipes_GetByRecipeId", new RecipeID = id, commandType: CommandType.StoredProcedure); 

        return result.FirstOrDefault();
    



// Nor this
public async Task<Recipe> GetRecipeByIdAsync3(int id)

    using (IDbConnection db = new MySqlConnection(GlobalConfig.CnnString("CookbookTest1")))
    
        // This is the line where the exception occurs
        var result = await db.QueryAsync<Recipe>("sp_recipes_GetByRecipeId", new id, commandType: CommandType.StoredProcedure); 

        return result.FirstOrDefault();
    



// This works perfectly, but I'm not sure how safe it is
public async Task<Recipe> GetRecipeByIdAsync4(int id)

    using (IDbConnection db = new MySqlConnection(GlobalConfig.CnnString("CookbookTest1")))
    
        var result = await db.QueryAsync<Recipe>($"call sp_recipes_GetByRecipeId(id)"); 

        return result.FirstOrDefault();
    



// And of course, this works, but is horrible practice
public async Task<Recipe> GetRecipeByIdAsync5(int id)

    using (IDbConnection db = new MySqlConnection(GlobalConfig.CnnString("CookbookTest1")))
    
        var result = await db.QueryAsync<Recipe>($"SELECT * FROM recipes WHERE recipes.id = id"); 

        return result.FirstOrDefault();
    


如果有人想要连接字符串

<connectionStrings>
    <add name="CookbookTest1" connectionString="Server=localhost;Database=cookbook_test1;Uid=vs_dev;Pwd=developer;" providerName="MySql.Data"/>
</connectionStrings>

堆栈跟踪:

System.Data.SqlTypes.SqlNullValueException
  HResult=0x80131931
  Message=Data is Null. This method or property cannot be called on Null values.
  Source=MySql.Data
  StackTrace:
   at MySql.Data.MySqlClient.MySqlDataReader.GetFieldValue(Int32 index, Boolean checkNull)
   at MySql.Data.MySqlClient.MySqlDataReader.GetString(Int32 i)
   at MySql.Data.MySqlClient.MySqlDataReader.GetString(String column)
   at MySql.Data.MySqlClient.SchemaProvider.GetProcedures(String[] restrictions)
   at MySql.Data.MySqlClient.ISSchemaProvider.GetProcedures(String[] restrictions)
   at MySql.Data.MySqlClient.ISSchemaProvider.GetSchemaInternal(String collection, String[] restrictions)
   at MySql.Data.MySqlClient.SchemaProvider.GetSchema(String collection, String[] restrictions)
   at MySql.Data.MySqlClient.MySqlConnection.GetSchemaCollection(String collectionName, String[] restrictionValues)
   at MySql.Data.MySqlClient.ProcedureCache.GetProcData(MySqlConnection connection, String spName)
   at MySql.Data.MySqlClient.ProcedureCache.AddNew(MySqlConnection connection, String spName)
   at MySql.Data.MySqlClient.ProcedureCache.GetProcedure(MySqlConnection conn, String spName, String cacheKey)
   at MySql.Data.MySqlClient.StoredProcedure.GetParameters(String procName)
   at MySql.Data.MySqlClient.StoredProcedure.CheckParameters(String spName)
   at MySql.Data.MySqlClient.StoredProcedure.Resolve(Boolean preparing)
   at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader(CommandBehavior behavior)
   at MySql.Data.MySqlClient.MySqlCommand.ExecuteDbDataReader(CommandBehavior behavior)
   at System.Data.Common.DbCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
   at Dapper.SqlMapper.<QueryAsync>d__33`1.MoveNext() in C:\projects\dapper\Dapper\SqlMapper.Async.cs:line 468
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at CookbookLibrary.DataAccess.MySqlConnector.<TestStoredProcAsync>d__5.MoveNext() in C:\Users\cyclone\Desktop\VS Projects\DigitalCookbook\CookbookLibrary\DataAccess\MySqlConnector.cs:line 119
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at DigitalCookbook.ViewModel.MainWindowModel.<TestProcedure>d__38.MoveNext() in C:\Users\cyclone\Desktop\VS Projects\DigitalCookbook\DigitalCookbook\ViewModel\MainWindowModel.cs:line 228
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at DigitalCookbook.ViewModel.MainWindowModel.<<get_TestCommand>b__31_0>d.MoveNext() in C:\Users\cyclone\Desktop\VS Projects\DigitalCookbook\DigitalCookbook\ViewModel\MainWindowModel.cs:line 114

【问题讨论】:

【参考方案1】:

这看起来像是 Oracle 的 MySQL 连接器/NET(又名 MySql.Data)中的一个错误。它看起来不像我在那个错误数据库中熟悉的任何错误。它可能需要作为新问题提交。 (Bug 75301 看起来很相似,但不是很明显这是同一个问题。)

我建议切换到MySqlConnector;它是 MySQL 的备用 ADO.NET 库,与 MySQL Connector/NET 中的 Dapper 和 fixes many known bugs 具有很好的兼容性。 MySqlConnector 也有真正的异步 I/O 支持,在 Connector/NET 中为not implemented;如果您想在代码中使用QueryAsync,这将很重要。

如果您想继续使用 Oracle 的 MySQL 连接器/NET,您可以通过在连接字符串中添加 CheckParameters=false 来解决此问题。请注意,这可能是对您的代码的重大更改;如果将设置设置为 false,则必须手动确保添加到每个 CommandType.StoredProcedure MySqlCommand 的参数与数据库的顺序完全相同(因为 MySql.Data 将不再为您修复它们) .

更新:查看 Connector/NET 源代码后,您的数据库似乎有一些它不期望的数据。以下两个查询中的任何一个是否产生行?如果是,哪些值是NULL

SELECT * FROM information_schema.routines
WHERE specific_name IS NULL OR
    routine_schema IS NULL OR
    routine_name IS NULL OR
    routine_type IS NULL OR
    routine_definition IS NULL OR
    is_deterministic IS NULL OR
    sql_data_access IS NULL OR
    security_type IS NULL OR
    sql_mode IS NULL OR
    routine_comment IS NULL OR
    definer IS NULL;

SELECT * FROM mysql.proc
WHERE specific_name IS NULL OR
    db IS NULL OR
    name IS NULL OR
    type IS NULL OR
    body IS NULL OR
    is_deterministic IS NULL OR
    sql_data_access IS NULL OR
    security_type IS NULL OR
    sql_mode IS NULL OR
    comment IS NULL OR
    definer IS NULL;

您使用的是什么 MySQL 服务器(MySQL、MariaDB、Amazon Aurora)以及哪个版本?

【讨论】:

感谢您的回答!这似乎是 MySql.Data 中的一个错误。我会试一试 MySqlConnector,如果这能解决问题,我会报告。好在我已经针对接口设计了我的 DAL 类,因此它们很容易在运行时换出 很高兴报告安装 MySqlConnector 已经成功。我正在使用 Oracle MySQL,版本 8.0.18,使用 MySQL 安装程序安装在 Windows 上,开发人员默认。第一个查询不返回任何行。第二次查询失败,“表 'mysql.proc' 不存在” 我无法在本地重现您的异常,所以我在猜测(根据代码)可能是什么触发了它。如果切换到 MySqlConnector 可以解决问题,它似乎确实确认这是一个连接器/NET 错误。

以上是关于使用 MySqlCommand 执行存储过程时出现 SqlNullValueException的主要内容,如果未能解决你的问题,请参考以下文章

尝试执行存储过程时出现意外错误

尝试使用 .NET 5.0 Web 应用程序执行存储过程时出现转换错误

尝试使用 .NET Core 执行存储过程时出现“ORA-03115:不支持的网络数据类型或表示”

执行存储过程“PLS-00103”时出现此错误

示例数据 - “执行包含更新和插入语句的存储过程时出现问题”

oracle 执行存储过程时出现卡死