Hibernate执行INSERT前先执行SELECT的原因及空指针问题解决
问题解析与解决方案
嘿,我来帮你把这个问题拆解开,从根源到解决办法,再到save()和persist()的区别,都给你讲明白:
一、为啥Hibernate在INSERT前先跑SELECT?
其实这是Spring Data JPA的save()方法的判断逻辑在搞事情:save()需要先搞清楚你要存的这个Book是全新的实体(还没进数据库)还是已经存在的实体(之前存过,现在只是更新)。
- 按道理,你用了
IDENTITY自增主键,只要bookId是null,就应该判定为新实体,直接INSERT就行。但如果你的实体类有问题(比如主键的getter写错了,或者字段映射有问题),Spring Data JPA没法通过主键值判断状态,就会跑去数据库查一下:“这个主键对应的记录存在不?”,以此来确定是插新数据还是更新旧数据。 - 另外,如果你的实体没正确写
equals()和hashCode(),或者主键的访问方式(比如用字段访问但又加了getter)不一致,也会触发这个前置查询。
二、怎么解决这个空指针异常?
看你的代码,有几个明显的坑,一个个来填:
- Book的author字段设置写错了!
你的Book实体里明明是private Author author;,结果保存的时候写了b1.setAuthorId(new Author(authorName));——这方法名完全不对啊!应该是b1.setAuthor(...),而且要确保Author类有能接收authorName的构造函数,或者你先new一个Author对象,再给它设authorName:Author author = new Author(); author.setAuthorName(authorName); b1.setAuthor(author); - 检查Book的bookId的getter方法
错误栈明确说Getter for property 'bookId' threw exception,说明你的Book类里getBookId()肯定有问题:要么是没写这个getter,要么是getter里有错误逻辑(比如返回null的时候瞎操作)。Spring Data JPA要靠这个getter拿主键值判断实体状态,所以必须写对:
就这么简单,别加多余的东西。public Integer getBookId() { return bookId; } - 把级联类型改得保守点
你给@ManyToOne加了CascadeType.ALL,这太猛了——ALL包含了删除、刷新等操作,其实你只需要保存Book的时候自动存关联的Author,用CascadeType.PERSIST或者CascadeType.MERGE就够了,避免不必要的级联操作搞出问题。
修正后的保存代码大概是这样:
@Autowired private BookRepository bookrepo; public boolean addBook(String bookName, String authorName, Date date) { try { Book b1 = new Book(); b1.setBookName(bookName); b1.setPublishedDate(date); Author author = new Author(); author.setAuthorName(authorName); b1.setAuthor(author); bookrepo.save(b1); return true; } catch (Exception ex) { ex.printStackTrace(); return false; } }
三、为啥有人说用persist()代替save()?
这俩方法的核心区别在语义和行为上:
- 语义更明确:
persist()专门用来存全新的实体,如果你给它传一个已经存在的实体,它直接就抛异常了;而save()是个“万金油”,不管是新实体还是旧实体,它都会先判断状态,再决定是插还是更。 - 效率更高:对于自增主键的场景,
persist()会直接触发INSERT,生成主键;而save()因为要先判断状态,可能会先跑个SELECT,就像你遇到的情况,多了一步操作。 - 返回值不同:
persist()不返回东西,就是把对象纳入持久化上下文;save()会返回一个新的实体对象(如果是更新操作的话)。
所以当你确定要存的是全新的实体时,用persist()更贴合业务逻辑,也能避免因为状态判断引发的奇怪问题。
内容的提问来源于stack exchange,提问作者GrootC
相关产品推荐
相关产品推荐

