Свойство ленивой загрузки и сеанс.получить проблему

#java #hibernate #lazy-loading

Вопрос:

В режиме гибернации у нас есть два класса со следующими классами с отображением JPA:

 package com.example.hibernate

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

@Entity
public class Foo {
  private long id;
  private Bar bar;

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  @ManyToOne(fetch = FetchType.LAZY)
  public Bar getBar() {
    return bar;
  }

  public void setBar(Bar bar) {
    this.bar = bar;
  }
}

package com.example.hibernate

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;


public class Bar {
  private long id;
  private String title;

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }


  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  } 
}
 

Теперь, когда мы загружаем из базы данных объект из класса Foo с помощью сеанса, например, получаем:

Foo foo = (Foo)сеанс.получить(Foo.class, 1 /* или какой-либо другой идентификатор, существующий в базе данных*/); элемент панели foo является прокси-объектом (в нашем случае прокси javassist, но он может быть cglib в зависимости от используемого вами поставщика байт-кода), который не инициализирован. Если затем вы используете session.get для извлечения объекта Bar, который является членом только что загруженного класса Foo (мы находимся в одном сеансе), Hibernate не выполняет другой запрос БД и извлекает объект из кэша сеанса (первого уровня). Проблема в том, что это прокси-сервер для класса Bar, который не инициализирован, и попытка вызвать этот объект getId() вернет 0, а getTitle() вернет null. Наше текущее решение довольно уродливо и проверяет, является ли объект, возвращаемый get, прокси-сервером. Вот код (сформируйте общую реализацию DAO):

 @SuppressWarnings("unchecked")
@Override
@Transactional(readOnly = true)
public <T extends IEntity> T get(Class<T> clazz, Serializable primaryKey) throws DataAccessException {
  T entity = (T) currentSession().get(clazz, primaryKey);
  if (entity != null) {
    if (LOG.isWarnEnabled()) {
      LOG.warn("Object not found for class "   clazz.getName()   " with primary key "   primaryKey);
    }
  } else if (entity instanceof HibernateProxy){ // TODO: force initialization due to Hibernate bug
    HibernateProxy proxy = (HibernateProxy)entity;
    if (!Hibernate.isInitialized(proxy)) {
      Hibernate.initialize(proxy);
    }
    entity = (T)proxy.getHibernateLazyInitializer().getImplementation();
  }
  return entity;
}
 

Есть ли лучший способ сделать это, не смог найти решение на форуме Hibernate и не нашел проблему в JIRA Hibernate.

Примечание: мы не можем просто использовать foo.getBar() (который правильно инициализирует прокси-сервер) для получения объекта класса Bar, потому что операция session.get для извлечения объекта Bar не знает (или не заботится об этом), что класс Bar также является ленивым членом только что извлеченного объекта Foo.

Ответ №1:

У меня была похожая проблема:

  • Я сделал Session.save(nastyItem), чтобы сохранить объект в сеансе. Тем не менее, я не заполнял покупателя недвижимости, который отображается как update=»false», insert=»false» (это часто случается, когда у вас есть составленный первичный ключ, затем вы сопоставляете множество к одному как insert=»false», update=»false»)
  • Я делаю запрос на загрузку списка элементов, и элемент, который я только что сохранил, оказывается частью результирующего набора
  • что же теперь идет не так? Hibernate видит, что элемент уже был в кэше, и Hibernate не заменяет (вероятно, чтобы не нарушать мою предыдущую ссылку на nastyItem) его новым загруженным значением, но использует МОЙ nastyItem, который я сам поместил в кэш сеанса. Еще хуже то, что теперь ленивая загрузка покупателя нарушена: она содержит null.

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

Ответ №2:

На самом деле я не видел этой проблемы, хотя мы получаем периодические ошибки ленивой загрузки — так что, возможно, у нас та же проблема, в любом случае, можно ли использовать другой сеанс для загрузки объекта Bar — я бы ожидал, что он должен загружаться с нуля…

Комментарии:

1. Загрузка в другом сеансе-это не вариант: реальная проблема более сложна, но короткий ответ заключается в том, что мы не можем.

Ответ №3:

Я не в состоянии воспроизвести поведение, которое вы видите. Вот мой код:

 @Entity
public class Foo {
    private Long id; private String name; private Bar bar;

    public Foo() { }
    public Foo(String name) { this.name = name; }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    @Basic
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    @ManyToOne(fetch = FetchType.LAZY)
    public Bar getBar() { return bar; }
    public void setBar(Bar bar) { this.bar = bar; }
}

@Entity
public class Bar {
    private Long id; private String name;

    public Bar() { }
    public Bar(String name) { this.name = name; }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    @Basic
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

    public void testGets() {
        SessionFactory sf = new AnnotationConfiguration()
            .addPackage("hibtest")
                .addAnnotatedClass(Foo.class)
                .addAnnotatedClass(Bar.class)
            .configure().buildSessionFactory();
        Session session = null;
        Transaction txn = null;

        // Create needed data
        try {
            session = sf.openSession();
            txn = session.beginTransaction();

            // Create a Bar
            Bar bar = new Bar("Test Bar");
            session.save(bar);

            // Create a Foo
            Foo foo = new Foo("Test Foo");
            session.save(foo);

            foo.setBar(bar);

            txn.commit();
        } catch (HibernateException ex) {
            if (txn != null) txn.rollback();
            throw ex;
        } finally {
            if (session != null) session.close();
        }

        // Try the fetch
        try {
            session = sf.openSession();
            Foo foo = (Foo) session.get(Foo.class, 1L);
            Bar bar = (Bar) session.get(Bar.class, 1L);
            System.out.println(bar.getName());
        } finally {
            if (session != null) session.close();
        }
    }
 

И все это прекрасно работает, как и следовало ожидать.

Ответ №4:

Вам действительно нужно выполнять ленивую загрузку?
Не могли бы вы вместо этого установить FetchType в EAGER и всегда загружать его (правильно) с помощью соединения?

Комментарии:

1. Мне действительно нужно выполнять ленивую загрузку, так как это свойство не всегда необходимо (например, представление списка объектов Foo).

2. Я, как правило, не использую ленивую загрузку, поэтому в ситуациях, когда иногда вам нужны дополнительные свойства, а иногда нет, я обычно пишу два разных метода — один, который выполняет объединение, а другой-нет. Затем это дает вам возможность настроить каждый запрос так, чтобы он делал именно то, что вы хотите.

Ответ №5:

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

Режим гибернации должен автоматически определяться всякий раз, когда ему необходимо выполнить выборку или запись в базу данных. Если вы выдаете getProperty() от прокси-сервера, hibernate или любой другой поставщик jpa должен извлечь соответствующую строку из базы данных.

Единственная ситуация, в которой я не уверен, что hibernate достаточно умен, — это если вы выдадите save (), а затем выдадите get() с идентификатором сохраненного объекта, может возникнуть проблема, если save() не сбросил объект в бд.