DevArt MySql EF6 驱动程序:可空 BIT(1) 列的空值行在 'COLUMN_NAME ?? false' 中始终评估为真
DevArt MySql EF6 Drivers: Nullable BIT(1) columns have their null-value rows always evaluated to true for 'COLUMN_NAME ?? false'
雇佣的图书馆员:
Devart.Data.dll => 5.0.1878.0
Devart.Data.MySql.dll => 8.10.1086.0
Devart.Data.MySql.Design.dll => 8.10.1086.0
Devart.Data.MySql.Entity.EF6.dll => 8.10.1086.0
EntityFramework => 6.2.0
我们的目标是来自 ASP.NET MVC5 项目的 .net4.7.1。我们的存储库构建在 EF .edmx(数据库优先方法)之上。我们的查询如下所示:
var results = _db.NB_FILTERS
.Select(x => new ReportFiltersDTO { IsHiddenSubFilter = x.NFS_SUBFILTER_YN ?? false })
.ToList();
NFS_SUBFILTER_YN 在我们的 .edmx 中被声明为布尔值:
<Property Name="NFS_SUBFILTER_YN" Type="boolean" />
NB_FILTERS ddl 是这样的:
CREATE TABLE NB_FILTERS (
[...]
NFS_SUBFILTER_YN BIT(1) NULL,
[...]
)
在这种情况下,如果某些行为空,则给定的 linq 表达式 returns 'true' 代替 'false'。罪魁祸首似乎在自动生成的 sql:
actual => CASE WHEN Extent1.NFS_SUBFILTER_YN IS NULL THEN 0 ELSE Extent1.NFS_SUBFILTER_YN END
corrected => CASE WHEN Extent1.NFS_SUBFILTER_YN IS NULL THEN 1 ELSE CAST(Extent1.NFS_SUBFILTER_YN AS SIGNED) END
如何在不更改基础表的情况下解决此错误and/or linq 语句本身,直到 Devart 推出实际修复程序?
旁注:
对于那些感兴趣的人,我们已经将此问题通知了开发人员,希望它会在某个时候得到解决:
有趣的是,当涉及的列是可为 null 的 decimal(x,y) 列时,此类查询有效 [自动生成的 sql 正确地使用了 CAST(... AS SIGNED)]。似乎以某种方式从符合此类处理条件的类型列表中省略了 bit(x)。
作为临时 stop-gap 解决方案,我们选择在 mysql 上使用 EF6 拦截器来检测和更正针对布尔列的 on-the-fly sql 语句(幸好大多数如果不是我们所有的布尔列都有 _YN 后缀,这有很大帮助):
using System.Data;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
using System.Text.RegularExpressions;
namespace Some.Namespace
{
//to future maintainers the devart mysql drivers for ef6 ver8.10.1086.0 suffer from what appears to be a very annoying b.ug which cause nullable bit(1) columns set to null
//to future maintainers to always be materialized to true when we evaluate them like so
//to future maintainers
//to future maintainers x.SOME_BOOLEAN_COLUMN ?? false
//to future maintainers
//to future maintainers this is obviously flat out wrong and to remedy this issue we intercept the offending sql snippets and hotfix them like so
//to future maintainers
//to future maintainers before => CASE WHEN Extent1.NFS_SUBFILTER_YN IS NULL THEN 1 ELSE Extent1.NFS_SUBFILTER_YN END
//to future maintainers after => CASE WHEN Extent1.NFS_SUBFILTER_YN IS NULL THEN 1 ELSE CAST(Extent1.NFS_SUBFILTER_YN AS SIGNED) END
//to future maintainers
public sealed class MySqlHotFixerCommandInterceptor : IDbCommandInterceptor
{
//beforeexecution
public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) => LogWarningBeforeExecutionIfSynchronousExecutionIsFound(command, interceptionContext);
public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) => LogWarningBeforeExecutionIfSynchronousExecutionIsFound(command, interceptionContext);
public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) => LogWarningBeforeExecutionIfSynchronousExecutionIsFound(command, interceptionContext);
//afterexecution
public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) {}
public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) {}
public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) {}
static private void LogWarningBeforeExecutionIfSynchronousExecutionIsFound<TResult>(IDbCommand command, DbCommandInterceptionContext<TResult> interceptionContext)
{
if (interceptionContext.Exception != null)
return;
command.CommandText = FaultySwitchCaseSpotter1.Replace(command.CommandText, "CAST( AS SIGNED)");
command.CommandText = FaultySwitchCaseSpotter2.Replace(command.CommandText, "CAST( AS SIGNED)");
}
private static readonly Regex FaultySwitchCaseSpotter1 = new Regex(@"(?<=CASE\s+WHEN\s+(`?[a-zA-Z0-9_]+?`?[.])?`?[a-zA-Z0-9_]+?`?\s+IS\s+NULL\s+THEN\s+(0|1)\s+ELSE\s+)((`?[a-zA-Z0-9_]+?`?[.])?`?[a-zA-Z0-9_]+?`?)(?=\s*END)", RegexOptions.IgnoreCase);
private static readonly Regex FaultySwitchCaseSpotter2 = new Regex(@"(?<=CASE\s+WHEN\s+(`?[a-zA-Z0-9_]+?`?[.])?`?[a-zA-Z0-9_]+?_YN`?\s+IS\s+NULL\s+THEN\s+(\d+|.[a-zA-Z0-9_]+?|`?[a-zA-Z0-9_]+?`?[.]`?[a-zA-Z0-9_]+?`?)\s+ELSE\s+)((`?[a-zA-Z0-9_]+?`?[.])?`?[a-zA-Z0-9_]+?_YN`?)(?=\s*END)", RegexOptions.IgnoreCase);
}
}
web.config/app.config 也需要像这样调整:
<configuration>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework">
[...]
</defaultConnectionFactory>
<providers>
[...]
</providers>
<interceptors>
<!-- keep first -->
<interceptor type="Some.Namespace.MySqlHotFixerCommandInterceptor, Organotiki.Infrastructure.Core">
</interceptor>
[...]
</interceptors>
</entityFramework>
</configuration>
雇佣的图书馆员:
Devart.Data.dll => 5.0.1878.0
Devart.Data.MySql.dll => 8.10.1086.0
Devart.Data.MySql.Design.dll => 8.10.1086.0
Devart.Data.MySql.Entity.EF6.dll => 8.10.1086.0
EntityFramework => 6.2.0
我们的目标是来自 ASP.NET MVC5 项目的 .net4.7.1。我们的存储库构建在 EF .edmx(数据库优先方法)之上。我们的查询如下所示:
var results = _db.NB_FILTERS
.Select(x => new ReportFiltersDTO { IsHiddenSubFilter = x.NFS_SUBFILTER_YN ?? false })
.ToList();
NFS_SUBFILTER_YN 在我们的 .edmx 中被声明为布尔值:
<Property Name="NFS_SUBFILTER_YN" Type="boolean" />
NB_FILTERS ddl 是这样的:
CREATE TABLE NB_FILTERS (
[...]
NFS_SUBFILTER_YN BIT(1) NULL,
[...]
)
在这种情况下,如果某些行为空,则给定的 linq 表达式 returns 'true' 代替 'false'。罪魁祸首似乎在自动生成的 sql:
actual => CASE WHEN Extent1.NFS_SUBFILTER_YN IS NULL THEN 0 ELSE Extent1.NFS_SUBFILTER_YN END
corrected => CASE WHEN Extent1.NFS_SUBFILTER_YN IS NULL THEN 1 ELSE CAST(Extent1.NFS_SUBFILTER_YN AS SIGNED) END
如何在不更改基础表的情况下解决此错误and/or linq 语句本身,直到 Devart 推出实际修复程序?
旁注:
对于那些感兴趣的人,我们已经将此问题通知了开发人员,希望它会在某个时候得到解决:
有趣的是,当涉及的列是可为 null 的 decimal(x,y) 列时,此类查询有效 [自动生成的 sql 正确地使用了 CAST(... AS SIGNED)]。似乎以某种方式从符合此类处理条件的类型列表中省略了 bit(x)。
作为临时 stop-gap 解决方案,我们选择在 mysql 上使用 EF6 拦截器来检测和更正针对布尔列的 on-the-fly sql 语句(幸好大多数如果不是我们所有的布尔列都有 _YN 后缀,这有很大帮助):
using System.Data;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
using System.Text.RegularExpressions;
namespace Some.Namespace
{
//to future maintainers the devart mysql drivers for ef6 ver8.10.1086.0 suffer from what appears to be a very annoying b.ug which cause nullable bit(1) columns set to null
//to future maintainers to always be materialized to true when we evaluate them like so
//to future maintainers
//to future maintainers x.SOME_BOOLEAN_COLUMN ?? false
//to future maintainers
//to future maintainers this is obviously flat out wrong and to remedy this issue we intercept the offending sql snippets and hotfix them like so
//to future maintainers
//to future maintainers before => CASE WHEN Extent1.NFS_SUBFILTER_YN IS NULL THEN 1 ELSE Extent1.NFS_SUBFILTER_YN END
//to future maintainers after => CASE WHEN Extent1.NFS_SUBFILTER_YN IS NULL THEN 1 ELSE CAST(Extent1.NFS_SUBFILTER_YN AS SIGNED) END
//to future maintainers
public sealed class MySqlHotFixerCommandInterceptor : IDbCommandInterceptor
{
//beforeexecution
public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) => LogWarningBeforeExecutionIfSynchronousExecutionIsFound(command, interceptionContext);
public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) => LogWarningBeforeExecutionIfSynchronousExecutionIsFound(command, interceptionContext);
public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) => LogWarningBeforeExecutionIfSynchronousExecutionIsFound(command, interceptionContext);
//afterexecution
public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) {}
public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) {}
public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) {}
static private void LogWarningBeforeExecutionIfSynchronousExecutionIsFound<TResult>(IDbCommand command, DbCommandInterceptionContext<TResult> interceptionContext)
{
if (interceptionContext.Exception != null)
return;
command.CommandText = FaultySwitchCaseSpotter1.Replace(command.CommandText, "CAST( AS SIGNED)");
command.CommandText = FaultySwitchCaseSpotter2.Replace(command.CommandText, "CAST( AS SIGNED)");
}
private static readonly Regex FaultySwitchCaseSpotter1 = new Regex(@"(?<=CASE\s+WHEN\s+(`?[a-zA-Z0-9_]+?`?[.])?`?[a-zA-Z0-9_]+?`?\s+IS\s+NULL\s+THEN\s+(0|1)\s+ELSE\s+)((`?[a-zA-Z0-9_]+?`?[.])?`?[a-zA-Z0-9_]+?`?)(?=\s*END)", RegexOptions.IgnoreCase);
private static readonly Regex FaultySwitchCaseSpotter2 = new Regex(@"(?<=CASE\s+WHEN\s+(`?[a-zA-Z0-9_]+?`?[.])?`?[a-zA-Z0-9_]+?_YN`?\s+IS\s+NULL\s+THEN\s+(\d+|.[a-zA-Z0-9_]+?|`?[a-zA-Z0-9_]+?`?[.]`?[a-zA-Z0-9_]+?`?)\s+ELSE\s+)((`?[a-zA-Z0-9_]+?`?[.])?`?[a-zA-Z0-9_]+?_YN`?)(?=\s*END)", RegexOptions.IgnoreCase);
}
}
web.config/app.config 也需要像这样调整:
<configuration>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework">
[...]
</defaultConnectionFactory>
<providers>
[...]
</providers>
<interceptors>
<!-- keep first -->
<interceptor type="Some.Namespace.MySqlHotFixerCommandInterceptor, Organotiki.Infrastructure.Core">
</interceptor>
[...]
</interceptors>
</entityFramework>
</configuration>