Join父子类型
前言
ES底层是Lucene,由于Lucene实际上是不支持嵌套类型的,所有文档都是以扁平的结构存储在Lucene中,ES对父子文档的支持,实际上也是采取了一种投机取巧的方式实现的.
父子文档均以独立的文档存入,然后添加关联关系,且父子文档必须在同一分片,由于父子类型文档并没有减少文档数量,而且增加了父子绑定关系,会导致查询效率低下,因此我们并不建议您在实际开发中使用父子类型.
ES本身更适合"大宽表"模式,不要带着传统关系型数据库那种思维方式去使用ES,我们完全可以通过把多张表中的字段和内容合并到一张表(一个索引)中,来完成期望功能,尽可能规避父子类型的使用,不仅效率高,功能也更强大.
当然存在即合理,也确实有个别场景下,不可避免的会用到父子类型,作为全球首屈一指的ES-ORM框架,我们对此也提供了支持,用户可以不用,但我们不能没有!
关于父子类型和嵌套类型的选择:如果对文档的写多于读,那么建议你选择父子类型,如果文档读多于写, 那么请选择嵌套类型.
推荐您在使用前先通过互联网了解一下ES原生的父子类型或RestHighLevelClient中父子类型相关的索引创建及CRUD,有一定理论基础后可以更方便理解本框架的API设计及使用. ES的父子类型及嵌套类型本身就是极度复杂的东西,使用成本非常高,在看完原生语法后相信您会给我们Star!
# 父子类型创建索引
- 步骤一 添加注解,指定父子关系:
/**
* Document根文档有子文档Author(作者)和Comment(评论),其中Author还有个子文档Contact(联系方式)
* Join父子类型结构如下所示
* Document
* / \
* Comment Author
* \
* Contact
* 上述结构可用@Join注解和@Node注解来表达,可参考下面案例
**/
@Join(nodes = {@Node(parentClass = Document.class, childClasses = {Author.class, Comment.class}), @Node(parentClass = Author.class, childClasses = Contact.class)})
public class Document {
// 省略其它无关字段
}
2
3
4
5
6
7
8
9
10
11
12
13
14
注意: 务必像上面示例一样,在父文档的类上加注解@Join配合@Node注解表达出父子结构,子文档的类上无需重复添加此注解,仅需要在Root根节点上加上此注解即可
- 步骤二 调用api完成索引创建(自动挡无需调用,项目启动则框架自动创建)
// 手动挡通过根mapper一键创建
documentMapper.createIndex();
2
创建完成后,父子类型索引结构如图:
# 父子类型 CRUD
注意父子类型由于都是独立的文档,独立的实体类,所以各自都需要有各自的mapper
API:
// 根据父id查询 (返回满足条件的所有子文档)
parentId(Object parentId, String type);
parentId(boolean condition, Object parentId, String type);
parentId(Object parentId, String type, Float boost);
parentId(boolean condition, Object parentId, String type, Float boost);
// 根据子查父匹配 (返回满足条件的所有子文档)
hasParent(Consumer<Param> consumer);
hasParent(String parentType, Consumer<Param> consumer);
Children hasParent(boolean condition, String parentType, Consumer<Param> consumer);
hasParent(boolean condition, String parentType, Consumer<Param> consumer, boolean score);
// 根据父查子 (返回满足条件的父文档)
hasChild(String type, Consumer<Param> consumer);
hasChild(String type, Consumer<Param> consumer, ScoreMode scoreMode);
hasChild(boolean condition, String type, Consumer<Param> consumer);
hasChild(boolean condition, String type, Consumer<Param> consumer, ScoreMode scoreMode);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CRUD示例:
@Test
public void testInsert() throws InterruptedException {
// 新新增父文档,然后再插入子文档
String parentId = "doc-1";
Document root = new Document();
root.setEsId(parentId);
root.setTitle("我是父文档的标题");
root.setContent("father doc");
documentMapper.insert(FIXED_ROUTING, root);
Thread.sleep(2000);
// 插入子文档1
Comment nodeA1 = new Comment();
nodeA1.setId("comment-1");
nodeA1.setCommentContent("test1");
// 这里特别注意,子文档必须指定其路由和父亲文档相同,否则傻儿子找不到爹别怪我没提醒 (es语法如此,非框架限制)
commentMapper.insert(FIXED_ROUTING, parentId, nodeA1);
// 插入子文档2
Comment nodeA2 = new Comment();
nodeA2.setId("comment-2");
nodeA2.setCommentContent("test2");
commentMapper.insert(FIXED_ROUTING, parentId, nodeA2);
// 插入子文档3
Author nodeB1 = new Author();
nodeB1.setAuthorId("author-1");
nodeB1.setAuthorName("tom");
authorMapper.insert(FIXED_ROUTING, parentId, nodeB1);
// 插入子文档4
Author nodeB2 = new Author();
nodeB2.setAuthorId("author-2");
nodeB2.setAuthorName("cat");
authorMapper.insert(FIXED_ROUTING, parentId, nodeB2);
Thread.sleep(2000);
// 插入孙子文档1(把孙子1挂在子文档3上)
Contact child1 = new Contact();
child1.setContactId("contact-1");
child1.setAddress("zhejiang province");
contactMapper.insert(FIXED_ROUTING, nodeB1.getAuthorId(), child1);
// 插入孙子文档2(把孙子2挂在子文档3上)
Contact child2 = new Contact();
child2.setContactId("contact-2");
child2.setAddress("hangzhou city");
contactMapper.insert(FIXED_ROUTING, nodeB1.getAuthorId(), child2);
// 插入孙子文档3(把孙子3挂在子文档4上)
Contact child3 = new Contact();
child3.setContactId("contact-3");
child3.setAddress("binjiang region");
contactMapper.insert(FIXED_ROUTING, nodeB2.getAuthorId(), child3);
// es写入数据有延迟 适当休眠 保证后续查询结果正确
Thread.sleep(2000);
}
@Test
public void testSelect() {
// 温馨提示,下面wrapper中的type实际上就是索引JoinField中指定的父子名称,与原生语法是一致的
// case1: hasChild查询,返回的是相关的父文档 所以查询用父文档实体及其mapper
LambdaEsQueryWrapper<Document> documentWrapper = new LambdaEsQueryWrapper<>();
documentWrapper.hasChild("comment", w -> w.eq(FieldUtils.val(Comment::getCommentContent), "test1"));
List<Document> documents = documentMapper.selectList(documentWrapper);
System.out.println(documents);
LambdaEsQueryWrapper<Author> authorWrapper = new LambdaEsQueryWrapper<>();
authorWrapper.hasChild("contact", w -> w.match(FieldUtils.val(Contact::getAddress), "city"));
List<Author> authors = authorMapper.selectList(authorWrapper);
System.out.println(authors);
// case2: hasParent查询,返回的是相关的子文档 所以查询用子文档实体及其mapper
LambdaEsQueryWrapper<Comment> commentWrapper = new LambdaEsQueryWrapper<>();
commentWrapper.like(Comment::getCommentContent, "test");
// 字段名称你也可以不用FieldUtils.val,直接传入字符串也行
commentWrapper.hasParent("document", w -> w.match("content", "father"));
List<Comment> comments = commentMapper.selectList(commentWrapper);
System.out.println(comments);
// case2.1: 孙子查爹的情况
LambdaEsQueryWrapper<Contact> contactWrapper = new LambdaEsQueryWrapper<>();
contactWrapper.hasParent("author", w -> w.eq(FieldUtils.val(Author::getAuthorName), "cat"));
List<Contact> contacts = contactMapper.selectList(contactWrapper);
System.out.println(contacts);
// case2.2: 2.1的简写
LambdaEsQueryWrapper<Contact> contactWrapper1 = new LambdaEsQueryWrapper<>();
// hasParent之所以可以不指定parentType简写是因为框架可以通过@Join注解中指定的父子关系自动推断出其父type,因此用户可以不指定父type直接查询,但hasChild不能简写,因为一个父亲可能有多个孩子,但一个孩子只能有一个亲爹
contactWrapper1.hasParent(w -> w.eq(FieldUtils.val(Author::getAuthorName), "cat"));
List<Contact> contacts1 = contactMapper.selectList(contactWrapper1);
System.out.println(contacts1);
// case3: parentId查询,返回的是相关的子文档,与case2类似,所以查询用子文档实体及其mapper
commentWrapper = new LambdaEsQueryWrapper<>();
commentWrapper.parentId("doc-1", "comment");
List<Comment> commentList = commentMapper.selectList(commentWrapper);
System.out.println(commentList);
contactWrapper = new LambdaEsQueryWrapper<>();
contactWrapper.parentId("author-2", "contact");
List<Contact> contactList = contactMapper.selectList(contactWrapper);
System.out.println(contactList);
}
@Test
public void testUpdate() {
// case1: 父文档/子文档 根据各自的id更新
Document document = new Document();
document.setEsId("doc-1");
document.setTitle("我是隔壁老王标题");
documentMapper.updateById(FIXED_ROUTING, document);
Contact contact = new Contact();
contact.setContactId("contact-2");
contact.setAddress("update address");
contactMapper.updateById(FIXED_ROUTING, contact);
// case2: 父文档/子文档 根据各自条件更新
Comment comment = new Comment();
comment.setCommentContent("update comment content");
LambdaEsUpdateWrapper<Comment> wrapper = new LambdaEsUpdateWrapper<>();
wrapper.eq(Comment::getCommentContent, "test1");
wrapper.routing(FIXED_ROUTING);
commentMapper.update(comment, wrapper);
}
@Test
public void testDelete() {
// case1: 父文档/子文档 根据各自的id删除
documentMapper.deleteById(FIXED_ROUTING, "doc-1");
//case2: 父文档/子文档 根据各自条件删除
LambdaEsQueryWrapper<Comment> wrapper = new LambdaEsQueryWrapper<>();
wrapper.like(Comment::getCommentContent, "test")
.routing(FIXED_ROUTING);
commentMapper.delete(wrapper);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
相关demo可参考源码的test模块->test目录->join包