对类型文档建模

1055 更新于: 2021-04-02 读完约需 9 分钟

概述

Elasticsearch对存储和索引的文档提供搜索和聚合功能。这些文档在HTTP请求的请求体中作为JSON对象发送。在NESTElasticsearch.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对象成员序列化规范的全局配置接口,你可以通过ConnectionSettingsDefaultFieldNameInferrer来设置。

以下是实现了蛇形命名的序列规范:

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的功能类似,是用来指定属性序列化名称规范的。

以下是一个演示如何使用NESTText特性的示例:

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仍然没有被序列化(被忽略了)。

之所以会出现这种情况,是因为MyBaseDocumentStringProperty成员的声明类型,当从表达式p => p.StringProperty中检索到StringPropertyMemberInfo时,它的声明类型是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成员信息时,DeclaringTypeReflectedType均为MyDerivedDocumentOverrideProperty

var memberInfo = typeof(MyDerivedDocumentOverrideProperty).GetProperty("StringProperty");
Console.WriteLine($"DeclaringType: {memberInfo.DeclaringType.Name}");
Console.WriteLine($"ReflectedType: {memberInfo.ReflectedType.Name}");

而使用表达式在MyDerivedDocumentOverrideProperty上获取StringProperty成员信息时,DeclaringTypeReflectedType均为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。

版权声明:本作品系原创,版权归码友网所有,如未经许可,禁止任何形式转载,违者必究。

发表评论

登录用户才能发表评论, 请 登 录 或者 注册