Skip to main content

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [List Home]
Re: [eclipselink-users] Spikes, FOSS JPA 1.0 test framework

Hi John,

   I didn't have a MappedSuperclass in my example but when I add one, in as in your example, I get the same problem.  I've filed a bug for this: https://bugs.eclipse.org/bugs/show_bug.cgi?id=233296

     Thanks for pointing this out!

         Shaun

https://bugs.eclipse.org/bugs/show_bug.cgi?id=233296

John Leach wrote:
Dear Shaun,

I was using EclipseLink M6, I just updated locally to M7 but still have the
problem. FYI Toplink Essentials is V2 B41.
I'll give you some code here, but you can get the whole thing from:

svn checkout https://lab.jugtorino.it/svn/sandbox/spikes/trunk spikes

The framework is using Spring 2.5.3 for the unit tests.

The base domain class (which contains the @PreUpdate)  file is
org/syger/example/domain/AbstractModel.java:

<pre>
package org.syger.example.domain;

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.PreUpdate;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Version;

/**
 * Modello astratto: contiene proprieta' commune a tutte le entita' -
 * Abstract model: contains properties common to all entities.
 * 
 * @author john.leach
 */
@MappedSuperclass
public abstract class AbstractModel {

	@Id 
	@GeneratedValue(strategy = GenerationType.AUTO)
	Long id;
	
	@Version
	@Column(nullable = false)
	Long version;
	
	@Column(updatable = false)
	@Temporal(TemporalType.TIMESTAMP)
	java.util.Date createdAt;
	
	@Temporal(TemporalType.TIMESTAMP)
	java.util.Date lastUpdated;

	public Long getId() { return id; }
	public Long getVersion() { return version; }
	public java.util.Date getCreatedAt() { return createdAt; }
	public java.util.Date getLastUpdated() { return lastUpdated; }

	public AbstractModel() {
		lastUpdated = createdAt = new java.util.Date();
	}

	/**
	 * Settaggio della data del'ultimo aggiornamento prima del salvataggio -
	 * Sets the last updated date prior to saving.
	 */
	@PreUpdate 
	protected void preUpdate() {
		lastUpdated = new java.util.Date();
	}
}
</pre>

The concrete User domain model (which has the not-null fields, and
hashCode() method that causes the NPE) file is
org/syger/example/domain/User.java:

<pre>
package org.syger.example.domain;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;

/**
 * User: un utente ha un nome (univoco) ed zero o piu' Client(i) -
 * User: a user has a unique name and zero or more Client(s).
 * 
 * @author john.leach
 */
@Entity 
@Table(name="app_user", // user can be a reserved SQL word
       uniqueConstraints=@UniqueConstraint(columnNames={"name"}))
public class User extends AbstractModel {

	@Basic(optional = false)
	@Column(nullable = false, updatable = false, length = 100)
	String name;

	@Basic(optional = false)
	String info;
	
	@OneToMany(mappedBy = "user", cascade = { CascadeType.ALL }) 
	@OrderBy("name ASC")
	List<Client> clients = new ArrayList<Client>();

	public User() {	
		super();
		info = "";
	}

	public User(String name) {
		this();
		this.name = name;
	}
	
	public String getName() { return name; }
	public void setName(String name) { // needed for Yaml
		if (this.name == null) {
			this.name = name;
		}
	}

	public List<Client> getClients() { return clients; }
	
	public String getInfo() { return info; }
	public void setInfo(String info) { this.info = info; }

	public void addClient(Client newClient) {
		newClient.setUser(this);
		if (clients.contains(newClient)) {
			return;
		}
		clients.add(newClient);
	}
	
	public void removeClient(Client oldClient) {
		if (clients.remove(oldClient)) {
			// oldClient.setUser(null); // removed to keep EclipseLink happy
		}
	}

	public boolean equals(Object other) {
		if (this == other) return true;
        	if (other == null) return false;
        	if ( !(other instanceof User) ) return false;
        	final User that = (User)other;
        	return this.name.equals(that.getName());
	}
	
	public int hashCode() {
		return name.hashCode();
	}
	
	public String toString() {
		return name;
	}
}
</pre>

The test classes use an abstract class, which itself sits on the Spring
framework, file is org/syger/unittest/AbstractJpaExperimentalTests.java:

<pre>
package org.syger.unittest;

import org.springframework.test.jpa.AbstractJpaTests;

import org.syger.example.manager.DataManager;

/**
 * Classe d'aiuto per tutte le unit tests -
 * Helper class for all unit tests.
 * 
 * @author john.leach
 */
public class AbstractJpaExperimentalTests extends AbstractJpaTests {

	private DataManager dataManager;
	
	protected String[] getConfigLocations() {
		return new String[] { "classpath:META-INF/test-spring-config.xml" };
	}

	public DataManager getDataManager() { return this.dataManager; }
	public void setDataManager(DataManager dataManager) { this.dataManager =
dataManager; }
	
	public void log(String message) {
		System.out.println(message);
		System.err.println(message);
	}
}
</pre>

Next is the null test which causes a wrapped org.springframework exception
in EclipseLink, but a java.lang.NPE in TopLink Essentials, plus the
@PreUpdate test, file org/syger/unittest/UserTest.java:

<pre>
package org.syger.unittest;

import org.syger.example.domain.User;

/**
 * Test basiliare per il domain model User -
 * Basic tests for the User domain model.
 * 
 * @author john.leach
 */
public class UserTest extends AbstractJpaExperimentalTests {

	public void testUserNullName() {
		User user = new User();
		try {
			getDataManager().persist(user);
			getDataManager().flush();
			fail("Not null exception expected");
		}
		catch (Exception ex) {
			if (!ex.getClass().getName().startsWith("org.springframework")) {
				ex.printStackTrace(System.out);
				fail("Not the expected exception " + ex.getClass().getName());	
			}				
		}
	}

	// ... other tests removed
	
	public void testUserPreUpdate() {
		User user = new User("updated");
		getDataManager().persist(user);
		getDataManager().flush();

		java.util.Date preUpdated = user.getLastUpdated();
		System.out.println("Original " + preUpdated.getTime());

		long time = System.currentTimeMillis();
		while (System.currentTimeMillis() - time < 1000L) {
			// dum de dum... just waiting a second
		}

		user.setInfo("info");
		getDataManager().flush();

		java.util.Date postUpdated = user.getLastUpdated();
		System.out.println("Changed " + postUpdated.getTime());

		assertFalse("Date didn't change", preUpdated.getTime() ==
postUpdated.getTime());
	}
}
</pre>

You probably want to see the persistence.xml file too (there is no orm.xml
file, or rather it is empty):

<pre>
<?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="JPA-Experiments"
transaction-type="RESOURCE_LOCAL">
        <description>The persistent unit for Example domain
classes.</description>
        
        <class>org.syger.example.domain.AbstractModel</class>
        <class>org.syger.example.domain.User</class>
        <class>org.syger.example.domain.Client</class>
        <class>org.syger.example.domain.Tagging</class>
        <class>org.syger.example.domain.Tag</class>

        <exclude-unlisted-classes>true</exclude-unlisted-classes>
        
        <properties>
            <!-- Hibernate properties -->
            <property name="hibernate.cfg.customizer"
value="org.syger.example.domain.HibernateConfiguration"/>
            
            <!-- TopLink Essentials properties -->
            <property name="persistence.tools.weaving" value="true"/>
            <property name="toplink.session.customizer"
value="org.syger.example.domain.TopLinkConfiguration"/>

            <!-- EclipseLink properties -->
            <property name="persistence.tools.weaving" value="true"/>
            <property name="eclipselink.session.customizer"
value="org.syger.example.domain.EclipseLinkConfiguration"/>

            <!-- OpenJPA properties -->
            <property name="openjpa.Log" value="DefaultLevel=TRACE,
Enhance=TRACE, MetaData=TRACE, Runtime=TRACE, Tool=TRACE, SQL=TRACE"/>
        </properties>
    </persistence-unit>
</persistence>
</pre>

Finally, just to make this post a little longer, first the TopLink
Essentials Spring config file:

<pre>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
                           http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                           http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
                           http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee-2.5.xsd
                           http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">

    <context:annotation-config/>

    <bean id="entityManagerFactory"
         
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="persistenceXmlLocation"
value="classpath:META-INF/persistence.xml"/>
        <property name="persistenceUnitName" value="JPA-Experiments"/>
        <property name="jpaVendorAdapter">
            <bean
class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
                <property name="databasePlatform"
value="oracle.toplink.essentials.platform.database.HSQLPlatform"/>
                <property name="showSql" value="true"/>
                <property name="generateDdl" value="true"/>
            </bean>
        </property>
    </bean>

    <bean id="dataSource" 
         
class="org.springframework.jdbc.datasource.DriverManagerDataSource"
          destroy-method="close">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
        <property name="url" value="jdbc:hsqldb:mem:testDb"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
    </bean>
    
    <bean id="transactionManager"
          class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    <bean
class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>
    <bean
class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>

    <bean id="tagManager" class="org.syger.example.manager.TagManager"
scope="prototype"/>
    <bean id="dataManager" class="org.syger.example.manager.DataManager"
scope="prototype">
        <property name="tagManager" ref="tagManager"/>
    </bean>
</beans>
</pre>

Then the EclipseLink Spring config file:

<pre>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
                           http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                           http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
                           http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee-2.5.xsd
                           http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">

    <context:annotation-config/>

    <bean id="entityManagerFactory"
         
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="persistenceXmlLocation"
value="classpath:META-INF/persistence.xml"/>
        <property name="persistenceUnitName" value="JPA-Experiments"/>
        <property name="jpaVendorAdapter">
            <bean
class="org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter">
                <property name="databasePlatform"
value="org.eclipse.persistence.platform.database.HSQLPlatform"/>
                <property name="showSql" value="true"/>
                <property name="generateDdl" value="true"/>
            </bean>
        </property>
    </bean>

    <bean id="dataSource" 
         
class="org.springframework.jdbc.datasource.DriverManagerDataSource"
          destroy-method="close">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
        <property name="url" value="jdbc:hsqldb:mem:testDb"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
    </bean>
    
    <bean id="transactionManager"
          class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    <bean
class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>
    <bean
class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>

    <bean id="tagManager" class="org.syger.example.manager.TagManager"
scope="prototype"/>
    <bean id="dataManager" class="org.syger.example.manager.DataManager"
scope="prototype">
        <property name="tagManager" ref="tagManager"/>
    </bean>
</beans>
</pre>

Again, you might find it quicker just to grab the Subversion trunk. I run
all the tests via an ant script within a DOS prompt, on Windows XP
Professional SP2. The computer was surrounded by a 3 inch thick anti-murphy
barrier, which I will be sending back to the manufacturers...

Sorry about the <pre></pre> marks, I thought they'd work

Hope that helps,

John


Shaun Smith wrote:
  
Hi John,

    
Shaun Smith wrote:
  
      
In terms of the problem you're addressing, things have changed since you 
wrote the article.
    
        
It's only a couple of week old!
  
      
No one can say we're sitting around doing nothing! ;-)
...
    
FYI, I have now updated Spikes to use TopLink Essentials, and there have
been a couple of problems:

1. Deliberately setting a non null field to null, and then attempting to
persist the object does not give back a Spring wrapped exception (as it
does
in EclipseLink), but rather the infamous NPE trying to call hashCode -
which
I assume means that TopLink is trying to put the object in its own cache,
without any 'formal' validation checks first - that's my hypothesis, more
than likely to be wrong. Here's a piece of the stack dump:
  
      
Can you post the class you're trying to persist and it's mappings?
...
    
2. Both TopLink and EclipseLink fail to update a @PreUpdate annotated
method
when that method is not  public - when it's public things work just fine.
Yet the specs state that (section 3.5.1)

The callback methods can have public, private, protected, or package
level
access, but must not be
static or final.

Here's the method:

	/**
	 * Settaggio della data del'ultimo aggiornamento prima del salvataggio -
	 * Sets the last updated date prior to saving.
	 */
	@PreUpdate 
	protected void preUpdate() {
		lastUpdated = new java.util.Date();
	}

Change protected to public and Robert is you proverbial father's brother.
This is not a showstopper of course, I'm personally not that squeemish
about
making the method public.
  
      
I couldn't reproduce this problem with EclipseLink M7.  I began a 
transaction, queried an object, modified a String field, and committed 
the transaction.  I tried package, private, protected, and public.  Each 
time my @PreUpdate method was called.

    Shaun
-- 


Oracle <http://www.oracle.com>
Shaun Smith | Principal Product Manager, TopLink | +1.905.502.3094
Oracle Fusion Middleware
110 Matheson Boulevard West, Suite 100
Mississauga, Ontario, Canada L5R 3P4

_______________________________________________
eclipselink-users mailing list
eclipselink-users@xxxxxxxxxxx
https://dev.eclipse.org/mailman/listinfo/eclipselink-users


    
  

--


Oracle
Shaun Smith | Principal Product Manager, TopLink | +1.905.502.3094
Oracle Fusion Middleware
110 Matheson Boulevard West, Suite 100
Mississauga, Ontario, Canada L5R 3P4

Back to the top