我正在使用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
}
AttributesValue
,MetadataValue
和ParametersValue
被声明为字符串,但作为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);
}
}
有关TestInit
和ModelContext
的简要说明:
ModelContext
继承自DbContext
,是由SqlModelContext
和OracleModelContext
(都覆盖OnModelCreating
)实现的抽象类。根据连接字符串,CreateContextWithoutClientId
返回SqlModelContext
或OracleModelContext
。摘要:工厂模式。
让我们开始讨论:TestMethod1
。
这里的问题出在Where
方法中,并且返回的错误是预期的:
SqlException:数据类型nvarchar和xml在等于运算符中不兼容。
(从现在开始,我将仅考虑AttributesValue
属性)
我想到了一些可能的解决方案,
在模型内部创建一个新属性(但未映射到数据库),并将其用作“代理”,而不是直接访问AttributesValue
。但是在Linq中只能使用映射的属性,因此我将其丢弃。
直接对IQueryable
生成的内部SQL查询进行操作,并为Oracle和Sql Server数据库使用自定义的CAST
。出于明显的原因,我宁愿避免这样做。
有没有一种方法可以指定自定义的Property Getter,以便在访问之前可以将AttributesValue
转换为字符串?还是在DbModelBuilder
上进行了某些配置?
我正在使用标准的Entity Framework 6,代码优先方法。
答案 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")))))