1

Can someone explain the following. I have the code:

@Entity public class Model { @Id @GeneratedValue(strategy = AUTO) @Column private long id; @Column(length = 200, nullable = false) private String field0; @Column(length = 200, nullable = false) private String field1; @Column(length = 200, nullable = false) private String field2; @Column(length = 200, nullable = false) private String field3; @Column(length = 200, nullable = false) private String field4; @Column(length = 200, nullable = false) private String field5; @Column(length = 200, nullable = false) private String field6; @Column(length = 200, nullable = false) private String field7; @Column(length = 200, nullable = false) private String field8; @Column(length = 200, nullable = false) private String field9; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getField0() { return field0; } public void setField0(String field0) { this.field0 = field0; } public String getField1() { return field1; } public void setField1(String field1) { this.field1 = field1; } public String getField2() { return field2; } public void setField2(String field2) { this.field2 = field2; } public String getField3() { return field3; } public void setField3(String field3) { this.field3 = field3; } public String getField4() { return field4; } public void setField4(String field4) { this.field4 = field4; } public String getField5() { return field5; } public void setField5(String field5) { this.field5 = field5; } public String getField6() { return field6; } public void setField6(String field6) { this.field6 = field6; } public String getField7() { return field7; } public void setField7(String field7) { this.field7 = field7; } public String getField8() { return field8; } public void setField8(String field8) { this.field8 = field8; } public String getField9() { return field9; } public void setField9(String field9) { this.field9 = field9; } @Override public String toString() { return "Model{" + "id=" + id + ", field0='" + field0 + '\'' + ", field1='" + field1 + '\'' + ", field2='" + field2 + '\'' + ", field3='" + field3 + '\'' + ", field4='" + field4 + '\'' + ", field5='" + field5 + '\'' + ", field6='" + field6 + '\'' + ", field7='" + field7 + '\'' + ", field8='" + field8 + '\'' + ", field9='" + field9 + '\'' + '}'; } } @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("/testContext.xml") public class MainTest { @Autowired private SessionFactory sessionFactory; private Session session; private Transaction tx; @Before public void before() { session = sessionFactory.openSession(); tx = session.beginTransaction(); session.setFlushMode(FlushMode.COMMIT); } @After public void after() { tx.commit(); session.close(); } @Test public void shouldFindModelByField() { Model model = createRandomModel(); session.save(model); model.setField0("TEST1"); session.save(model); assertTrue(null != session.createSQLQuery( "select id from model where field0 = '" + model.getField0() + "'").uniqueResult()); } private Model createRandomModel() { Model ret = new Model(); ret.setField0(RandomStringUtils.randomAlphanumeric(10)); ret.setField1(RandomStringUtils.randomAlphanumeric(10)); ret.setField2(RandomStringUtils.randomAlphanumeric(10)); ret.setField3(RandomStringUtils.randomAlphanumeric(10)); ret.setField4(RandomStringUtils.randomAlphanumeric(10)); ret.setField5(RandomStringUtils.randomAlphanumeric(10)); ret.setField6(RandomStringUtils.randomAlphanumeric(10)); ret.setField7(RandomStringUtils.randomAlphanumeric(10)); ret.setField8(RandomStringUtils.randomAlphanumeric(10)); ret.setField9(RandomStringUtils.randomAlphanumeric(10)); return ret; } } 

If I run the test as is, the test fails and I get an error java.lang.AssertionError.

I have three varints to change @Test method to run the test successfully:

1)

@Test public void shouldFindModelByField() { Model model = createRandomModel(); session.save(model); session.evict(model); model.setField0("TEST1"); session.save(model); assertTrue(null != session.createSQLQuery( "select id from model where field0 = '" + model.getField0() + "'").uniqueResult()); } 

2)

@Test public void shouldFindModelByField() { Model model = createRandomModel(); session.save(model); model.setField0("TEST1"); session.save(model); tx.commit(); tx = session.beginTransaction(); assertTrue(null != session.createSQLQuery( "select id from model where field0 = '" + model.getField0() + "'").uniqueResult()); } 

3)

@Test public void shouldFindModelByField() { Model model = createRandomModel(); session.save(model); model.setField0("TEST1"); session.save(model); session.flush(); assertTrue(null != session.createSQLQuery( "select id from model where field0 = '" + model.getField0() + "'").uniqueResult()); } 

Questions: 1) Why test fails if I run it as is? 2) What variant is correct? 3) If none of them, how to correct the code?

2
  • I can explain everything but Fix#1.. Are you sure that you only added evict() and another save() and that's it? Could you please double check? Also, what's your DB? Commented Jul 7, 2017 at 17:45
  • I just added evict after first "save". My db is H2. Please see the screenshot of xml file prntscr.com/fswg10. It would be great if you could answer my questions above. Commented Jul 7, 2017 at 18:12

1 Answer 1

5

When Hibernate executes SQL

  • FlushMode dictates when Hibernate generates the actual SQL statements. The default (auto) is pretty sensible and it tries to delay the statements as much as possible. But it will flush before each SELECT statement (otherwise you won't find the records you just persisted).
  • Hibernate must generate an ID for the entity when you save it (the result of the save() is a PERSISTED entity which must have an ID). Thus no matter which FlushMode you choose ORM will issue INSERT statement if that's what's needed for ID to be generated. If you were to use Sequence generator - INSERT could be postponed, but you use Identity - this one cannot be postponed as the ID is generated by the DB during the INSERT.

Why original code doesn't work

You set FlushMode to COMMIT which means Hibernate executes SQL right before the transaction commit. Thus when you update your entity the UPDATE statement is not invoked. It would've been invoked only at the end when you commit the transaction (which you never do).

Why Fix #1 "works"

Your original INSERT statement for the new entity still executes even with FlushMode COMMIT - the ID has to be generated.

After you evict() entity Hibernate doesn't know about it anymore, but it has an ID, so next time you save() Hibernate knows that it's a DETACHED entity. Every time a detached entity is saved() an UPDATE is invoked.

Why Fix #2 works

Well, you actually commit the transaction so Hibernate flushes all the SQLs including UPDATE statements. Your FlushMode=COMMIT works as expected.

Why Fix #3 works

In this case you manually flush() the changes - it will execute SQL statements no matter which Flush Mode you chose.

How to write Hibernate tests

First of all SpringJUnit4ClassRunner supports @Transactional annotations on tests. So instead of handling transactions manually in @Before & @After you can use the annotation.

Second, to be sure that the test actually works you need to flush and clear 1st level cache manually. Otherwise you risk to work with cached entities instead of the real DB. So your test can look like this:

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("/testContext.xml") @Transactional public class MainTest { ... @Test public void canUpdateAllTheFields() { Model original = createRandomModel(); session.save(original); session.flush(); session.clear(); Model updates = createRandomModel(); model.setId(original.getId()); session.update(updates); session.flush(); session.clear(); assertReflectionEquals(updates, session.get(Model.class, original.getId())); } } 

Notes:

  • You can combine flush() and clear() in a separate method so that it doesn't take that much space.
  • assertReflectionEquals() comes from Unitils lib.

You can find an example of such tests here (TestNG) and here (Spock).

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for such detailed answer. Could you please explain how to use Transactional annotation instead of Before and After annotations
Updated the answer
I'm about to implement your answer in a project, but I wonder why you commit the transaction. If it's a test I don't care about the values after the assertions are made, and I want a clean context between tests, so I think rollbacking is more appropiated. Am I missing something?
@Transactional tests are rolled back by default. It commits if you put @Commit as well.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.