動態

詳情 返回 返回

修復達夢EFCore驅動布爾類型兼容問題 - 動態 詳情

dm庫相比其他庫本身缺少一些語法差異,也可以説是缺陷。
比如:
0和1無法直接在sql中當作真假值用,where 0這種寫法不支持,報錯:查詢使用值表達式作為過濾條件;
t.field is null 也無法直接作為select項;
不支持OUTER APPLY等SQL語法;
以及數據庫函數中的又只能用0和1作為布爾參數值。

但是dm.efcore生成的語句就是這樣的,現在來給修復辦法。

1.查詢使用值表達式作為過濾條件

這個問題其實不好修,正常的修復辦法需要重寫很多代碼,主要是EFCore裏面的這個代碼SqlNullabilityProcessor,主要就是為了優化sql語句的

  • a == true -> a

a == true -> a 變成where a這樣,就是這個代碼導致的,但是很遺憾達夢不支持。

public class SqlNullabilityProcessor
{
    .....

    private SqlExpression OptimizeComparison(
        SqlBinaryExpression sqlBinaryExpression,
        SqlExpression left,
        SqlExpression right,
        bool leftNullable,
        bool rightNullable,
        out bool nullable)
    {
        var leftNullValue = leftNullable && left is SqlConstantExpression or SqlParameterExpression;
        var rightNullValue = rightNullable && right is SqlConstantExpression or SqlParameterExpression;

        // a == null -> a IS NULL
        // a != null -> a IS NOT NULL
        if (rightNullValue)
        {
            var result = sqlBinaryExpression.OperatorType == ExpressionType.Equal
                ? ProcessNullNotNull(_sqlExpressionFactory.IsNull(left), leftNullable)
                : ProcessNullNotNull(_sqlExpressionFactory.IsNotNull(left), leftNullable);

            nullable = false;

            return result;
        }

        // null == a -> a IS NULL
        // null != a -> a IS NOT NULL
        if (leftNullValue)
        {
            var result = sqlBinaryExpression.OperatorType == ExpressionType.Equal
                ? ProcessNullNotNull(_sqlExpressionFactory.IsNull(right), rightNullable)
                : ProcessNullNotNull(_sqlExpressionFactory.IsNotNull(right), rightNullable);

            nullable = false;

            return result;
        }

        if (TryGetBool(right, out var rightBoolValue)
            && !leftNullable
            && left.TypeMapping!.Converter == null)
        {
            nullable = leftNullable;

            // only correct in 2-value logic
            // a == true -> a
            // a == false -> !a
            // a != true -> !a
            // a != false -> a
            return sqlBinaryExpression.OperatorType == ExpressionType.Equal ^ rightBoolValue
                ? OptimizeNonNullableNotExpression(_sqlExpressionFactory.Not(left))
                : left;
        }

        if (TryGetBool(left, out var leftBoolValue)
            && !rightNullable
            && right.TypeMapping!.Converter == null)
        {
            nullable = rightNullable;

            // only correct in 2-value logic
            // true == a -> a
            // false == a -> !a
            // true != a -> !a
            // false != a -> a
            return sqlBinaryExpression.OperatorType == ExpressionType.Equal ^ leftBoolValue
                ? OptimizeNonNullableNotExpression(_sqlExpressionFactory.Not(right))
                : right;
        }

        // only correct in 2-value logic
        // a == a -> true
        // a != a -> false
        if (!leftNullable
            && left.Equals(right))
        {
            nullable = false;

            return _sqlExpressionFactory.Constant(
                sqlBinaryExpression.OperatorType == ExpressionType.Equal,
                sqlBinaryExpression.TypeMapping);
        }

        if (!leftNullable
            && !rightNullable
            && sqlBinaryExpression.OperatorType is ExpressionType.Equal or ExpressionType.NotEqual)
        {
            var leftUnary = left as SqlUnaryExpression;
            var rightUnary = right as SqlUnaryExpression;

            var leftNegated = IsLogicalNot(leftUnary);
            var rightNegated = IsLogicalNot(rightUnary);

            if (leftNegated)
            {
                left = leftUnary!.Operand;
            }

            if (rightNegated)
            {
                right = rightUnary!.Operand;
            }

            // a == b <=> !a == !b -> a == b
            // !a == b <=> a == !b -> a != b
            // a != b <=> !a != !b -> a != b
            // !a != b <=> a != !b -> a == b

            nullable = false;

            return sqlBinaryExpression.OperatorType == ExpressionType.Equal ^ leftNegated == rightNegated
                ? _sqlExpressionFactory.NotEqual(left, right)
                : _sqlExpressionFactory.Equal(left, right);
        }

        nullable = false;

        return sqlBinaryExpression.Update(left, right);
    }
}

本來像的修復辦法是重寫這個類,但是這個方法不是虛方法還是私有的,無法重寫,就需要從調用這個方法的所有方法全部重寫,這就太麻煩了,這種重寫還會有其他問題,比如下次升級efcore時,新版本的這個方法有改動,但我重寫了就需要再重新重寫一次。
在想不到辦法的時候,無意間注意到這個方法轉換0 和1 的地方就時TryGetBool這個幾個if判斷中(本來就時想改這裏),都有下面這個判斷
XXX.TypeMapping!.Converter == null
那麼我就想如果boolTypeMapping.Converter不是null的不就可以繞過這個轉換了嗎。
所以就有了下面這個解決辦法,直接上代碼,不解釋了,都能看懂:

public class MyDmBoolTypeMapping : BoolTypeMapping
{
    public MyDmBoolTypeMapping(string storeType, DbType? dbType)
        : base(new RelationalTypeMappingParameters(
                new(typeof(bool), new DmBooleanConverter(), null), storeType))
    {
    }

    protected MyDmBoolTypeMapping(RelationalTypeMappingParameters parameters)
        : base(parameters)
    {
    }
    protected override string GenerateNonNullSqlLiteral(object value)
    {
        if (!(bool)value)
        {
            return "0";
        }
        return "1";
    }

    protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
    {
        return (RelationalTypeMapping)(object)new MyDmBoolTypeMapping(parameters);
    }
    private sealed class DmBooleanConverter : ValueConverter<bool, bool>
    {
        public DmBooleanConverter()
            : base(b => b, b => b)
        {
        }
    }
}
public class MyDmTypeMappingSource:DmTypeMappingSource
{
    public MyDmTypeMappingSource([NotNull] TypeMappingSourceDependencies dependencies, [NotNull] RelationalTypeMappingSourceDependencies relationalDependencies)
        : base(dependencies, relationalDependencies)
    {
        var f = typeof(DmTypeMappingSource).GetField("_bool",BindingFlags.Instance| BindingFlags.NonPublic);
        f.SetValue(this,new MyDmBoolTypeMapping("BIT", DbType.Boolean));
    }
}

上面代碼就時給Bool類型的加上了Converter,它不再是null了,同樣像上一個dmefcore驅動文章説的一樣,用把這個MyDmTypeMappingSource替換服務類

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    base.OnConfiguring(optionsBuilder);
    optionsBuilder.ReplaceService<IRelationalTypeMappingSource, MyDmTypeMappingSource>();
}

到這裏就解決了a == true -> a這一種“查詢使用值表達式作為過濾條件”報錯問題,但是不徹底,還會有其他比如 a.Contains(t.Id) -> false這樣的情況,當然還有其他很多情況。
無法直接改代碼修復,我又繼續思考,然後我發現達夢不支持0和1作為條件,但是支持false和true這樣的作為條件,那麼我就把這個MyDmBoolTypeMapping中的GenerateNonNullSqlLiteral方法改成返回FALSE和TRUE不就可以了嗎,所以就有了下面的代碼,驗證發現完美解決問題

public class MyDmBoolTypeMapping : BoolTypeMapping
{
    public MyDmBoolTypeMapping(string storeType, DbType? dbType)
        : base(new RelationalTypeMappingParameters(
                new(typeof(bool), new DmBooleanConverter(), null), storeType))
    {
    }

    protected MyDmBoolTypeMapping(RelationalTypeMappingParameters parameters)
        : base(parameters)
    {
    }
    protected override string GenerateNonNullSqlLiteral(object value)
    {
        if (!(bool)value)
        {
            return "FALSE";
        }
        return "TRUE";
    }

    protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
    {
        return (RelationalTypeMapping)(object)new MyDmBoolTypeMapping(parameters);
    }
    private sealed class DmBooleanConverter : ValueConverter<bool, bool>
    {
        public DmBooleanConverter()
            : base(b => b, b => b)
        {
        }
    }
}

2.數據庫函數中的又只能用0和1作為布爾參數值

先説函數這個問題,因為我們改成了FALSE和TRUE導致的,那麼我再繼續重寫:

public class MyDmSqlNullabilityProcessor : SqlNullabilityProcessor
{
    protected override SqlExpression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression, bool allowOptimizedExpansion, out bool nullable)
    {
        var exp = base.VisitSqlFunction(sqlFunctionExpression, allowOptimizedExpansion, out nullable);
        if (exp is SqlFunctionExpression functionExpression)
        {
            var arguments = functionExpression.Arguments.Select(e =>
            {
                if (e is SqlConstantExpression constantExpression && constantExpression.Value is bool value)
                {
                    return _sqlExpressionFactory.Constant(value ? 1 : 0, new IntTypeMapping("INT", DbType.Int32));
                }
                return e;
            }).ToArray();
            return functionExpression.Update(functionExpression.Instance, arguments);
        }
        return exp;
    }
}

3.其他問題

主要修復代碼就是下面這個,關鍵代碼是這行UpdateShaperExpression(Visit(shapedQueryExpression.ShaperExpression))
同樣需要其他類new它並且跟其他服務類一樣替換注入,就不多解釋了。

public class MySearchConditionConvertingExpressionVisitor : SearchConditionConvertingExpressionVisitor
{

    protected override Expression VisitExtension(Expression extensionExpression)
        => extensionExpression switch
    {
        ShapedQueryExpression shapedQueryExpression
            => shapedQueryExpression
            .UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression))
            .UpdateShaperExpression(Visit(shapedQueryExpression.ShaperExpression)),

         _ => base.VisitExtension(extensionExpression),
    };
}

Add a new 評論

Some HTML is okay.