Question

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>
Was it helpful?

Solution 3

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...

OTHER TIPS

So is this object actually in detached state? it is in detached state when it has been managed and you close the persistence context, or you explicitly call "em.detach". Anything else is likely in transient state. The log shows you what state objects are in. Clearly you could enable the persistence property

datanucleus.allowAttachOfTransient

as per the docs, but then you haven't enabled that by what you post.

You also seem to be confused about which API you're using, quoting a JDO PMF specification, and then saying you're using JPA.

The first error that I see is that you used the @Basic annotation for a relation. See the documentation for the allowed types of the Basic annotation.

Instead use the @JoinColumn annotation (if you want to change any default settings).

The second problem that I see is that you persist the entity anyway, no matter the ID. See the correct code with the problem (in repository):

    if (a.getId() != null) {
         entityManager.merge(a);
    } else {
         entityManager.persist(a);//this line should be in an ELSE branch (in your code it is not)
    }
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top