概述
Elasticsearch对存储和索引的文档提供搜索和聚合功能。这些文档在HTTP请求的请求体中作为JSON对象发送。在NEST和Elasticsearch.Net中使用POCO对文档建模是很自然的事情。
本节主要介绍如何使用类型和类型层次结构对文档建模。
默认序列化行为
NEST的默认序列化行为是将类型属性名称序列化为驼峰式的JSON对象成员,比如给定如下POCO实体对象:
public class MyDocument
{
public string StringProperty { get; set; }
}
使用NEST创建索引文档:
var indexResponse = Client.Index(
new MyDocument { StringProperty = "value" },
i => i.Index("my_documents"));
POCO对象的StringProperty属性名将被序列化成stringProperty:
{
"stringProperty": "value"
}
DefaultFieldNameInferrer设置
不同系统在将文档索引到Elasticsearch中,可能对JSON对象成员使用不同的序列化规范(可能不是驼峰式命名)。NEST也提供了自定义JSON对象成员序列化规范的全局配置接口,你可以通过ConnectionSettings的DefaultFieldNameInferrer来设置。
以下是实现了蛇形命名的序列规范:
var settings = new ConnectionSettings();
static string ToSnakeCase(string s)
{
var builder = new StringBuilder(s.Length);
for (int i = 0; i < s.Length; i++)
{
var c = s[i];
if (char.IsUpper(c))
{
if (i == 0)
builder.Append(char.ToLowerInvariant(c));
else if (char.IsUpper(s[i - 1]))
builder.Append(char.ToLowerInvariant(c));
else
{
builder.Append("_");
builder.Append(char.ToLowerInvariant(c));
}
}
else
builder.Append(c);
}
return builder.ToString();
}
settings.DefaultFieldNameInferrer(p => ToSnakeCase(p));
var client = new ElasticClient(settings);
var indexResponse = client.Index(
new MyDocument { StringProperty = "value" },
i => i.Index("my_documents"));
序列化后的JSON结果为:
{
"string_property": "value"
}
PropertyName特性
有时,可能只需要指定特定POCO属性的序列化方式。PropertyName特性可以应用于POCO属性,以控制POCO属性将序列化为哪个名称,并从哪个名称反序列化。以下示例演示了如何将POCO对象实体成员的StringProperty属性使用PropertyName特性序列化成string_propertyJSON字段名的:
public class MyDocumentWithPropertyName
{
[PropertyName("string_property")]
public string StringProperty { get; set; }
}
var indexResponse = Client.Index(
new MyDocumentWithPropertyName { StringProperty = "value" },
i => i.Index("my_documents"));
序列化后的JSON结果为:
{
"string_property": "value"
}
NEST属性特性
PropertyName特性可以用来控制对象属性名称的序列化规范,NEST还提供了其他一些特性,比如:Text特性,它不仅可以用来指定属性名称的序列化规范,还可以指定对象属性在Easticesearch中的映射关系。Text特性中的Name属性与PropertyName的功能类似,是用来指定属性序列化名称规范的。
以下是一个演示如何使用NEST中Text特性的示例:
public class MyDocumentWithTextProperty
{
[Text(Name = "string_property")]
public string StringProperty { get; set; }
}
var indexResponse = Client.Index(
new MyDocumentWithTextProperty { StringProperty = "value" },
i => i.Index("my_documents"));
序列化后的JSON结果为:
{
"string_property": "value"
}
DataMember特性
System.Runtime.Serialization.DataMember特性与PropetyName特性相似,在一些不依赖NEST类库的项目中,你可以使用DataMember代替PropertyName,使用示例如下:
public class MyDocumentWithDataMember
{
[DataMember(Name = "string_property")]
public string StringProperty { get; set; }
}
var indexResponse = Client.Index(
new MyDocumentWithDataMember { StringProperty = "value" },
i => i.Index("my_documents"));
序列化后的JSON结果为:
{
"string_property": "value"
}
DefaultMappingFor<TDocument>设置
虽然DefaultFieldNameInferrer对所有POCO属性应用序列化的约定,但可能存在只有特定POCO的特定属性以不同的方式序列化的情况。此时,可以通过ConnectionSettings上的DefaultMappingFor <TDocument>设置更改类型的属性映射规范。
以下演示了如何更改MyDocument类型的StringProperty成员的序列化规范:
var settings = new ConnectionSettings();
settings.DefaultMappingFor<MyDocument>(d => d
.PropertyName(p => p.StringProperty, nameof(MyDocument.StringProperty))
);
var client = new ElasticClient(settings);
var indexResponse = client.Index(
new MyDocument { StringProperty = "value" },
i => i.Index("my_documents"));
序列化后的JSON结果为:
{
"StringProperty": "value"
}
当涉及到类层次结构时,DefaultMappingFor<TDocument>可能会很有用,比如:
public class MyBaseDocument
{
public string StringProperty { get; set; }
}
public class MyDerivedDocument : MyBaseDocument
{
public int IntProperty { get; set; }
}
序列化MyDerivedDocument对象:
var indexResponse = Client.Index(
new MyDerivedDocument { StringProperty = "value", IntProperty = 2 },
i => i.Index("my_documents"));
序列化的JSON结果为:
{
"intProperty": 2,
"stringProperty": "value"
}
现在,使用DefaultMappingFor<TDocument>来控制MyDerivedDocument的映射方式:
var settings = new ConnectionSettings();
settings.DefaultMappingFor<MyDerivedDocument>(d => d
.PropertyName(p => p.IntProperty, nameof(MyDerivedDocument.IntProperty))
.Ignore(p => p.StringProperty)
);
var client = new ElasticClient(settings);
var indexResponse = client.Index(
new MyDerivedDocument { StringProperty = "value", IntProperty = 2 },
i => i.Index("my_documents"));
序列化的JSON结果为:
{
"IntProperty": 2
}
结果显示,POCO中属性名为IntProperty被序列化出来了,但StringProperty没有被序列化(被忽略了)。
现在,我们再索引一个基于MyBaseDocument的文档:
var indexResponse2 = client.Index(
new MyBaseDocument { StringProperty = "value" },
i => i.Index("my_documents"));
序列化后的JSON结果为:
{}
即使我们使用DefaultMappingFor<TDocument>设置了派生类MyDerivedDocument的默认序列化映射关系,但基类的StringProperty仍然没有被序列化(被忽略了)。
之所以会出现这种情况,是因为MyBaseDocument是StringProperty成员的声明类型,当从表达式p => p.StringProperty中检索到StringProperty的MemberInfo时,它的声明类型是MyBaseDocument。因为DefaultMappingFor<TDocument>为所有类型在字典中以MemberInfo为键持久化了属性映射关系,因此,使用DefaultMappingFor<MyDerivedDocument>定义的属性PropertyName()映射关系也适用于基类MyBaseDocment的映射关系。
考虑一个更复杂的示例,其中基类将成员定义为virtual,而派生类重写了该成员,如:
public class MyBaseDocumentVirtualProperty
{
public virtual string StringProperty { get; set; }
}
public class MyDerivedDocumentOverrideProperty : MyBaseDocumentVirtualProperty
{
public override string StringProperty { get; set; }
public int IntProperty { get; set; }
}
与前面的示例类似,DefaultMappingFor<TDocument>定义了派生类MyDerivedDocumentOverrideProperty:
var settings = new ConnectionSettings();
settings.DefaultMappingFor<MyDerivedDocumentOverrideProperty>(d => d
.PropertyName(p => p.IntProperty, nameof(MyDerivedDocumentOverrideProperty.IntProperty))
.Ignore(p => p.StringProperty)
);
var client = new ElasticClient(settings);
var indexResponse = client.Index(
new MyDerivedDocumentOverrideProperty { StringProperty = "value", IntProperty = 2 },
i => i.Index("my_documents"));
序列化后的JSON的结果为:
{
"stringProperty": "value",
"IntProperty": 2
}
值得注意的是,即使DefaultMappingFor<MyDerivedDocumentOverrideProperty>配置指定忽略StringProperty成员,但这里仍然被序列化了。
再测试序列化基类MyBaseDocumentVirtualProperty:
var indexResponse2 = client.Index(
new MyBaseDocumentVirtualProperty { StringProperty = "value" },
i => i.Index("my_documents"));
序列化后的JSON结果为:
{}
你可能会感到惊讶了,为什么序列化出来的基类的JSON是空的呢?
这是因为使用反射成使用表达式来获取成员的MemberInfo是有区别的。
当使用反射在MyDerivedDocumentOverrideProperty上获取StringProperty成员信息时,DeclaringType和ReflectedType均为MyDerivedDocumentOverrideProperty。
var memberInfo = typeof(MyDerivedDocumentOverrideProperty).GetProperty("StringProperty");
Console.WriteLine($"DeclaringType: {memberInfo.DeclaringType.Name}");
Console.WriteLine($"ReflectedType: {memberInfo.ReflectedType.Name}");
而使用表达式在MyDerivedDocumentOverrideProperty上获取StringProperty成员信息时,DeclaringType和ReflectedType均为MyBaseDocumentVirtualProperty。
public class MemberVisitor : ExpressionVisitor
{
protected override Expression VisitMember(MemberExpression node)
{
Console.WriteLine($"DeclaringType: {node.Member.DeclaringType.Name}");
Console.WriteLine($"ReflectedType: {node.Member.ReflectedType.Name}");
return base.VisitMember(node);
}
}
Expression<Func<MyDerivedDocumentOverrideProperty, string>> memberExpression =
p => p.StringProperty;
var visitor = new MemberVisitor();
visitor.Visit(memberExpression);
再看另外一个示例,使用new关键字隐藏基类型成员的派生类型:
public class MyDerivedDocumentShadowProperty : MyBaseDocument
{
public new string StringProperty { get; set; }
}
现在配置DefaultMappingFor<TDocument>类型为MyDerivedDocumentShadowProperty:
var settings = new ConnectionSettings();
settings.DefaultMappingFor<MyDerivedDocumentShadowProperty>(d => d
.Ignore(p => p.StringProperty)
);
var client = new ElasticClient(settings);
var indexResponse = client.Index(
new MyDerivedDocumentShadowProperty { StringProperty = "value" },
i => i.Index("my_documents"));
序列化后的JSON结果为:
{}
然而,换成基类MyBaseDocument:
var indexResponse2 = client.Index(
new MyBaseDocument { StringProperty = "value" },
i => i.Index("my_documents"));
序列化后的JSON结果为:
{
"stringProperty": "value"
}
总之,在使用类型层次结构在Elasticsearch中建立索引的文档时,应该仔细考虑。如果可能的话,一般建议使用简单的POCO。
发表评论
登录用户才能发表评论, 请 登 录 或者 注册