【问题标题】:Lazy attribute is null inside transaction after creation创建后事务内部的惰性属性为空
【发布时间】:2018-11-09 10:32:22
【问题描述】:

我有一个小示例,其中包含 Spring Boot 中的一些 get/post 映射和 JpaRepository 调用。

首先我有两个实体类:

@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;
}

我有一个从 StockItemStock 的多对一关联。 我插入一个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 方法时:

  • 它会创建一个新项目
  • 设置关联属性的id
  • 保存并结束交易

这就是事情变得奇怪的地方...... 我在帖子之后调用 get 并进行上述存储库调用,它有一个 join fetch,当我调用 stockitem.getStock().getValue() 时,当我期望 LazyInitializationException 时,我得到一个空指针。

如果我在类外部从映射中调用get(),它会成功加载关联的对象。

我什至从 get 中删除了 @Transaction 注释,以及 再次从我的查询中获取连接,如果我从它工作的类之外和从帖子中调用它,它会崩溃并显示NullPointerException

我已将get 放在TransactionTemplate.execute() 中,当从班级内部调用时,我仍然得到NullPointerException

所以主要问题是:

  • 为什么我得到的是NullPointerException 而不是LazyInitializationException
  • 没有事务但成功获取惰性属性背后的事务魔法是什么??

【问题讨论】:

    标签: spring-boot spring-data-jpa spring-transactions


    【解决方案1】:

    这里的问题是您在滥用 JPA。从另一个答案的 cmets 来看,您似乎已经意识到,您已经两次映射了 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

    存储库调用将在 Persistence Context 中找到 StockItem 的 id 并返回该实例,即与 post 方法中使用的完全相同,引用仍为 null

    没有事务但成功获取惰性属性背后的事务魔法是什么??

    这里不涉及魔法。 fetch 规范仅用于对象遍历。 JPQL 查询不支持这些。

    悬而未决的问题仍然存在:如何解决这种情况?

    明显的解决方法是丢失简单的列,而只使用 JPA 预期的实体引用。

    您不想这样做是为了避免在某处访问数据库。但只要您只访问引用的Stockid,它就不应该被初始化。因此,这似乎只需要 Lazy Fetching 就可以实现。

    或者,我建议删除多对一关系并为Stock 创建一个存储库,并在需要时手动加载它。

    【讨论】:

    • 那么,如果我通过请求映射调用 get() 与从 post() 的返回调用 get() 相比,为什么会初始化“Stock”? fetch 连接似乎在后一种情况下不起作用
    【解决方案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 = falseupdatable = false,它不会插入到您的数据库中,也不会允许更新,因此您会得到NullPointerException。您至少应该允许插入以便基于外键 stock_id 运行查询

    更新

    使用基于属性的访问权限更改您的实体类:

    @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”。因为这是一个重复的列
    • 是的,您肯定会遇到该错误,因为您使用了基于字段的访问而不是基于属性的访问。等待更新您的答案。
    • 查看我的更新。看看你的问题能不能解决
    • 是的,但是这可行,这是从实体中删除 stockID .. 这是我需要的。
    • 还有为什么 get() 在类外访问时与从内部调用中正确解析?真的在寻找解释而不是答案
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-03-26
    • 2017-05-19
    • 1970-01-01
    • 1970-01-01
    • 2021-01-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多