Issue
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?
Solution
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 aPERSISTED
entity which must have an ID). Thus no matter which FlushMode you choose ORM will issueINSERT
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 theINSERT
.
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).
Answered By - Stanislav Bashkyrtsev