简体   繁体   English

JPA / Hibernate无法理解持久顺序

[英]JPA/Hibernate cannot understand persist order

I'm trying to understand how the jpa/hibernate "magic" really works in practice, to avoid future (and common) pitfalls. 我试图了解jpa / hibernate的“魔术”在实际中是如何工作的,以避免将来(以及常见的)陷阱。

So I created some simple JUnit tests, where the set of instructions is exactly the same, but the call order of em.persist() is different. 因此,我创建了一些简单的JUnit测试,其中的指令集完全相同,但是em.persist()的调用顺序不同。

Note that I'm using Hibernate 5.2.10 and bean validator 5.2.4 with hibernate.jdbc.batch_size and hibernate.order_inserts (more details on persistence.xml). 请注意,我将Hibernate 5.2.10和bean验证程序5.2.4与hibernate.jdbc.batch_sizehibernate.order_inserts一起使用 (有关persistence.xml的更多详细信息)。

You can also access the full code on GitHub 您还可以在GitHub上访问完整代码

the two test entities: 两个测试实体:

@Entity
public class Node implements Serializable
{
    @Id
    private long id = System.nanoTime();

    @NotNull
    @Column(nullable = false)
    private String name;

    @OneToMany(mappedBy = "startNode", cascade = ALL, orphanRemoval = true)
    private Set<Edge> exitEdges = new HashSet<>();

    @OneToMany(mappedBy = "endNode", cascade = ALL, orphanRemoval = true)
    private Set<Edge> enterEdges = new HashSet<>();

    public Node() {}

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

    ...
}

and

@Entity
public class Edge implements Serializable
{
    @Id
    private long id = System.nanoTime();

    @NotNull
    @ManyToOne
    private Node startNode;

    @NotNull
    @ManyToOne
    private Node endNode;

    ...
}

tests: 测试:

@Test
public void test1()
{
    accept(em ->
    {
        Node n1 = new Node("n11");
        em.persist(n1);

        Node n2 = new Node("n12");
        em.persist(n2);

        Edge e1 = new Edge();
        e1.setStartNode(n1);
        n1.getExitEdges().add(e1);
        e1.setEndNode(n2);
        n2.getExitEdges().add(e1);
        em.persist(e1);
    });
}

@Test
public void test2()
{
    accept(em ->
    {
        Node n1 = new Node("n21");
        em.persist(n1);

        Node n2 = new Node("n22");
        em.persist(n2);

        Edge e1 = new Edge();
        em.persist(e1);  // <-------- early persist call (no exception)
        e1.setStartNode(n1);
        n1.getExitEdges().add(e1);
        e1.setEndNode(n2);
        n2.getExitEdges().add(e1);
    });
    // exception here: java.sql.SQLIntegrityConstraintViolationException: Column 'ENDNODE_ID'  cannot accept a NULL value.
}

@Test
public void test3()
{
    accept(em ->
    {
        Node n1 = new Node("n31");
        Node n2 = new Node("n32");

        Edge e1 = new Edge();
        e1.setStartNode(n1);
        n1.getExitEdges().add(e1);
        e1.setEndNode(n2);
        n2.getExitEdges().add(e1);

        em.persist(n1); // <-------- late persist calls: org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved beforeQuery current operation : hibernate.model.Edge.endNode -> hibernate.model.Node
        em.persist(n2);
        em.persist(e1);
    });
}

test1 , which follows the canonical order of instructions, obviously passes. 遵循指令的规范顺序的test1显然通过了。

test2 , which calls persist immediately after the constructor call, fails on commit with a database null constraint violation for EDGE.ENDNODE_ID . test2 (在构造函数调用之后立即persist调用)在提交时失败,并且违反了EDGE.ENDNODE_ID数据库空约束。
I thought this should not happen and I believed that either: 我认为这不应该发生,并且我相信:

  • the exception should be thrown on persist, instead of on commit 应该在持久化而不是提交时抛出异常
  • there should be no exception, since, on commit, e1 should be linked with both n1 and n2 . 应该没有例外,因为在提交时, e1应该同时与n1n2链接。

test3 , which calls persist late, fails directly on em.persist(n1); test3调用persist较晚,直接在em.persist(n1);上失败em.persist(n1); line (and not on commit). 行(而不是提交)。
I thought this should not happen too. 我认为这也不应该发生。
An exception is thrown (by cascade) for e1.endNode referencing a transient entity, while in test2 no exception is called on persist even if e1.endNode is NULL. 引用临时实体的e1.endNode会抛出一个异常(级联),而在test2中,即使e1.endNode为NULL也不会在persist上调用任何异常。


Can someone explain WHY test2 exception is thrown on commit and test3 is thrown on persist (while using order_inserts )? 有人可以解释为什么在提交时抛出test2异常,而在持久化时抛出test3(使用order_inserts时 )?

Shouldn't Hibernate cache (and order) insert statements until commit? 在提交之前,Hibernate是否不应该缓存(和顺序)插入语句?


UPDATE UPDATE

I don't need a fix , I need an explanation. 我不需要修复 ,我需要一个解释。 I'll try to make the questions more clear: 我将尝试使问题更清楚:

  1. T2: why hibernate ignores a @NotNull constraint on persist? T2: 为什么休眠状态在持久化时忽略@NotNull约束?
  2. T2: why , although issued a e1.setEndNode(n2) , a null reaches the db? T2: 为什么 ,尽管发出了e1.setEndNode(n2) ,但空值到达了数据库? shouldn't e1 be managed after calling persist and track end-node n2 ? 调用persist并跟踪终端节点n2之后不应该管理e1吗?
  3. T3: why hibernate throws an TPVE early (on persist and not on flush/commit)? T3: 为什么休眠模式会提前(在持久状态而不是在刷新/提交状态)抛出TPVE? Shouldn't hibernate wait until flush time to throw the exception? 难道不应该在刷新时间之前休眠休眠以引发异常吗? Isn't this in contrast with the behavior in T2? 这不是与T2中的行为形成对比吗? BTW, javadoc of persist does not specify TPVE. 顺便说一句,persist的javadoc没有指定TPVE。

I'll try to answer myself: 我会尝试回答自己:

  1. hibernate tries to postpone the validation as late as possible (perfectly fine with me). hibernate尝试尽可能推迟验证(对我来说很好)。
  2. I cannot find any reasonable explanation... It simply makes no sense to me. 我找不到任何合理的解释。。。对我而言,这完全没有意义。
  3. after persist, the managed n1 would have a relation with the transient e1 and such situation has to be avoided. 在持续存在之后,被管理的n1将与瞬态e1有关系,因此必须避免这种情况。
    Nevertheless I can: 不过,我可以:

     Node n1 = new Node("n31"); em.persist(n1); Edge e1 = new Edge(); e1.setEndNode(n1); // same situation on this line 

to obtain the exact situation (managed n1 is related to transient e1 ), so there must be another reason. 为了获得确切的情况(受管理的n1与瞬态e1 ),因此必须有另一个原因。

To cut a long story short, I need to understand the reasons of such, apparently controversial, behaviors and determine if they are deliberate or not (maybe bugs?). 简而言之,我需要了解这种有争议的行为的原因 ,并确定它们是否是故意的(可能是错误?)。


Thank you @AlanHay, now it's more clear. 谢谢@AlanHay,现在更清楚了。
I suppose you are right, it seems that hibernate generates insert statements on persist. 我想您是对的,似乎hibernate在persist上生成insert语句。 And now the order makes sense. 现在,顺序变得有意义了。

Nevertheless I still think it's controversial and a dumb implementation. 尽管如此,我仍然认为这是有争议的,而且是愚蠢的实现。

Why on earth would you generate insert statements on persist? 到底为什么会在persist上生成insert语句?
A smart impl should remember managed entities and generate insert statements just before flush/commit, generating up-to-date statements. 一个聪明的impl应该记住管理实体,并在刷新/提交之前生成插入语句,从而生成最新的语句。

And why on earth wouldn't you run bean validator when generating statements? 为什么在生成语句时不运行bean验证器呢?
It's available, and yet it's not used. 它可用,但尚未使用。

A word about order_inserts : it is used to group inserts by table, ie: 关于order_inserts的字眼 :它用于按表对插入进行分组,即:

insert into Node (id, name) values (1, 'x')
insert into Edge (id, startnode_id, endnode_id) values (2, 1, 3)
insert into Node (id, name) values (3, 'y')

becomes

insert into Node (id, name) values (1, 'x'), (3, 'y')
insert into Edge (id, startnode_id, endnode_id) values (2, 1, 3)

It can be used not only as an optimization, but also to control the statements order (the first block fails, but the second succeeds). 它不仅可以用作优化,还可以用于控制语句顺序(第一个块失败,但是第二个块成功)。
Anyway, in this case, it's irrelevant. 无论如何,在这种情况下,这是无关紧要的。

T2: em.persist(entity); T2: em.persist(entity);

http://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#persist(java.lang.Object) http://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#persist(java.lang.Object)

Make an instance managed and persistent. 使实例托管和持久化。

Says nothing about when the data will be flushed to the database. 关于何时将数据刷新到数据库什么也没说。 In the absence of an explicit flush statement then this will occur when the persistence provider decides: which (in the absence of any query being issued in the same transaction , the results of which may be affected by pending changes) will most likely be when the transaction commits. 在没有显式的flush语句的情况下,当持久性提供程序做出以下决定时,将发生以下情况:哪个(在同一事务中未发出任何查询,其结果可能会受到挂起的更改的影响)将在以下情况下发生:事务提交。

http://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#flush() http://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#flush()

Synchronize the persistence context to the underlying database. 将持久性上下文同步到基础数据库。

So you can make T2 fail prior to commit by calling em.persist() and then either calling em.flush() or by issung a query aginst Edges: in the latter case pending changes would be flushed automatically to ensure that the query returned consistent results. 因此,可以通过调用em.persist()然后调用em.flush()或通过issung查询Edge来使T2在提交之前失败:在后一种情况下,待处理的更改将自动刷新以确保查询返回一致结果。

@Test
public void test2()
{
    accept(em ->
    {
        Node n1 = new Node("n21");
        em.persist(n1);

        Node n2 = new Node("n22");
        em.persist(n2);

        Edge e1 = new Edge();
        em.persist(e1);  
        //explict flush : should fail immediately
        //em.flush(); 

        //implicit flush :  should fail immediately
        //Query query = em.createQUery("select e from Edge e");
        //query.getResultList();

        e1.setStartNode(n1);
        n1.getExitEdges().add(e1);
        e1.setEndNode(n2);
        n2.getExitEdges().add(e1);
    });
}

T3: em.persist(n1); T3: em.persist(n1);

Here we can see that this is a Hibernate exception rather than an SQL exception. 在这里,我们可以看到这是一个Hibernate异常,而不是SQL异常。 At the time of calling persist Hibernate is aware that n1 references the transient instance e1. 在调用持久化时,Hibernate意识到n1引用了临时实例e1。 You either need to make e1 persistent or add @Cascade options to the relationship. 您需要使e1持久化或向关系添加@Cascade选项。

See further The JPA specification: 进一步参见JPA规范:

http://download.oracle.com/otndocs/jcp/persistence-2_1-fr-eval-spec/index.html http://download.oracle.com/otndocs/jcp/persistence-2_1-fr-eval-spec/index.html

3.2.4 Synchronization to the Database 3.2.4与数据库同步

Update 更新

You seem to think that the results you are seeing wuth this usage of the API is "apparently controversial" behaviour and that order_inserts should somehow fix your broken code. 您似乎认为使用此API会看到的结果是“明显有争议的”行为,并且order_inserts应该以某种方式修复损坏的代码。

Order inserts is as, far as I can see, a means to optimize the writing of SQL statements generated via a correct interaction with the API for a valid in-memory model : not to fix incorrect usage of the API. 就我所知,订单插入是一种针对有效内存模型优化通过与API的正确交互生成的SQL语句的编写的方法:不修正API的不正确用法。

If we suppose that Hibernate generates the buffered SQL statements on the call to persist() (where else would it do it after all) then the behaviour makes perfect sense. 如果我们假设Hibernate在对persist()的调用上生成了缓冲的SQL语句persist()毕竟它将在其他地方执行此操作),那么该行为就很合理了。 At that point it cannot set a value for the null relationship. 此时,它无法为null关系设置值。 It seems however that after then adding the relationship you expect that (possibly due to the presence of order_inserts or perhaps regardless of this) it will be smart enough to go back and modify the already generated SQL insert statement. 但是,在添加关系之后,您似乎希望(可能由于order_inserts的存在,或者可能与此无关)会足够聪明,可以返回并修改已经生成的SQL插入语句。

  • T2 > em.persist(e1); T2> em.persist(e1); > generate an insert statement with endnode_id as null. >生成一个endnode_id为null的插入语句。

  • T3 > em.persist(n1); T3> em.persist(n1); > n1 has a relationship to a transient endNode n2. > n1与瞬态端点n2有关系。 What do I do with it? 我该怎么办? There is no cacade so I cannot save it so throw an exception. 没有cacade,所以我无法保存它,因此抛出异常。

I've focused the problem to a minimal example and it's a bug , indeed. 我已将问题集中在一个最小的示例上,这确实是一个错误

Consider a simple entity Node with two properties: 考虑具有两个属性的简单实体节点

  • name ( required with a @NotNull and the db column is not allowing nulls) 名称( 需要具有@NotNull和DB柱不允许空值)
  • label ( optional and the db column is allowing nulls) 标签( 可选 ,并且db列允许为空)

Then consider this test: 然后考虑这个测试:

@Test
public void test1()
{
    accept(em ->
    {
        Node n = new Node();
        em.persist(n);

        n.setName("node-1");
        n.setLabel("label-1");
    });
}

test1 will fail with: test1将失败,并显示以下内容:

Caused by: java.sql.SQLIntegrityConstraintViolationException: Column 'NAME'  cannot accept a NULL value.

The incoherence is in the fact that no consistent behavior is met. 这种不连贯之处在于无法满足一致的行为。 A consistent behavior is either one: 一致的行为是以下之一:

  • a javax.validation.ConstraintViolationException (for @NotNull ) should be thrown (on persist or flush/commit) 应该抛出一个javax.validation.ConstraintViolationException (对于@NotNull )(在persist或flush / commit上)
  • or test1 should pass 或test1应该通过

Supposing that the expected behavior is a validation exception being thrown, the validator is executed on the entity on flush/commit time, but at that time the entity has the "name" set. 假设预期行为是抛出的验证异常,则在刷新/提交时在实体上执行验证器,但此时实体已设置了“名称”。
Then, this leads to an out-of-sync between the entity being validated and the generated statement to be executed, making validation return a false positive. 然后,这导致要验证的实体与要执行的生成语句之间不同步,从而使验证返回误报。

To show it, consider a second simple test: 为了说明这一点,请考虑另一个简单的测试:

@Test
public void test2()
{
    accept(em ->
    {
        Node n = new Node();
        em.persist(n);
    });
}

Correctly, this is failing with: 正确地,这失败了:

Caused by: javax.validation.ConstraintViolationException: Validation failed for classes [hibernate.model.Node] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
    ConstraintViolationImpl{interpolatedMessage='may not be null', propertyPath=name, rootBeanClass=class hibernate.model.Node, messageTemplate='{javax.validation.constraints.NotNull.message}'}
]

On the other hand, supposing that the expected behavior is that test1 should pass, the incoherence is due to statement generation time. 另一方面,假设预期的行为是test1应该通过,则不一致的原因是语句生成时间。

To show it, consider a second simple test: 为了说明这一点,请考虑另一个简单的测试:

@Test
public void test3()
{
    accept(em ->
    {
        Node n = new Node();
        n.setName("node-3");

        em.persist(n);

        n.setLabel("label-3");
    });

    Node n = apply(em -> em.createQuery("select x from Node x", Node.class).getSingleResult());

    Assert.assertEquals("label-3", n.getLabel());
}

Even if the test passes, two statements are generated (and executed). 即使测试通过,也会生成(并执行) 两个语句。

Hibernate: insert into Node (label, name, id) values (?, ?, ?)
Hibernate: update Node set label=?, name=? where id=?

I suppose the first statement is generated on persist and the second on flush/commit; 我想第一条语句是在persist上生成的,第二条是在flush / commit上生成的; but, in this case, I'm expecting a single insert statement generated immediately after the entity has been validated (then on flush/commit time). 但是,在这种情况下,我希望在验证实体之后立即生成一个插入语句(然后在刷新/提交时间)。

In conclusion, I see two possible solutions: 总之,我看到两种可能的解决方案:

  • run the validator inside persist() 在persist()中运行验证器
  • postpone statements generation to flush/commit time 将语句生成推迟到刷新/提交时间

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM