Атрибут Lazy имеет значение null внутри транзакции после создания

У меня есть небольшой пример с некоторыми сопоставлениями get / post и вызовами JpaRepository в Spring Boot.

Во-первых, у меня есть два класса сущностей:

@Entity
@Table(name = "stock")
public class Stock extends BaseEntity
{
    @Column(name = "value")
    public String value;

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

@Entity
@Table(name = "stock_item")
public class StockItem extends BaseEntity
{

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "stock_id", insertable = false, updatable = false)
    public Stock stock;

    @Column(name = "stock_id")
    public Long stockId;

    @Column(name = "value")
    public String value;
}

У меня связь "многие к одному" от StockItem к Stock. Я вставляю Stock и имею контроллер, как показано ниже:

@Autowired
public Controller(StockItemRepository stockItemRepository) {
    this.stockItemRepository = stockItemRepository;
}

@RequestMapping("/")
@Transactional(readOnly = true)
public String get() {

    List<StockItem> stockItemList = stockItemRepository.getItemsById(1L);
    System.out.println("TX MANAGER: " + TransactionSynchronizationManager.isActualTransactionActive());

    for (StockItem stockItem : stockItemList) {
        System.out.println(stockItem.getStock().getValue());
    }


    return "get";
}

@RequestMapping("/fromSave")
@Transactional
public String post() {
    StockItem stockItem = new StockItem();
    stockItem.setStockId(1L);
    stockItemRepository.saveAndFlush(stockItem);

    System.out.println("saveCalled");

    return get();
}

а getItemsById в репозитории определяется следующим образом:

@Query("FROM StockItem si " +
        "JOIN FETCH si.stock stk " +
        "WHERE si.stockId = :id")
List<StockItem> getItemsById(@Param("id") Long id);

Насколько я понимаю, когда я вызываю метод post:

  • он создает новый элемент
  • устанавливает идентификатор связанного атрибута
  • сохраняет и завершает транзакцию

Вот где все становится странно ... Я вызываю get после публикации и делаю вышеуказанный вызов репозитория, который имеет объединенную выборку, и когда я вызываю stockitem.getStock().getValue(), я получаю нулевой указатель, когда я ожидаю LazyInitializationException.

Если я вызываю get() из сопоставления вне класса, он успешно загружает связанный объект.

Я даже удалил аннотацию @Transaction из get, а также объединение-выборка из моего запроса и снова, если я вызываю извне класса, он работает и из сообщения, он вылетает с NullPointerException.

Я поместил get внутрь TransactionTemplate.execute(), и я все еще получаю NullPointerException при вызове изнутри класса.

Итак, основные вопросы:

  • Почему я получаю NullPointerException вместо LazyInitializationException?
  • В чем заключается магия транзакции при отсутствии транзакции, но успешном получении ленивого атрибута?
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Версия Java на основе версии загрузки
Версия Java на основе версии загрузки
Если вы зайдете на официальный сайт Spring Boot , там представлен start.spring.io , который упрощает создание проектов Spring Boot, как показано ниже.
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
2
0
621
2

Ответы 2

@Entity
@Table(name = "stock_item")
public class StockItem extends BaseEntity
{

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "stock_id", insertable = false, updatable = false) //here is your problem
    public Stock stock;

    @Column(name = "stock_id")
    public Long stockId; // why explicitly define a separate column for foreign key after mapping it above

    @Column(name = "value")
    public String value;
}

с insertable = false и updatable = false он не будет вставлен в вашу БД и не разрешит обновление, поэтому вы получаете NullPointerException. Вы должны хотя бы разрешить вставку, чтобы выполнить запрос на основе внешнего ключа stock_id

ОБНОВИТЬ

Измените свой класс Entity с помощью доступ на основе собственности:

@Entity
@Table(name = "stock_item")
public class StockItem extends BaseEntity
{
    private Stock stock; // variables should always be private since you have getters and setters
    private String value;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "stock_id", updatable = false)
    public Stock getStock() {
        return stock;
    }

    public void setStock(Stock stock) {
        this.stock = stock;
    }

    @Basic
    @Column(name = "value")
    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

это дает мне «невозможно построить Hibernate SessionFactory» при сборке. потому что это повторяющийся столбец

dez 30.05.2018 16:30

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

sam 30.05.2018 16:33

смотри мое обновление. Посмотрим, решится ли ваша проблема или нет

sam 30.05.2018 16:41

да, это работает, однако это удаляет stockID из объекта .. который мне нужен.

dez 30.05.2018 16:45

также почему get () разрешается правильно при доступе за пределами класса vs из внутреннего вызова? действительно ищу объяснение больше, чем ответ

dez 30.05.2018 16:49

Престижность .. Вы можете нажать на галочку, чтобы принять этот ответ как решение вашего вопроса. Удачного кодирования !!!

sam 30.05.2018 16:49

Это старая ошибка.

sam 30.05.2018 16:50

См. Этот сайт для получения дополнительных ссылок мысли-on-java.org/…

sam 30.05.2018 16:50

Мне нужно сохранить атрибут Long id, потому что это сокращает ненужные вызовы db позже в моей системе. Я хочу, чтобы вся ассоциация оставалась ленивой, если она мне явно не нужна. Так что я не чувствую, что ты ответил на мой вопрос

dez 30.05.2018 16:57

проблема не в сохранении, экономия работает. Когда я отлаживаю идентификатор, сохраняется, но проблема связана с извлечением, почему он не извлекается правильно?

dez 30.05.2018 17:22

также вставляемый и обновляемый работают только с включением атрибута в операторы вставки и обновления

dez 31.05.2018 13:05

Проблема здесь в том, что вы неправильно используете JPA. Как вы, похоже, знаете, судя по комментариям к другому ответу, вы дважды сопоставили столбец stock_id. Когда-то как отношения многие-к-одному

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "stock_id", insertable = false, updatable = false)
public Stock stock;

и однажды как простой столбец

@Column(name = "stock_id")
public Long stockId;

Когда вы устанавливаете простой столбец и сбрасываете изменения, как в вашем методе post(), происходит следующее:

  • значение устанавливается в простой столбец. Ссылка все еще null.
  • значение сохраняется в базе данных. Ссылка все еще null.

Вызов репозитория найдет идентификатор StockItem в Контекст постоянства и вернет этот экземпляр, то есть тот же самый, что использовался в методе post, со ссылкой все еще null.

What is the transaction magic behind having no transaction but successfully fetching a lazy attribute??

Здесь нет никакой магии. Спецификации fetch используются только для обхода объекта. Запросы JPQL этого не учитывают.

Остается незаданный вопрос: как исправить ситуацию?

Очевидное решение - потерять простой столбец и просто использовать ссылки на сущности, как задумано JPA.

Вы не хотите этого делать, чтобы где-то избежать доступа к БД. Но пока вы обращаетесь только к id упомянутого Stock, он не должен инициализироваться. Кажется, что это должно быть возможно только с Ленивая загрузка.

В качестве альтернативы я бы предложил удалить связь «многие к одному» и создать репозиторий для Stock и вручную загружать его при необходимости.

тогда почему инициализируется «Stock», если я вызываю get () через сопоставление запросов, а не вызываю get () из возврата post ()? Соединение выборки, похоже, не работает в последнем сценарии

dez 31.05.2018 10:22

Другие вопросы по теме