【问题标题】:Query string properties stored as XML查询存储为 XML 的字符串属性
【发布时间】:2018-12-08 11:20:43
【问题描述】:

我正在使用 Entity Framework 来查询由模型定义的数据库:在这个模型中,我有几个具有 #region dynamic values 的类:

[DataContract]
public class Job : AbstractEntity, IJob
{
    [DataMember]
    public virtual Guid Id { get; set; }

    ...

    #region dynamic values

    [DataMember]
    public virtual string MetadataValue { get; set; }
    [DataMember]
    public virtual string ParametersValue { get; set; }
    [DataMember]
    public virtual string AttributesValue { get; set; }

    #endregion

    #region links
    ...
    #endregion
}

AttributesValueMetadataValueParametersValue 被声明为字符串,但作为 XML 文档存储在数据库中。我知道这与模型不一致,应该更改,但由于某些原因,它已以这种方式进行管理,我不允许对其进行修改。 为了更好地处理问题,我创建了一个单元测试,代码如下:

public class UnitTest1
{
    private ModelContext mc;

    [TestInitialize]
    public void TestInit()
    {
        IModelContextFactory mfactory = ModelContextFactory.GetFactory();
        mc = mfactory.CreateContextWithoutClientId();
    }

    [TestMethod]
    public void TestMethod1()
    {
        DbSet<Job> jobs = mc.Job;

        IQueryable<string> query = jobs
            .Where(elem => elem.AttributesValue == "<coll><item><key>ids:ui:description</key><value>Session Test</value></item><item><key>ids:all:type</key><value>signature</value></item></coll>")
            .Select(elem => elem.AttributesValue);

        List<string> attrs = new List<string>(query);
        foreach (string av in attrs)
        {
            Console.WriteLine(av ?? "null");
        }

        Assert.AreEqual(1, 1);
    }
}

关于TestInitModelContext 的简要说明: ModelContext继承自DbContext,是SqlModelContextOracleModelContext实现的抽象类(都覆盖OnModelCreating)。根据连接字符串,CreateContextWithoutClientId 返回SqlModelContextOracleModelContext。摘要:工厂模式。

让我们开始着手:TestMethod1。 这里的问题出在Where 方法中,返回的错误正如预期的那样:

SqlException: 数据类型 nvarchar 和 xml 在等于运算符中不兼容。

(从现在开始我只考虑AttributesValue 属性)

我想到了一些可能的解决方案,分别是:

  • 在模型中创建一个新属性(但未映射到数据库)并将其用作“代理”,而不是直接访问AttributesValue。但是在 Linq 中只能使用映射的属性,所以我放弃了它。

  • 直接对IQueryable 生成的内部 SQL 查询进行操作,并为 Oracle 和 Sql Server db 使用自定义的 CAST。出于明显的原因,我宁愿避免这样做。

有没有办法指定一个自定义的属性获取器,以便我可以在访问之前将AttributesValue 转换为字符串?或者DbModelBuilder上的一些配置?

我正在使用标准实体框架 6,代码优先方法。

【问题讨论】:

  • 它对我有什么帮助?但是,它不起作用,只是尝试了。还是谢谢。
  • EF6 还是 EF Core?如果是 EF6,代码优先模型还是 edmx?
  • @IvanStoev 我完全忘记指定了,谢谢。

标签: c# sql xml entity-framework linq


【解决方案1】:

没有标准的 xml 数据类型或标准的规范函数用于将字符串转换为 xml,反之亦然。

幸运的是,EF6 支持所谓的Entity SQL Language,它支持一个有用的构造,称为CAST

CAST (expression AS data_type)

强制转换表达式与 Transact-SQL CONVERT 表达式具有相似的语义。强制转换表达式用于将一种类型的值转换为另一种类型的值。

它可以在EntityFramework.Functions 包和Model defined functions 的帮助下使用。

模型定义的函数允许您将实体 SQL 表达式与用户定义的函数相关联。要求是函数参数必须是实体。

Entity SQL 运算符的好处是它们独立于数据库(类似于规范函数),因此最终的 SQL 仍然由数据库提供程序生成,因此您不需要为 SqlServer 和 Oracle 编写单独的实现。

通过Nuget安装EntityFramework.Functions包并添加如下类(注意:所有代码都需要using EntityFramework.Functions;):

public static class JobFunctions
{
    const string Namespace = "EFTest";

    [ModelDefinedFunction(nameof(MetadataValueXml), Namespace, "'' + CAST(Job.MetadataValue AS String)")]
    public static string MetadataValueXml(this Job job) => job.MetadataValue;

    [ModelDefinedFunction(nameof(ParametersValueXml), Namespace, "'' + CAST(Job.ParametersValue AS String)")]
    public static string ParametersValueXml(this Job job) => job.ParametersValue;

    [ModelDefinedFunction(nameof(AttributesValueXml), Namespace, "'' + CAST(Job.AttributesValue AS String)")]
    public static string AttributesValueXml(this Job job) => job.AttributesValue;
}

基本上我们为每个 xml 属性添加简单的扩展方法。方法的主体没有做任何有用的事情——这些方法的全部目的不是直接调用,而是在 LINQ to Entities 查询中使用时转换为 SQL。所需的映射通过ModelDefinedFunctionAttribute 提供,并通过包实现的自定义FunctionConvention 应用。 Namespace 常量必须等于 typeof(Job).Namespace。不幸的是,由于要求属性只能使用常量,我们无法避免硬编码字符串以及 Entity SQL 字符串中的实体类/属性名称。

需要更多解释的是'' + CAST 的用法。我希望我们可以简单地使用CAST,但我的测试表明SqlServer“太聪明”(或有问题?)并且当在WHERE中使用时从表达式中删除CAST。附加空字符串的技巧可以防止这种行为。

然后您需要通过将以下行添加到您的数据库上下文 OnModelCreating 覆盖来将这些函数添加到实体模型:

modelBuilder.AddFunctions(typeof(JobFunctions));

现在您可以在 LINQ to Entities 查询中使用它们:

IQueryable<string> query = jobs
    .Where(elem => elem.AttributesValueXml() == "<coll><item><key>ids:ui:description</key><value>Session Test</value></item><item><key>ids:all:type</key><value>signature</value></item></coll>")
    .Select(elem => elem.AttributesValue);

在 SqlServer 中翻译成这样的东西:

SELECT
    [Extent1].[AttributesValue] AS [AttributesValue]
    FROM [dbo].[Jobs] AS [Extent1]
    WHERE N'<coll><item><key>ids:ui:description</key><value>Session Test</value></item><item><key>ids:all:type</key><value>signature</value></item></coll>'
    = ('' +  CAST( [Extent1].[AttributesValue] AS nvarchar(max)))

在甲骨文中:

SELECT
"Extent1"."AttributesValue" AS "AttributesValue"
FROM "ORATST"."Jobs" "Extent1"
WHERE ('<coll><item><key>ids:ui:description</key><value>Session Test</value></item><item><key>ids:all:type</key><value>signature</value></item></coll>'
= ((('')||(TO_NCLOB("Extent1"."AttributesValue")))))

【讨论】:

  • 今天早上我发现了EntityFramework.Function package的存在。不过,真的很好解释。如果它有效,明天我将接受它。谢谢。
  • Ivan 的好作品!一个警告。如您所知,对于 XML 数据类型,首选的查询方式是使用数据库的内置 XPath 查询运算符。当然,EF 中不支持这些运算符,因此从性能 POV 来看,使用 Dapper 等运行 SQL 语句可能会更好。不过,我不认为这是这个问题的答案,就像你的那样。
  • 完美运行。只有一件事看起来很奇怪:我必须使用的命名空间是 CodeFirstNamespace,即使我的解决方案中甚至没有它的匹配项。
  • 是的,这很奇怪。我不能使用它,Function 代码抛出异常,我应该在我的情况下使用“EFTest”。查看包源代码,它根据概念模型中第一个实体类型的名称空间验证名称空间。 “CodeFirstNamespace”听起来好像有一个由 EF 自动生成的实体,但我不知道如何以及何时。
猜你喜欢
  • 1970-01-01
  • 2018-04-08
  • 1970-01-01
  • 1970-01-01
  • 2018-10-24
  • 1970-01-01
  • 2015-12-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多