概述
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_property
JSON字段名的:
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。
发表评论
登录用户才能发表评论, 请 登 录 或者 注册