查询存储为XML的字符串属性

时间:2018-06-29 09:38:32

标签: c# sql xml entity-framework linq

我正在使用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数据库使用自定义的CAST。出于明显的原因,我宁愿避免这样做。

有没有一种方法可以指定自定义的Property Getter,以便在访问之前可以将AttributesValue转换为字符串?还是在DbModelBuilder上进行了某些配置?

我正在使用标准的Entity Framework 6,代码优先方法。

1 个答案:

答案 0 :(得分:3)

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

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

CAST (expression AS data_type)
  

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

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

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

关于实体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。不幸的是,由于要求属性只能使用常量,因此我们无法避免在实体SQL字符串中使用该硬编码字符串以及实体类/属性名称。

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

然后,您需要通过将以下行添加到数据库上下文中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)))

和在Oracle中:

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")))))