Як отримати FetchType.LAZY асоціації з JPA та Hibernate у весняному контролері


146

Я маю клас Особи:

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToMany(fetch = FetchType.LAZY)
    private List<Role> roles;
    // etc
}

Маючи ледачий зв’язок "багато до багатьох".

У мене в контролері є

@Controller
@RequestMapping("/person")
public class PersonController {
    @Autowired
    PersonRepository personRepository;

    @RequestMapping("/get")
    public @ResponseBody Person getPerson() {
        Person person = personRepository.findOne(1L);
        return person;
    }
}

А PersonRepository - це саме цей код, написаний відповідно до цього посібника

public interface PersonRepository extends JpaRepository<Person, Long> {
}

Однак у цьому контролері мені справді потрібні ледачі дані. Як я можу викликати його завантаження?

Спроба отримати доступ до нього не вдасться

не вдалося ініціалізувати колекцію ролей: no.dusken.momus.model.Person.roles, не вдалося ініціалізувати проксі - немає сесії

або інші винятки залежно від того, що я намагаюся.

Мій xml-опис , у випадку необхідності.

Дякую.


Чи можете ви написати метод, який створить запит для отримання Personоб'єкта із заданим параметром? У цьому Queryвключіть fetchпункт і завантажте Rolesзанадто для людини.
SudoRahul

Відповіді:


206

Вам доведеться здійснити явний дзвінок на ліниву колекцію, щоб ініціалізувати її (звичайна практика - дзвонити .size() з цією метою). У сплячому режимі існує спеціальний метод для цього ( Hibernate.initialize()), але JPA не має аналогічного. Звичайно, вам доведеться переконатися, що виклик виконано, коли сеанс все ще доступний, тому зазначайте метод контролера за допомогою@Transactional . Альтернативою є створення проміжного сервісного рівня між контролером та сховищем, який міг би відкрити методи, які ініціалізують ледачі колекції.

Оновлення:

Зауважте, що вищезазначене рішення є простим, але призводить до двох різних запитів до бази даних (одного для користувача, іншого для його ролей). Якщо ви хочете досягти кращої ефективності, додайте наступний метод до інтерфейсу сховища Spring Data JPA:

public interface PersonRepository extends JpaRepository<Person, Long> {

    @Query("SELECT p FROM Person p JOIN FETCH p.roles WHERE p.id = (:id)")
    public Person findByIdAndFetchRolesEagerly(@Param("id") Long id);

}

Цей метод використовуватиме застереження про приєднання JPQL для прискореного завантаження асоціації ролей в одному зворотному переході до бази даних, а отже, пом’якшить покарання за ефективність, понесене двома різними запитами у вищезазначеному рішенні.


3
Зауважте, що це просте рішення, але це призводить до двох різних запитів до бази даних (одного для користувача, іншого для його ролей). Якщо ви хочете досягти кращої ефективності, спробуйте написати спеціальний метод, який охоче збирає користувача та пов'язані з ним ролі в один крок, використовуючи JPQL або API критеріїв, як запропонували інші.
zagyi

Зараз я попросив приклад відповіді Хосе, маю визнати, що не цілком розумію.
Matsemann

Будь ласка, перевірте можливе рішення для потрібного способу запиту в моїй оновленій відповіді.
zagyi

7
Цікаво зазначити, що якщо ви просто joinбез цього fetch, набір буде повернутий initialized = false; тому все-таки видаючи другий запит після доступу до набору. fetchє важливим для того, щоб відносини були повністю завантажені та уникали другого запиту.
FGreg

Здається, що проблема з виконанням і отримання, і з’єднання полягає в тому, що критерії предиката приєднання ігноруються, і ви в кінцевому підсумку отримуєте все у списку чи карті. Якщо ви хочете всього, тоді скористайтеся результатом, якщо ви хочете чогось конкретного, то з'єднання, але, як було сказано, приєднання буде порожнім. Це перешкоджає використанню завантаження .LAZY.
К.Ніколас

37

Хоча це старе повідомлення, будь ласка, подумайте про використання @NamedEntityGraph (стійкість Javax) та @EntityGraph (Spring Data JPA). Комбінація працює.

Приклад

@Entity
@Table(name = "Employee", schema = "dbo", catalog = "ARCHO")
@NamedEntityGraph(name = "employeeAuthorities",
            attributeNodes = @NamedAttributeNode("employeeGroups"))
public class EmployeeEntity implements Serializable, UserDetails {
// your props
}

а потім весняне репо як нижче

@RepositoryRestResource(collectionResourceRel = "Employee", path = "Employee")
public interface IEmployeeRepository extends PagingAndSortingRepository<EmployeeEntity, String>           {

    @EntityGraph(value = "employeeAuthorities", type = EntityGraphType.LOAD)
    EmployeeEntity getByUsername(String userName);

}

1
Зауважте, що @NamedEntityGraphце частина API JPA 2.1, яка не реалізована в режимі глибокого сну до версії 4.3.0.
naXa

2
@EntityGraph(attributePaths = "employeeGroups")може бути використаний безпосередньо у сховищі даних Spring для того, щоб коментувати метод без необхідності @NamedEntityGraphна вашому @Entity - менший код, який легко зрозуміти, коли ви відкриваєте репо.
Десіслав камінь

13

У вас є кілька варіантів

  • Напишіть метод у сховище, яке повертає ініціалізовану сутність, як запропонував RJ.

Більше роботи, найкраща продуктивність.

  • Використовуйте OpenEntityManagerInViewFilter, щоб тримати сеанс відкритим для всього запиту.

Менша робота, зазвичай прийнятна у веб-середовищі.

  • Використовуйте клас-помічник для ініціалізації об'єктів, коли це потрібно.

Менш робота, корисна, коли OEMIV не є можливим, наприклад, у додатку Swing, але може бути корисним і для реалізації сховищ, щоб ініціалізувати будь-яку сутність за один кадр.

Для останнього варіанту я написав клас утиліти, JpaUtils щоб ініціювати об'єкти в деякому дефісі.

Наприклад:

@Transactional
public class RepositoryHelper {

    @PersistenceContext
    private EntityManager em;

    public void intialize(Object entity, int depth) {
        JpaUtils.initialize(em, entity, depth);
    }
}

Оскільки всі мої запити є простими REST-дзвінками без надання тощо, транзакція - це в основному весь мій запит. Дякуємо за ваш внесок
Matsemann

Як зробити перший? Я знаю, як написати запит, але не як робити те, що ви говорите. Не могли б ви показати приклад? Було б дуже корисно.
Matsemann

Загйі подав приклад у своїй відповіді, хоча все-таки вказував на мене в правильному напрямку.
Matsemann

Я не знаю, як би називався ваш клас! не закінчений розчин витрачає інший час
Шаді Шериф

Використовуйте OpenEntityManagerInViewFilter, щоб тримати сеанс відкритим для всього запиту. - Неправильна ідея. Я б зробив додатковий запит, щоб отримати всі колекції для моїх організацій.
Ян Хоньський


6

Я думаю, що вам потрібен OpenSessionInViewFilter, щоб тримати сеанс відкритим під час візуалізації перегляду (але це не надто хороша практика).


1
Оскільки я не використовую JSP або щось інше, просто роблю REST-api, @Transactional зробить для мене. Але стане в нагоді в інший час. Дякую.
Matsemann

@Matsemann Я знаю, що вже пізно ... але ви можете використовувати OpenSessionInViewFilter навіть у контролері, а сеанс буде існувати, поки не буде складено відповідь ...
Vishwas Shashidhar

@Matsemann Дякую! Транзакційно-анотація зробила для мене трюк! fyi: Це навіть спрацьовує, якщо ви просто коментуєте суперклас класу відпочинку.
desperateCoder

3

Весняні дані JpaRepository

Дані Spring JpaRepositoryвизначають наступні два методи:

Тим НЕ менше, в вашому випадку, ви не дзвонили ні getOneабо findById:

Person person = personRepository.findOne(1L);

Отже, я припускаю, що findOneметод - це метод, який ви визначили в PersonRepository. Однак findOneметод не дуже корисний у вашому випадку. Оскільки вам потрібно взяти Personразом з rolesколекцією, краще findOneWithRolesзамість цього використовувати метод.

Спеціальні методи весняних даних

Ви можете визначити PersonRepositoryCustomінтерфейс таким чином:

public interface PersonRepository
    extends JpaRepository<Person, Long>, PersonRepositoryCustom { 

}

public interface PersonRepositoryCustom {
    Person findOneWithRoles(Long id);
}

І визначте його реалізацію так:

public class PersonRepositoryImpl implements PersonRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public Person findOneWithRoles(Long id)() {
        return entityManager.createQuery("""
            select p 
            from Person p
            left join fetch p.roles
            where p.id = :id 
            """, Person.class)
        .setParameter("id", id)
        .getSingleResult();
    }
}

Це воно!


Чи є причина, чому ви самі написали запит і не використовували рішення, як EntityGraph у відповіді @rakpan? Чи не призведе це до такого ж результату?
Єроен Вандевельде

Витрати на використання EntityGraph вище, ніж JPQL-запит. Зрештою, вам краще написати запит.
Влад Михальча

Чи можете ви детальніше розібратися над накладними (Звідки вона береться, це помітно, ...)? Тому що я не розумію, чому існує більший накладні витрати, якщо вони обидва генерують один і той же запит.
Єроен Вандевельде

1
Оскільки плани EntityGraphs не кешовані, як JPQL. Це може бути значним хітом для продуктивності.
Влад Михальча

1
Саме так. Мені доведеться написати статтю про це, коли у мене буде час.
Влад

1

Ви можете зробити так само, як це:

@Override
public FaqQuestions getFaqQuestionById(Long questionId) {
    session = sessionFactory.openSession();
    tx = session.beginTransaction();
    FaqQuestions faqQuestions = null;
    try {
        faqQuestions = (FaqQuestions) session.get(FaqQuestions.class,
                questionId);
        Hibernate.initialize(faqQuestions.getFaqAnswers());

        tx.commit();
        faqQuestions.getFaqAnswers().size();
    } finally {
        session.close();
    }
    return faqQuestions;
}

Просто використовуйте faqQuestions.getFaqAnswers (). Size () у вашому контролері, і ви отримаєте розмір, якщо ліниво перетворити список, не виймаючи сам список.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.