Lazy/Eager loading/fetching in Neo4j/Spring-Data

У меня простая установка и я столкнулся с головоломной (по крайней мере для меня) проблемой:

У меня есть три pojos, которые связаны друг с другом:

@NodeEntity
public class Unit {
    @GraphId Long nodeId;
    @Indexed int type;
    String description;
}


@NodeEntity
public class User {
    @GraphId Long nodeId;
    @RelatedTo(type="user", direction = Direction.INCOMING)
    @Fetch private Iterable worker;
    @Fetch Unit currentUnit;

    String name;

}

@NodeEntity
public class Worker {
    @GraphId Long nodeId;
    @Fetch User user;
    @Fetch Unit unit;
    String description;
}

Итак, у вас есть User-Worker-Unit с "currentunit", который отмечает в пользователе, что позволяет перейти непосредственно к "current unit". Каждый пользователь может иметь несколько работников, но один работник назначается только одному подразделению (одно подразделение может иметь несколько работников).

Мне интересно, как управлять аннотацией @Fetch на "User.worker". На самом деле я хочу, чтобы она запускалась только при необходимости, потому что большую часть времени я работаю только с "Worker".

Я просмотрел http://static.springsource.org/spring-data/data-neo4j/docs/2.0.0.RELEASE/reference/html/ и мне не совсем понятно:

  • worker является итерабельным, потому что он должен быть только для чтения (входящее отношение) - в документации это указано четко, но в примерах чаще всего используется ''Set''. Почему? Или это не имеет значения...
  • Как заставить worker загружаться только при доступе? (ленивая загрузка)
  • Почему я должен аннотировать @Fetch даже простые отношения (worker.unit). Разве нет лучшего способа? У меня есть другая сущность с МНОГИМИ такими простыми отношениями - я действительно хочу избежать необходимости загружать весь граф только потому, что мне нужны свойства одного объекта.
  • Мне не хватает конфигурации Spring, чтобы он работал с ленивой загрузкой?
  • Есть ли способ загрузить любые отношения (которые не помечены как @Fetch) через дополнительный вызов?

Насколько я понимаю, эта конструкция загружает всю базу данных, как только мне нужен Worker, даже если мне не важен пользователь большую часть времени.

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

------- Update -------

Я работаю с neo4j уже довольно давно и нашел решение для вышеописанной проблемы, которое не требует постоянного вызова fetch (и, следовательно, не загружает весь граф). Единственный минус: это runtime aspect:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.neo4j.annotation.NodeEntity;
import org.springframework.data.neo4j.support.Neo4jTemplate;

import my.modelUtils.BaseObject;

@Aspect
public class Neo4jFetchAspect {

    // thew neo4j template - make sure to fill it 
    @Autowired private Neo4jTemplate template;

    @Around("modelGetter()")
    public Object autoFetch(ProceedingJoinPoint pjp) throws Throwable {
        Object o = pjp.proceed();
        if(o != null) {
            if(o.getClass().isAnnotationPresent(NodeEntity.class)) {
                if(o instanceof BaseObject) {
                    BaseObject bo = (BaseObject)o;
                    if(bo.getId() != null && !bo.isFetched()) {
                        return template.fetch(o);
                    }
                    return o;
                }
                try {
                    return template.fetch(o);
                } catch(MappingException me) {
                    me.printStackTrace();
                }
            }
        }
        return o;
    }

    @Pointcut("execution(public my.model.package.*.get*())")
    public void modelGetter() {}

}

Вам просто нужно адаптировать classpath, на котором должен применяться аспект: my.model.package. .get())")

Я применяю аспект ко ВСЕМ методам get в моих классах моделей. Это требует нескольких предварительных условий:

  • Вы ДОЛЖНЫ использовать геттеры в ваших классах модели (аспект не работает на публичных атрибутах - которые вы не должны использовать в любом случае)
  • все классы модели находятся в одном пакете (поэтому вам нужно немного адаптировать код) - я думаю, вы можете адаптировать фильтр
  • aspectj как компонент времени выполнения необходим (немного сложно, когда вы используете tomcat) - но это работает :)
  • ВСЕ классы модели должны реализовать интерфейс BaseObject, который обеспечивает:

    public interface BaseObject { public boolean isFetched(); }

Это предотвращает двойную выборку. Я просто проверяю подкласс или атрибут, который является обязательным (т.е. имя или что-то еще, кроме nodeId), чтобы увидеть, действительно ли он получен. Neo4j создаст объект, но заполнит только nodeId и оставит все остальное нетронутым (так что все остальное будет NULL).

т.е.

@NodeEntity
public class User implements BaseObject{
    @GraphId
    private Long nodeId;

        String username = null;

    @Override
    public boolean isFetched() {
        return username != null;
    }
}

Если кто-то найдет способ сделать это без этого странного обходного пути, пожалуйста, добавьте свое решение :), потому что это работает, но я бы хотел получить решение без aspectj.

Дизайн базового объекта, который не требует проверки пользовательского поля

Одной из оптимизаций было бы создание базового класса вместо интерфейса, который фактически использует булево поле (булево loaded) и проверяет его (так что вам не нужно беспокоиться о ручной проверке)

public abstract class BaseObject {
    private Boolean loaded;
    public boolean isFetched() {
        return loaded != null;
    }
    /**
     * getLoaded will always return true (is read when saving the object)
     */
    public Boolean getLoaded() {
        return true;
    }

    /**
     * setLoaded is called when loading from neo4j
     */
    public void setLoaded(Boolean val) {
        this.loaded = val;
    }
}

Это работает, потому что при сохранении объекта "true" возвращается для loaded. Когда аспект смотрит на объект, он использует isFetched(), которая - если объект еще не получен - возвращает null. Когда объект получен, вызывается setLoaded, и переменная loaded устанавливается в true.

Как предотвратить срабатывание ленивой загрузки в jackson?

(В качестве ответа на вопрос в комментарии - обратите внимание, что я еще не опробовал это, так как у меня не было этой проблемы).

В jackson я предлагаю использовать пользовательский сериализатор (см. например http://www.baeldung.com/jackson-custom-serialization ). Это позволит вам проверять сущность перед получением значений. Вы просто делаете проверку, если она уже извлечена, и либо продолжаете всю сериализацию, либо просто используете id:

public class ItemSerializer extends JsonSerializer {
    @Override
    public void serialize(BaseObject value, JsonGenerator jgen, SerializerProvider provider)
      throws IOException, JsonProcessingException {
        // serialize the whole object
        if(value.isFetched()) {
            super.serialize(value, jgen, provider);
            return;
        }
        // only serialize the id
        jgen.writeStartObject();
        jgen.writeNumberField("id", value.nodeId);
        jgen.writeEndObject();
    }
}

Spring Configuration

Это пример конфигурации Spring, которую я использую - вам нужно настроить пакеты под ваш проект:




    
    

     

    
         
    
        
     

AOP config

это /META-INF/aop.xml для того, чтобы это работало:


    
        
            
            
        
        
            
            
        
    

15
задан Niko 14 June 2016 в 12:25
поделиться