I have an Android client with AppEngine for backend. There is also a JPA and datanucleus persistence provider. Client and server communicate through REST + JSON. When user logs in to the application on the phone, I am retrieving user's information from the server and store it on the phone. After that I am creating an object 'A' and try to save it on the server, providing existing user as a field in this object. As a result both my object 'A' and user are stored, but for user, duplicated entry is created.
This behavior is now clear, as per Datanucleus documentation: An object that has a PK field set is transient unless it was detached from persistence.. Setting datanucleus.allowAttachOfTransient to true doesn't help (documentation for this property for JPA says: "When you call EM.merge with a transient object (with PK fields set), if you enable this feature then it will first check for existence of an object in the datastore with the same identity and, if present, will merge into that object (rather than just trying to persist a new object"), but at first I am persisting object A, and not merging it.
Mapping:
@MappedSuperclass
class BaseEntity {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@JsonIgnore
private Key id;
// more field, getters and setters
}
@Entity
class User extends BaseEntity {
private String name;
// more fields, getters and setters
}
@Entity
class A extends BaseEntity {
@Basic
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private User user;
// more fields, getters and setters
}
persistence.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
<persistence-unit name="transactions-optional">
<provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>
<class>com.example.A</class>
<class>com.example.User</class>
<properties>
<property name="datanucleus.NontransactionalRead" value="true" />
<property name="datanucleus.NontransactionalWrite" value="true" />
<property name="datanucleus.ConnectionURL" value="appengine" />
<property name="datanucleus.singletonEMFForName" value="true" />
<property name="datanucleus.allowAttachOfTransient" value="true" />
<property name="datanucleus.maxFetchDepth" value="2"/>
<property name="datanucleus.DetachAllOnCommit" value="true"/>
</properties>
</persistence-unit>
</persistence>
Repository:
@Repository
public class ARepository {
@PersistenceContext
private EntityManager entityManager;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public A saveA(A a) {
try {
if (a.getId() != null) {
entityManager.merge(a);
} else {
entityManager.persist(a);
}
} finally {
entityManager.close();
}
return a;
}
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}
}
Upd 2.: Logs IF I don't load and Set user before storing A, logs show following line:
Object "com.exampe.User@7886c691" (id="com.example.User:A(6192449487634432)/User(6473924464345088)") has a lifecycle change : "P_NEW"->"DETACHED_CLEAN"
which is of course expected.
When I do load and set user before saving A, following output is logged:
[INFO] jaan 28, 2014 6:20:22 PM org.datanucleus.state.LifeCycleState changeState
[INFO] FINE: Object "com.example.User@31991a3c" (id="com.example.User:A(5629499534213120)/User(5066549580791808)") has a lifecycle change : "HOLLOW"->"DETACHED_CLEAN"
[INFO] jaan 28, 2014 6:20:22 PM org.datanucleus.store.connection.ConnectionManagerImpl closeAllConnections
[INFO] FINE: Connection found in the pool : com.google.appengine.datanucleus.DatastoreConnectionFactoryImpl$DatastoreManagedConnection@23fc3932 for key=org.datanucleus.ObjectManagerImpl@40f1413 in factory=ConnectionFactory:nontx[com.google.appengine.datanucleus.DatastoreConnectionFactoryImpl@79eeed79] but owner object closing so closing connection
[INFO] jaan 28, 2014 6:20:22 PM org.datanucleus.store.connection.ConnectionManagerImpl$1 managedConnectionPostClose
[INFO] FINE: Connection removed from the pool : com.google.appengine.datanucleus.DatastoreConnectionFactoryImpl$DatastoreManagedConnection@23fc3932 for key=org.datanucleus.ObjectManagerImpl@40f1413 in factory=ConnectionFactory:nontx[com.google.appengine.datanucleus.DatastoreConnectionFactoryImpl@79eeed79]
[INFO] jaan 28, 2014 6:20:39 PM org.datanucleus.state.LifeCycleState changeState
[INFO] FINE: Object "com.example.User@716f5d2f" (id="com.example.User:A(5629499534213120)/User(5066549580791808)") has a lifecycle change : "P_CLEAN"->"P_NONTRANS"
[INFO] jaan 28, 2014 6:20:39 PM com.google.appengine.datanucleus.DatastoreConnectionFactoryImpl$DatastoreManagedConnection <init>
[INFO] FINE: Created ManagedConnection using DatastoreService = com.google.appengine.api.datastore.DatastoreServiceImpl@1cc63c88
[INFO] jaan 28, 2014 6:20:39 PM org.datanucleus.store.connection.ConnectionManagerImpl allocateConnection
[INFO] FINE: Connection added to the pool : com.google.appengine.datanucleus.DatastoreConnectionFactoryImpl$DatastoreManagedConnection@438518d4 for key=org.datanucleus.ObjectManagerImpl@5116a644 in factory=ConnectionFactory:tx[com.google.appengine.datanucleus.DatastoreConnectionFactoryImpl@2aa8911c]
[INFO] jaan 28, 2014 6:20:39 PM com.google.appengine.datanucleus.MetaDataValidator warn
[INFO] WARNING: Meta-data warning for com.example.A.user: Error in meta-data for com.example.A.user : The datastore does not support joins and therefore cannot honor requests to place related objects in the default fetch group. The field will be fetched lazily on first access. You can modify this warning by setting the datanucleus.appengine.ignorableMetaDataBehavior property in your config. A value of NONE will silence the warning. A value of ERROR will turn the warning into an exception
This is weird that after "P_CLEAN"->"P_NONTRANS"
lyfecycle change nothing happens, may be it is related to caching, transactions? So I've started setting up spring transactions but run into following exception:
[INFO] Caused by: javax.persistence.PersistenceException: Illegal argument
[INFO] at org.datanucleus.api.jpa.NucleusJPAHelper.getJPAExceptionForNucleusException(NucleusJPAHelper.java:298)
[INFO] at org.datanucleus.api.jpa.JPAEntityTransaction.commit(JPAEntityTransaction.java:122)
[INFO] at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:512)
[INFO] ... 62 more
[INFO] Caused by: java.lang.IllegalArgumentException: transaction has expired or is invalid
[INFO] at com.google.appengine.api.datastore.DatastoreApiHelper.translateError(DatastoreApiHelper.java:39)
[INFO] at com.google.appengine.api.datastore.DatastoreApiHelper$1.convertException(DatastoreApiHelper.java:76)
[INFO] at com.google.appengine.api.utils.FutureWrapper.get(FutureWrapper.java:94)
[INFO] at com.google.appengine.api.datastore.Batcher$ReorderingMultiFuture.get(Batcher.java:129)
[INFO] at com.google.appengine.api.datastore.FutureHelper$TxnAwareFuture.get(FutureHelper.java:171)
[INFO] at com.google.appengine.api.utils.FutureWrapper.get(FutureWrapper.java:86)
[INFO] at com.google.appengine.api.datastore.FutureHelper.getInternal(FutureHelper.java:71)
[INFO] at com.google.appengine.api.datastore.FutureHelper.quietGet(FutureHelper.java:32)
[INFO] at com.google.appengine.api.datastore.DatastoreServiceImpl.put(DatastoreServiceImpl.java:86)
[INFO] at com.google.appengine.datanucleus.WrappedDatastoreService.put(WrappedDatastoreService.java:112)
[INFO] at com.google.appengine.datanucleus.EntityUtils.putEntitiesIntoDatastore(EntityUtils.java:764)
[INFO] at com.google.appengine.datanucleus.DatastorePersistenceHandler.insertObjectsInternal(DatastorePersistenceHandler.java:314)
[INFO] at com.google.appengine.datanucleus.DatastorePersistenceHandler.insertObject(DatastorePersistenceHandler.java:218)
[INFO] at org.datanucleus.state.JDOStateManager.internalMakePersistent(JDOStateManager.java:2377)
[INFO] at org.datanucleus.state.JDOStateManager.flush(JDOStateManager.java:3769)
[INFO] at org.datanucleus.store.mapped.mapping.PersistableMapping.setObjectAsValue(PersistableMapping.java:446)
[INFO] at org.datanucleus.store.mapped.mapping.PersistableMapping.setObject(PersistableMapping.java:321)
[INFO] at com.google.appengine.datanucleus.StoreFieldManager.storeRelations(StoreFieldManager.java:777)
[INFO] at com.google.appengine.datanucleus.DatastorePersistenceHandler.insertObjectsInternal(DatastorePersistenceHandler.java:367)
[INFO] at com.google.appengine.datanucleus.DatastorePersistenceHandler.insertObject(DatastorePersistenceHandler.java:218)
[INFO] at org.datanucleus.state.JDOStateManager.internalMakePersistent(JDOStateManager.java:2377)
[INFO] at org.datanucleus.state.JDOStateManager.flush(JDOStateManager.java:3769)
[INFO] at org.datanucleus.ObjectManagerImpl.flushInternalWithOrdering(ObjectManagerImpl.java:3884)
[INFO] at org.datanucleus.ObjectManagerImpl.flushInternal(ObjectManagerImpl.java:3807)
[INFO] at org.datanucleus.ObjectManagerImpl.flush(ObjectManagerImpl.java:3747)
[INFO] at org.datanucleus.ObjectManagerImpl.preCommit(ObjectManagerImpl.java:4137)
[INFO] at org.datanucleus.ObjectManagerImpl.transactionPreCommit(ObjectManagerImpl.java:428)
[INFO] at org.datanucleus.TransactionImpl.internalPreCommit(TransactionImpl.java:400)
[INFO] at org.datanucleus.TransactionImpl.commit(TransactionImpl.java:288)
[INFO] at org.datanucleus.api.jpa.JPAEntityTransaction.commit(JPAEntityTransaction.java:103)
spring config looks like this:
<mvc:annotation-driven />
<tx:annotation-driven transaction-manager="transactionManager"/>
<bean id="jacksonMessageConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
<bean id="persistenceUnitManager" class="org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager">
<property name="persistenceXmlLocation">
<value>classpath*:/META-INF/persistence.xml</value>
</property>
</bean>
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean"
lazy-init="true">
<property name="persistenceUnitName" value="transactions-optional" />
</bean>
<bean name="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
It started working after I have added the following properties:
<property name="datanucleus.appengine.datastoreEnableXGTransactions" value="true"/>
<property name="datanucleus.appengine.relationDefault" value="unowned"/>
I still need to pre-load User object and set it to A, before saving the A. I am pretty sure there are plenty of problems ahead thou...