Статьи

Хранение иерархических данных в MongoDB

Продолжая путешествие по NoSQL с MongoDB , я хотел бы коснуться одного конкретного случая использования, который встречается очень часто: хранение иерархических отношений документов. MongoDB — это отличное хранилище данных документа, но что если документы имеют отношения родитель-потомок? Можем ли мы эффективно хранить и запрашивать такие иерархии документов? Ответ, конечно, да, мы можем. MongoDB имеет несколько рекомендаций, как хранить деревья в MongoDB . Одно из описанных там решений и довольно широко используемых — использование материализованного пути.

Позвольте мне объяснить, как это работает, предоставив очень простые примеры. Как и в предыдущих статьях, мы будем создавать приложение Spring с использованием недавно выпущенной версии 1.0 проекта Spring Data MongoDB . Наш файл POM содержит очень простые зависимости, не более того.

01
02
03
04
05
06
07
08
09
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
4.0.0
 
mongodb
com.example.spring
0.0.1-SNAPSHOT
jar
 
 
    UTF-8
    3.0.7.RELEASE
 
 
 
     
        org.springframework.data
        spring-data-mongodb
        1.0.0.RELEASE
         
             
                org.springframework
                spring-beans
             
             
                org.springframework
                spring-expression
             
         
     
 
     
        cglib
        cglib-nodep
        2.2
     
 
     
        log4j
        log4j
        1.2.16
     
 
     
        org.mongodb
        mongo-java-driver
        2.7.2
     
 
     
        org.springframework
        spring-core
        ${spring.version}
     
 
     
        org.springframework
        spring-context
        ${spring.version}
     
 
     
        org.springframework
        spring-context-support
        ${spring.version}
     
 
 
 
     
         
            org.apache.maven.plugins
            maven-compiler-plugin
            2.3.2
             
                1.6
                1.6
             
         
    

Чтобы правильно настроить контекст Spring , я буду использовать конфигурационный подход с использованием классов Java. Я все больше и больше выступаю за использование этого стиля, поскольку он обеспечивает строго типизированную конфигурацию, и большинство ошибок могут быть обнаружены во время компиляции, больше нет необходимости проверять ваши XML-файлы. Вот как это выглядит:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.mongodb.hierarchical;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoFactoryBean;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
 
@Configuration
public class AppConfig {
    @Bean
    public MongoFactoryBean mongo() {
        final MongoFactoryBean factory = new MongoFactoryBean();
        factory.setHost( "localhost" );
        return factory;
    }
 
    @Bean
    public SimpleMongoDbFactory mongoDbFactory() throws Exception{
        return new SimpleMongoDbFactory( mongo().getObject(), "hierarchical" );
    }
 
    @Bean
    public MongoTemplate mongoTemplate() throws Exception {
        return new MongoTemplate( mongoDbFactory() );
    }
 
    @Bean
    public IDocumentHierarchyService documentHierarchyService() throws Exception {
        return new DocumentHierarchyService( mongoTemplate() );
    }
}

Это довольно мило и понятно. Спасибо, весна, ребята! Теперь все стандартные вещи готовы. Давайте перейдем к интересной части: документы. Наша база данных будет содержать коллекцию документов, в которой хранятся документы типа SimpleDocument. Мы описываем это, используя аннотации Spring Data MongoDB для SimpleDocument POJO.

01
02
03
04
05
06
07
08
09
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
package com.example.mongodb.hierarchical;
 
import java.util.Collection;
import java.util.HashSet;
 
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
 
@Document( collection = "documents" )
public class SimpleDocument {
    public static final String PATH_SEPARATOR = ".";
 
    @Id private String id;
    @Field private String name;
    @Field private String path;
 
    // We won't store this collection as part of document but will build it on demand
    @Transient private Collection< SimpleDocument > documents = new HashSet< SimpleDocument >();
 
    public SimpleDocument() {
    }
 
    public SimpleDocument( final String id, final String name ) {
        this.id = id;
        this.name = name;
        this.path = id;
    }
 
    public SimpleDocument( final String id, final String name, final SimpleDocument parent ) {
        this( id, name );
        this.path = parent.getPath() + PATH_SEPARATOR + id;
    }
 
    public String getId() {
        return id;
    }
 
    public void setId(String id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getPath() {
        return path;
    }
 
    public void setPath(String path) {
        this.path = path;
    }
 
    public Collection< SimpleDocument > getDocuments() {
        return documents;
    }
}

Позвольте мне объяснить несколько вещей здесь. Во-первых, путь магического свойства: это ключ для построения и запроса через нашу иерархию. Путь содержит идентификаторы всех родителей документа, обычно разделенные каким-то разделителем, в нашем случае просто . (точка) Хранение иерархических отношений документа таким способом позволяет быстро построить иерархию, искать и перемещаться. Во-вторых, обратите внимание на временную коллекцию документов : эта непостоянная коллекция создается постоянным провайдером и содержит все документы-потомки (которые, в случае, также содержат собственных потомков). Давайте посмотрим на это в действии, изучив реализацию метода find :

01
02
03
04
05
06
07
08
09
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
package com.example.mongodb.hierarchical;
 
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
 
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
 
public class DocumentHierarchyService {
    private MongoOperations template;
 
    public DocumentHierarchyService( final MongoOperations template ) {
        this.template = template;
    }
 
    @Override
    public SimpleDocument find( final String id ) {
        final SimpleDocument document = template.findOne(
            Query.query( new Criteria( "id" ).is( id ) ),
            SimpleDocument.class
        );
 
        if( document == null ) {
            return document;
        }
 
        return build(
            document,
            template.find(
                Query.query( new Criteria( "path" ).regex( "^" + id + "[.]" ) ),
                SimpleDocument.class
            )
        );
    }
 
    private SimpleDocument build( final SimpleDocument root, final Collection< SimpleDocument > documents ) {
        final Map< String, SimpleDocument > map = new HashMap< String, SimpleDocument >();
 
        for( final SimpleDocument document: documents ) {
            map.put( document.getPath(), document );
        }
 
        for( final SimpleDocument document: documents ) {
            map.put( document.getPath(), document );
 
            final String path = document
                .getPath()
                .substring( 0, document.getPath().lastIndexOf( SimpleDocument.PATH_SEPARATOR ) );
 
            if( path.equals( root.getPath() ) ) {
                root.getDocuments().add( document );
            } else {
                final SimpleDocument parent = map.get( path );
                if( parent != null ) {
                    parent.getDocuments().add( document );
                }
            }
        }
 
        return root;
    }
}

Как видите, чтобы получить один документ с целой иерархией, нам нужно выполнить всего два запроса (но более оптимальный алгоритм может сократить его до одного запроса). Вот пример иерархии и результат чтения корневого документа из MongoDB

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
template.dropCollection( SimpleDocument.class );
 
final SimpleDocument parent = new SimpleDocument( "1", "Parent 1" );
final SimpleDocument child1 = new SimpleDocument( "2", "Child 1.1", parent );
final SimpleDocument child11 = new SimpleDocument( "3", "Child 1.1.1", child1 );
final SimpleDocument child12 = new SimpleDocument( "4", "Child 1.1.2", child1 );
final SimpleDocument child121 = new SimpleDocument( "5", "Child 1.1.2.1", child12 );
final SimpleDocument child13 = new SimpleDocument( "6", "Child 1.1.3", child1 );
final SimpleDocument child2 = new SimpleDocument( "7", "Child 1.2", parent );
 
template.insertAll( Arrays.asList( parent, child1, child11, child12, child121, child13, child2 ) );
 
...
 
final ApplicationContext context = new AnnotationConfigApplicationContext( AppConfig.class );
final IDocumentHierarchyService service = context.getBean( IDocumentHierarchyService.class );
 
final SimpleDocument document = service.find( "1" );
//  Printing document show following hierarchy:
//
//  Parent 1
//   |-- Child 1.1
//     |-- Child 1.1.1
//     |-- Child 1.1.3
//     |-- Child 1.1.2
//       |-- Child 1.1.2.1
//   |-- Child 1.2

Вот и все. Простая мощная концепция. Конечно, добавление индекса к свойству пути значительно ускорит запрос. Есть много улучшений и оптимизаций, но основная идея должна быть ясна сейчас.

Справка: Хранение иерархических данных в MongoDB от нашего партнера по JCG Андрея Редько в блоге Андрея Редько .