Hibernate: лучший способ вытащить все ленивые коллекции

92

Что я имею:

@Entity
public class MyEntity {
  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
  @JoinColumn(name = "myentiy_id")
  private List<Address> addreses;

  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
  @JoinColumn(name = "myentiy_id")
  private List<Person> persons;

  //....
}

public void handle() {

   Session session = createNewSession();
   MyEntity entity = (MyEntity) session.get(MyEntity.class, entityId);
   proceed(session); // FLUSH, COMMIT, CLOSE session!

   Utils.objectToJson(entity); //TROUBLES, because it can't convert to json lazy collections
}

Какая проблема:

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

Какой раствор (крупный раствор):

а) Перед закрытием сеанса принудительно спящий режим для извлечения ленивых коллекций

entity.getAddresses().size();
entity.getPersons().size();

....

б) Возможно, более изящный способ - использовать @Fetch(FetchMode.SUBSELECT)аннотацию

Вопрос:

Каков наилучший / распространенный / более элегантный способ сделать это? Означает преобразовать мой объект в JSON.

VB_
источник

Ответы:

102

Используйте Hibernate.initialize()внутри @Transactionalдля инициализации ленивых объектов.

 start Transaction 
      Hibernate.initialize(entity.getAddresses());
      Hibernate.initialize(entity.getPersons());
 end Transaction 

Теперь вне транзакции вы можете получать ленивые объекты.

entity.getAddresses().size();
entity.getPersons().size();
Прабхакаран Рамасвами
источник
1
Выглядит привлекательно). Насколько я понимаю, если я буду использовать @Fetch (FetchMode.SUBSELECT), то смогу вызвать Hibernate.initialize только один раз, чтобы получить все коллекции. Я прав?
VB_
4
А как вы справляетесь, когда получаете коллекцию MyEntity?
Alexis Dufrenoy
1
Если вы вызываете какой-либо метод, например size () для коллекции в транзакции, он также инициализирует его, поэтому ваш пример после инициализации не самый лучший. Здесь сказано, что «Hibernate.initialize (...)» семантически лучше, чем collection.size (), так что у вас есть лучший совет.
Тристан
7

Вы можете перемещаться по геттерам объекта Hibernate в той же транзакции, чтобы гарантировать, что все ленивые дочерние объекты будут быстро извлечены с помощью следующего универсального вспомогательного класса:

HibernateUtil.initializeObject (myObject, «my.app.model»);

package my.app.util;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;

import org.aspectj.org.eclipse.jdt.core.dom.Modifier;
import org.hibernate.Hibernate;

public class HibernateUtil {

public static byte[] hibernateCollectionPackage = "org.hibernate.collection".getBytes();

public static void initializeObject( Object o, String insidePackageName ) {
    Set<Object> seenObjects = new HashSet<Object>();
    initializeObject( o, seenObjects, insidePackageName.getBytes() );
    seenObjects = null;
}

private static void initializeObject( Object o, Set<Object> seenObjects, byte[] insidePackageName ) {

    seenObjects.add( o );

    Method[] methods = o.getClass().getMethods();
    for ( Method method : methods ) {

        String methodName = method.getName();

        // check Getters exclusively
        if ( methodName.length() < 3 || !"get".equals( methodName.substring( 0, 3 ) ) )
            continue;

        // Getters without parameters
        if ( method.getParameterTypes().length > 0 )
            continue;

        int modifiers = method.getModifiers();

        // Getters that are public
        if ( !Modifier.isPublic( modifiers ) )
            continue;

        // but not static
        if ( Modifier.isStatic( modifiers ) )
            continue;

        try {

            // Check result of the Getter
            Object r = method.invoke( o );

            if ( r == null )
                continue;

            // prevent cycles
            if ( seenObjects.contains( r ) )
                continue;

            // ignore simple types, arrays und anonymous classes
            if ( !isIgnoredType( r.getClass() ) && !r.getClass().isPrimitive() && !r.getClass().isArray() && !r.getClass().isAnonymousClass() ) {

                // ignore classes out of the given package and out of the hibernate collection
                // package
                if ( !isClassInPackage( r.getClass(), insidePackageName ) && !isClassInPackage( r.getClass(), hibernateCollectionPackage ) ) {
                    continue;
                }

                // initialize child object
                Hibernate.initialize( r );

                // traverse over the child object
                initializeObject( r, seenObjects, insidePackageName );
            }

        } catch ( InvocationTargetException e ) {
            e.printStackTrace();
            return;
        } catch ( IllegalArgumentException e ) {
            e.printStackTrace();
            return;
        } catch ( IllegalAccessException e ) {
            e.printStackTrace();
            return;
        }
    }

}

private static final Set<Class<?>> IGNORED_TYPES = getIgnoredTypes();

private static boolean isIgnoredType( Class<?> clazz ) {
    return IGNORED_TYPES.contains( clazz );
}

private static Set<Class<?>> getIgnoredTypes() {
    Set<Class<?>> ret = new HashSet<Class<?>>();
    ret.add( Boolean.class );
    ret.add( Character.class );
    ret.add( Byte.class );
    ret.add( Short.class );
    ret.add( Integer.class );
    ret.add( Long.class );
    ret.add( Float.class );
    ret.add( Double.class );
    ret.add( Void.class );
    ret.add( String.class );
    ret.add( Class.class );
    ret.add( Package.class );
    return ret;
}

private static Boolean isClassInPackage( Class<?> clazz, byte[] insidePackageName ) {

    Package p = clazz.getPackage();
    if ( p == null )
        return null;

    byte[] packageName = p.getName().getBytes();

    int lenP = packageName.length;
    int lenI = insidePackageName.length;

    if ( lenP < lenI )
        return false;

    for ( int i = 0; i < lenI; i++ ) {
        if ( packageName[i] != insidePackageName[i] )
            return false;
    }

    return true;
}
}
Флориан Сагер
источник
Спасибо за этот ответ. Я знаю, что это было давно, но я пытался решить эту проблему, и это шло медленно, пока я не прочитал здесь ваш код. Я также добавил ifs в начало второго метода initializeObject (object, visibleObjects, insidePackageName): if (object instanceof List) { for(Object item : (List<Object>) object) { initializeObject(item, seenObjects, insidePackageName); } return; } else if (object instanceof Set) { for(Object item : (Set<Object>) object) { initializeObject(item, seenObjects, insidePackageName); } return; } итерация списков в противном случае игнорируется.
Чип
Что, если SecurityException выбрасывается в o.getClass (). GetMethods () ;?
Алексей Кислицын
6

Не лучшее решение, но вот что у меня получилось:

1) Аннотируйте получатель, который вы хотите инициализировать, с помощью этой аннотации:

@Retention(RetentionPolicy.RUNTIME)
public @interface Lazy {

}

2) Используйте этот метод (можно поместить в общий класс или изменить T с помощью класса Object) для объекта после чтения его из базы данных:

    public <T> void forceLoadLazyCollections(T entity) {

    Session session = getSession().openSession();
    Transaction tx = null;
    try {

        tx = session.beginTransaction();
        session.refresh(entity);
        if (entity == null) {
            throw new RuntimeException("Entity is null!");
        }
        for (Method m : entityClass.getMethods()) {

            Lazy annotation = m.getAnnotation(Lazy.class);
            if (annotation != null) {
                m.setAccessible(true);
                logger.debug(" method.invoke(obj, arg1, arg2,...); {} field", m.getName());
                try {
                    Hibernate.initialize(m.invoke(entity));
                }
                catch (Exception e) {
                    logger.warn("initialization exception", e);
                }
            }
        }

    }
    finally {
        session.close();
    }
}
Дамиан
источник
Я использую session.refresh в итерации для загрузки lazyCollections. и каждый раз, когда я запускаю свою программу только для одной из моих сущностей, я загружал LazyInitializationException и другие коллекции после вызова session.refresh. Как такое могло случиться
саба сафави
5

Поместите Utils.objectToJson (entity); позвонить перед закрытием сеанса.

Или вы можете попробовать установить режим выборки и поиграть с таким кодом

Session s = ...
DetachedCriteria dc = DetachedCriteria.forClass(MyEntity.class).add(Expression.idEq(id));
dc.setFetchMode("innerTable", FetchMode.EAGER);
Criteria c = dc.getExecutableCriteria(s);
MyEntity a = (MyEntity)c.uniqueResult();
СтаниславL
источник
FetchMode.EAGER устарел. Документ javadoc рекомендует использовать FetchMode.JOIN прямо сейчас.
Alexis Dufrenoy
4

В Hibernate 4.1.6 представлена ​​новая функция для решения этих проблем с ленивыми ассоциациями. Когда вы включаете свойство hibernate.enable_lazy_load_no_trans в hibernate.properties или в hibernate.cfg.xml, у вас больше не будет LazyInitializationException.

Для получения дополнительной информации см .: https://stackoverflow.com/a/11913404/286588

Ферма
источник
3
На самом деле это антипаттерн. Для получения дополнительной информации: vladmihalcea.com/…
Ph03n1x
3

Когда вам нужно получить несколько коллекций, вам необходимо:

  1. ПРИСОЕДИНЯЙТЕСЬ ПОЛУЧИТЬ одну коллекцию
  2. Используйте Hibernate.initializeдля остальных коллекций.

Итак, в вашем случае вам понадобится первый запрос JPQL, подобный этому:

MyEntity entity = session.createQuery("select e from MyEntity e join fetch e.addreses where e.id 
= :id", MyEntity.class)
.setParameter("id", entityId)
.getSingleResult();

Hibernate.initialize(entity.persons);

Таким образом, вы можете достичь своей цели с помощью 2 запросов SQL и избежать декартова произведения.

Влад Михалча
источник
Привет Влад, это работает, если я позвоню, Hibernate#initialize(entity.getSubSet())если getSubSet вернется Collections.unmodifyableSet(this.subSet). Я пробовал, но не получилось. Подложка коллекции - «PersistentSet». Та же история с #size()
звонком
Но, возможно, проблема в том, что я позже вызываю contains, а мои equals используют прямой доступ к полю, а не геттеры ..
Вадим Кирильчук
Это сработает, если вы выполните шаги, указанные в моем ответе.
Влад Михалча,
2

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

Давек
источник
0

Попробуйте использовать Gson библиотеку для преобразования объектов в Json

Пример с сервлетами:

  List<Party> parties = bean.getPartiesByIncidentId(incidentId);
        String json = "";
        try {
            json = new Gson().toJson(parties);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(json);
Мохамед Надь
источник
0

если вы используете репозиторий jpa, установите properties.put ("hibernate.enable_lazy_load_no_trans", true); в jpaPropertymap

userSait
источник
0

Вы можете использовать @NamedEntityGraphаннотацию к своей сущности, чтобы создать загружаемый запрос, который устанавливает, какие коллекции вы хотите загрузить в свой запрос.

Основное преимущество этого выбора заключается в том, что спящий режим выполняет один-единственный запрос для извлечения объекта и его коллекций, и только тогда, когда вы решите использовать этот график, например:

Конфигурация объекта

@Entity
@NamedEntityGraph(name = "graph.myEntity.addresesAndPersons", 
attributeNodes = {
    @NamedAttributeNode(value = "addreses"),
    @NamedAttributeNode(value = "persons"
})

Применение

public MyEntity findNamedGraph(Object id, String namedGraph) {
        EntityGraph<MyEntity> graph = em.getEntityGraph(namedGraph);

        Map<String, Object> properties = new HashMap<>();
        properties.put("javax.persistence.loadgraph", graph);

        return em.find(MyEntity.class, id, properties);
    }
гуторы
источник
0

Есть какое-то недоразумение насчет ленивых коллекций в JPA-Hibernate. Прежде всего, давайте проясним, почему попытка прочитать ленивую коллекцию вызывает исключения, а не просто возвращает NULL для преобразования или дальнейшего использования?.

Это потому, что пустые поля в базах данных, особенно в соединенных столбцах, имеют значение, а не просто непредставленное состояние, как языки программирования. когда вы пытаетесь интерпретировать ленивую коллекцию в значение Null, это означает (на стороне хранилища данных), что между этими объектами нет отношений, и это не так. поэтому выброс исключения - это своего рода передовой опыт, и вам придется иметь дело с этим, а не с Hibernate.

Итак, как упоминалось выше, я рекомендую:

  1. Отсоедините желаемый объект перед его изменением или использованием сеанса без сохранения состояния для запроса
  2. Преобразуйте ленивые поля в желаемые значения (ноль, ноль и т. Д.)

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

Мохсен Госпожа
источник