I'm writing tests for my Dao Spring application. I found out that when I delete not saved items no exception is invoked as I'd expect, I've got no idea why.
Model:
#Entity
public class Ingredient {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String condition;
private int quantity;
public Ingredient() {
}
}
The Dao implementation:
#Override
public void delete(Object o) throws DaoException {
try {
Session session = mSessionFactory.openSession();
session.beginTransaction();
session.delete(o);
session.getTransaction().commit();
session.close();
} catch (Exception ex) {
throw new DaoException(ex, String.format("Problem deleting %s object (delete method).", o));
}
}
And my test, expecting DaoException:
#Test
public void testDeleteNotSavedThrowsDaoException() throws Exception {
Ingredient ingredient = new Ingredient("Not saved ingredient","", 1);
ingredientDao.delete(ingredient);
}
Hibernate's Javadoc for Session#delete(Object) states:
Remove a persistent instance from the datastore. The argument may be an instance associated with the receiving Session or a transient instance with an identifier associated with existing persistent state.
So it's not an error to pass in a transient entity (as you do). Also, the Session#delete method does not declare any exceptions, so it's not defined what happens when you pass in an entity with an ID that does not exist in the DB. As you can see - nothing happens - you requested the entity not to exist in the DB, it's not there to start with, so no reason to throw an exception (according to Hibernate, at least).
Compare this to the basic SQL DELETE FROM X WHERE ID = Y - this does not check if a record with ID=Y exists, it will succeed either way (updating 0 or 1 rows).
UPDATE after realizing the passed in transient entity has null ID.
I've dug into the sources of Hibernate 5.2.2 Session and it seems that if the passed in entity has no ID, no DELETE query is even performed on that entity's table.
See DefaultDeleteEventListener#onDelete(DeleteEvent, Set):
if (ForeignKeys.isTransient( persister.getEntityName(), entity, null, source ) ) {
// yes, your entity is transient according to ForeignKeys.isTransient
deleteTransientEntity( source, entity, event.isCascadeDeleteEnabled(), persister, transientEntities );
return;
}
Now
protected void deleteTransientEntity(
EventSource session,
Object entity,
boolean cascadeDeleteEnabled,
EntityPersister persister,
Set transientEntities) {
LOG.handlingTransientEntity(); // Only log it
if ( transientEntities.contains( entity ) ) {
LOG.trace( "Already handled transient entity; skipping" );
return;
}
transientEntities.add( entity );
// Cascade deletion to related entities
cascadeBeforeDelete( session, persister, entity, null, transientEntities );
cascadeAfterDelete( session, persister, entity, transientEntities );
}
this will just print "HHH000114: Handling transient entity in delete processing" in the logs and do nothing with the entity (however, it will cascade the deletion to the related entities if there are any - not your case).
So again - it's OK to pass in a transient entity without an ID - it will simply not run a DELETE on the DB.
And that was an answer, Adam, there was no exception, because id of my new, not saved item was null. When I set id to value which not persist in DB exception was thrown.
Related
For a simple batch update of a MariaDB table, properly mapped as a Hibernate entity class, a simple update via Hibernate produces the error
org.hibernate.StaleStateException: Batch update returned unexpected row count from update
Each table record is modeled by an Entity class, which is a simple POJO that needs to be updated (if it already exists) or inserted as a new object (if it does not exist in the table), with a primary id field (not auto-incremented) and some other values, all scalar. The error can be reproduced by the following method.
public static void update(Set<Long> ids) {
Session session = createSession();
Transaction t = session.beginTransaction();
try {
for (Long id : ids) {
Entity entity = session.get(Entity.class, id);
if (entity == null) {
entity = new Entity();
}
entity.setId(id);
// Other entity value settings
session.saveOrUpdate(entity);
}
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
session.close();
}
}
What is the correct way of implementing the above operation in Hibernate?
You are using saveOrUpdate() in this way Hibernate decides by his own logic what is a new (Transient) and what is an old (Persisted) object and depends on this performs save() or update() method accordingly.
Hibernate assumes that an instance is an unsaved transient instance if:
The identifier property is null.
The version or timestamp property (if it exists) is null.
A new instance of the same persistent class, created by Hibernate internally, has the same database identifier values as the given instance.
You supply an unsaved-value in the mapping document for the class, and the value of the identifier property matches. The unsaved-value attribute is also available for version and timestamp mapping elements.
Entity data with the same identifier value isn't in the second-level cache.
You supply an implementation or org.hibernate.Interceptor and return Boolean.TRUE from Interceptor.isUnsaved() after checking the instance in your code.
Otherwise: entity will be determined like already saved persisted
In your example, Hibernate did not determine the new (Transient) object and as result, perform update() method for it. It produced UPDATE instead of INSERT statement. UPDATE statement for not existing record returns zero updated records, so it is the reason for your exception.
Solution: explicitly use save() method for new entities:
public void update(Set<Long> ids) {
Session session = getSessionFactory().openSession();
Transaction transaction = session.beginTransaction();
try {
for (Long id : ids) {
HibernateEntity entity = session.get(HibernateEntity.class, id);
if (entity == null) {
entity = new HibernateEntity();
}
// Other entity value settings
entity.setValue(entity.getValue() + "modification");
if (entity.getId() == null) {
entity.setId(id);
session.save(entity);
}
}
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
session.close();
}
}
update() method is not required to call explicitly. Transactional persistent instances (i.e. objects loaded, saved, created or queried by the Session) can be manipulated by the application, and any changes to persistent state will be persisted when the Session is flushed. According to documentation.
I have an object "Chemical" that is updated according to entries in an HTML page. The data is returned to the Java code correctly, yet this one field is not updating or being created in the database. The chemical entity is defined as follows:
#Entity
#NamedQuery(name="Chemical.findAll", query="SELECT c FROM Chemical c")
public class Chemical implements Serializable {
#Id
#Column(name="chemical_id")
#GeneratedValue(strategy=GenerationType.IDENTITY)
private int chemicalId;
...
private string formula; <--- THE FIELD THAT WILL NOT UPDATE/BE CREATED
....
public void setFormula(String formula) {
this.formula = formula;
}
public String getFormula() {
return this.formula;
}
.....
The Java code that actually saves/updates the database:
public void saveOrUpdate(final T data) throws CPDPersistenceException {
final EntityManager em = getEntityManager();
try {
final EntityTransaction transaction = em.getTransaction();
transaction.begin();
em.merge(data);
transaction.commit();
} catch (final PersistenceException e) {
throw new PersistenceException(e);
}
}
After the code is executed, other fields have changed in the database if changes have been made. However, "formula" is not changed and remains a NULL field. Can anybody see why?
There is I think a difference between UPDATE and CREATE.
Try to replace em.merge(data) with em.persist(data)
Does it save the content the first time you create a new row with persist ?
What is the length of your string definition in the database ?
Check the database logs whenever the HQL is run from your code, do the log complain at one point during the persist ?
It might be necessary to first test if the row exists before you can run a merge.
I try to use objectify transaction, but I have some issues when I need to reload an object created in the same transaction.
Take this sample code
#Entity
public class MyObject
{
#Parent
Key<ParentClass> parent;
#Index
String foo;
}
ofy().transact(new VoidWork()
{
#Override
public void vrun()
{
ParentClass parent = load();// load the parent
String fooValue = "bar";
Key<ParentClass> parentKey = Key.create(ParentClass.class, parent.getId())
MyObject myObject = new MyObject(parentKey);
myObject.setFoo(fooValue);
ofy().save().entity(myObject).now();
MyObject reloaded = ofy().load().type(MyObject.class).ancestor(parentKey).filter("foo", fooValue).first().now();
if(reloaded == null)
{
throw new RuntimeException("error");
}
}
});
My object reloaded is always null, maybe I miss something, but logically within a transaction I can query an object which was created in the same transaction?
Thanks
Cloud Datastore differs from relational databases in this particular case. The documentation states that -
Unlike with most databases, queries and gets inside a Cloud Datastore
transaction do not see the results of previous writes inside that
transaction. Specifically, if an entity is modified or deleted within
a transaction, a query or lookup returns the original version of the
entity as of the beginning of the transaction, or nothing if the
entity did not exist then.
https://cloud.google.com/datastore/docs/concepts/transactions#isolation_and_consistency
Is there a general method that can
if(entity is persisted before){
entity = entity.merge();
}else{
entity.persist();
}
So the method contain above logic is safe everywhere?
If you need to know is object already in persistence context you should use contains method of EntityManager.
Only EntityManager can tell you is entity persisted or not, entity does not have such information.
Here you can check javadoc for contains method.
if (!em.contains(entity)) {
em.persist(entity);
} else {
em.merge(entity);
}
To check if entity object has been persisted or not by the current PersistenceContext you can use the EntityManager method contains(Object entity)
Maybe it's too late, but here are my findings!
If you have an entity with a generate value, you can use it to check if the entity is already in DB, assuming you are not modifying this value manually.
#Entity
public class MyEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Integer id;
// getter...
}
public class Main {
public static void main() {
MyEntity myEntity1 = new MyEntity();
MyEntity myEntity2 = em.find(MyEntity.class, 4);
em.detach(myEntity2); // or em.close()
// other stuff and modifications
// begin transaction
persistEntity(myEntity1); // will use persist()
persistEntity(myEntity2); // will use merge()
// commit transaction
}
// This will manage correctly entities in different state
public void persistEntity(MyEtity entity) {
if (myEntity.getId() != null) em.merge(entity);
else em.persist(entity);
}
}
Using em.contains(entity) will fail in this scenario:
public static void main(){
MyEntity myEntity = em.find(MyEntity.class, 5);
em.detach(myEntity); // or em.close()
// We are going to execute persist() because the entity is detached
if (!em.contains(myEntity))
// This call will produce an exception org.hibernate.PersistentObjectException
em.persist(myEntity);
else
em.merge(myEntity);
}
There are a performance reasons to try to achieve what OP is trying to do. You surely can use em.merge() instead of em.persist(), but not without a cost.
A call to em.merge() is trying to retrieve an existing entity from DB with a SELECT query and update it. So if the entity was never persisted, this will waste some CPU cycles. On the other side em.persist() will only produce one INSERT query.
I wonder if anyone has come across this error and can explain what's happening:
<openjpa-2.1.1-SNAPSHOT-r422266:1087028 nonfatal user error>
org.apache.openjpa.persistence.InvalidStateException:
Primary key field com.qbe.config.bean.QBEPropertyHistory.id of com.qbe.config.bean.QBEPropertyHistory#1c710ab has non-default value.
The instance life cycle is in PNewProvisionalState state and hence an
existing non-default value for the identity field is not permitted.
You either need to remove the #GeneratedValue annotation or modify the
code to remove the initializer processing.
I have two objects, Property and PropertyHistory. Property has OneToMany List of PropertyHistory:
#OneToMany(fetch = FetchType.LAZY, cascade=CascadeType.MERGE, orphanRemoval=false)
#JoinColumn(name="PROPERTY_NAME")
#OrderBy("updatedTime DESC")
private List<QBEPropertyHistory> history = new ArrayList<QBEPropertyHistory>();
And Property object is loaded and saved like this:
public T find(Object id) {
T t = null;
synchronized(this) {
EntityManager em = getEm();
t = em.find(type, id);
//em.close(); //If this is uncommented, fetch=LAZY doesn't work. And fetch=EAGER is too slow.
}
return t;
}
public T update(T t) {
synchronized(this) {
EntityManager em = getEm();
em.getTransaction().begin();
t = em.merge(t);
em.getTransaction().commit();
em.close();
return t;
}
}
In the service layer I load a property using find(id) method, instantiate a new PropertyHistory, add it into property prop.getHistory().add(propHist) then call update(prop) and get the above error.
The error disappears if I close EntityManager in find() but that breaks lazy loading and prop.getHistory() always returns null. If I set fetch=EAGER it becomes unacceptably slow as there are 10s of 1000s of records and I need to select thousands of property objects at a time and history is not needed 99.99% of the time.
I can't remove the #GeneratedValue as the error text suggests because it is generated (DB2, autoincrement). Now I wonder how would i "modify the code to remove the initializer processing" ?
Thanks!
The problem is that you are trying to share an Entity across persistence contexts(EntityManager). You could change your methods to take an EntityManager instance and use the same EM for the find and update operations.