Although this workaround works in my toy example, it does not seem to work in our application, switching to GenerationType.TABLE does.
Henno -----Oorspronkelijk bericht----- Van: Henno Vermeulen [mailto:[email protected]] Verzonden: maandag 21 februari 2011 17:50 Aan: '[email protected]' Onderwerp: RE: adding new entities to an existing List fails when using GenerationType.IDENTITY I found a workaround: In my test I directly added the new ProductOrderLines to the List in the ProductOrder. I do not know the implementation of this List because it is the one returned by OpenJPA (at least after detaching). When I set the list reference to a new arraylist and add both the existing elements and the new ones to this list, the test passes. I.e. change the line firstSaved.getProducts().addAll(0, Arrays.asList(banana, pear)); to List<ProductOrderLine> newProducts = new ArrayList<ProductOrderLine>(); firstSaved.getProducts().addAll(Arrays.asList(banana, pear)); newProducts.addAll(firstSaved.getProducts()); firstSaved.setProducts(newProducts); Makes the test work. Regards, Henno -----Oorspronkelijk bericht----- Van: Henno Vermeulen [mailto:[email protected]] Verzonden: maandag 21 februari 2011 17:19 Aan: '[email protected]' Onderwerp: adding new entities to an existing List fails when using GenerationType.IDENTITY Hello, I think I found a serious issue (in both OpenJPA 2.0.0 and 2.1.0) when I try to add new entities to an existing list and I use a generated identity with GenerationType.IDENTITY. I start with a fresh database and let OpenJPA create the schema. I have a ProductOrder entity that contains a List of ProductOrderLines, annotated as such: @Entity public class ProductOrder { ... @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) private List<ProductOrderLine> products = new ArrayList<ProductOrderLine>(); ... } The entity in the List (ProductOrderLine) has a generated id with GenerationType.IDENTITY. @Entity public class ProductOrderLine { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; ... } I start with a ProductOrder that has these products: 1 - orange 2 - apple I insert two new products into the list so that I get: null - banana null - pear 1 - orange 2 - apple Then I merge the entity (I work with attach/detach, not sure if this matters). OpenJPA merge correctly returns a ProductOrder with this list: 3 - banana 4 - pear 1 - orange 2 - apple However OpenJPA generates the wrong SQL so that the database contains something completely different and indeed selecting the ProductOrder by it's id gives: 3 - banana 4 - pear 4 - pear 4 - pear I tested this with sql server. (I tried hsqldb but this also suffers from bug https://issues.apache.org/jira/browse/OPENJPA-1066). This problem does not occur when I use GenerationType.AUTO for ProductOrderLine. My example uses a join table, but foreign key columns seem to have the same problem. Should I use another generation type? If so which one, and is it compatible with existing sql server data that had id values automatically generated by sql server? Please let me know if I should file a bug report and if I should attempt to convert my unit test to one accepted by OpenJPA standards. Regards, Henno Vermeulen package entities; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity public class ProductOrderLine { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; public ProductOrderLine() { } public ProductOrderLine(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Long getId() { return id; } } package entities; import java.util.ArrayList; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; @Entity public class ProductOrder { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // Workaround for https://issues.apache.org/jira/browse/OPENJPA-1947 private String name; @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) private List<ProductOrderLine> products = new ArrayList<ProductOrderLine>(); public ProductOrder() { } public Long getId() { return id; } public void setProducts(List<ProductOrderLine> products) { this.products = products; } public List<ProductOrderLine> getProducts() { return products; } } import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import junit.framework.TestCase; import entities.ProductOrder; import entities.ProductOrderLine; /** * I tested this with MSSQL SERVER (I tried hsqldb but this suffers from an * additional problem where it mixes up id 0 with id null, see * https://issues.apache.org/jira/browse/OPENJPA-1066)/ * * @author Henno Vermeulen */ public class ProductOrderTest extends TestCase { private EntityManagerFactory factory; public void setUp() { factory = Persistence.createEntityManagerFactory("testPU", System .getProperties()); } public void testProductOrderLineCopy() { // If we do this, it fails after the first save!!!! // hsqldb seems to convert null to 0 and confuse this with an existing // id or something // insertSomeData(); ProductOrder original = new ProductOrder(); ArrayList<ProductOrderLine> products = new ArrayList<ProductOrderLine>(); products.addAll(Arrays.asList(new ProductOrderLine("orange"), new ProductOrderLine("apple"))); original.setProducts(products); // START // null - orange // null - apple printProducts("Before first save:", original); ProductOrder firstSaved = save(original); // 1 - orange // 2 - apple printProducts("After first save:", firstSaved); ProductOrderLine orange = firstSaved.getProducts().get(0); ProductOrderLine apple = firstSaved.getProducts().get(1); assertEquals("orange", orange.getName()); assertEquals("apple", apple.getName()); Long orangeId = orange.getId(); Long appleId = apple.getId(); assertTrue(orangeId != null && appleId != null); ProductOrderLine banana = new ProductOrderLine("banana"); ProductOrderLine pear = new ProductOrderLine("pear"); assertTrue(banana.getId() == null && pear.getId() == null); firstSaved.getProducts().addAll(0, Arrays.asList(banana, pear)); // Changed to // null - banana // null - pear // 1 - orange // 2 - apple printProducts("Before second save:", firstSaved); assertExpectedProductLines(firstSaved, orangeId, appleId, true); ProductOrder secondSaved = save(firstSaved); // Expected after save: // 3 - banana // 4 - pear // 1 - orange // 2 - apple // On SQL server this is actually returned by save, but not by find! printProducts("After second save:", secondSaved); assertExpectedProductLines(secondSaved, orangeId, appleId, false); ProductOrder found = findById(secondSaved.getId()); printProducts("Found by id after second save:", found); // This one fails!!!! // On SQL server this returns // 3 - banana // 4 - pear // 4 - pear // 4 - pear assertExpectedProductLines(found, orangeId, appleId, false); } private void printProducts(String description, ProductOrder order) { System.out.println(description); for (ProductOrderLine p : order.getProducts()) { System.out.println(p.getId() + " - " + p.getName()); } } private void assertExpectedProductLines(ProductOrder line, Long orangeId, Long appleId, boolean beforeMerge) { List<ProductOrderLine> products = new ArrayList<ProductOrderLine>(); products.addAll(line.getProducts()); Collections.sort(products, new Comparator<ProductOrderLine>() { @Override public int compare(ProductOrderLine o1, ProductOrderLine o2) { return o1.getName().compareTo(o2.getName()); } }); assertEquals("apple", products.get(0).getName()); assertEquals(appleId, products.get(0).getId()); assertEquals("banana", products.get(1).getName()); assertEquals(beforeMerge, null == products.get(1).getId()); assertEquals("orange", products.get(2).getName()); assertEquals(orangeId, products.get(2).getId()); assertEquals("pear", products.get(3).getName()); assertEquals(beforeMerge, null == products.get(3).getId()); } private ProductOrder save(ProductOrder order) { EntityManager em = factory.createEntityManager(); em.getTransaction().begin(); ProductOrder result; if (order.getId() == null) { em.persist(order); result = order; } else { result = em.merge(order); } em.getTransaction().commit(); em.detach(result); em.close(); return result; } private ProductOrder findById(Long id) { EntityManager em = factory.createEntityManager(); em.getTransaction().begin(); ProductOrder result = em.find(ProductOrder.class, id); em.getTransaction().commit(); em.detach(result); em.close(); return result; } } <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 persistence_1_0.xsd" version="1.0"> <persistence-unit name="testPU"> <provider>org.apache.openjpa.persistence.PersistenceProviderImpl </provider> <class>entities.ProductOrder</class> <class>entities.ProductOrderLine</class> <exclude-unlisted-classes>true</exclude-unlisted-classes> <properties> <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema" /> <property name="openjpa.ConnectionDriverName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" /> <property name="openjpa.ConnectionURL" value="jdbc:sqlserver://localhost\\SQL2008:1433;databaseName=obliprototypeunittest" /> <property name="openjpa.ConnectionUserName" value="obliprototype" /> <property name="openjpa.ConnectionPassword" value="hidden" /> <property name="openjpa.Log" value="DefaultLevel=INFO" /> </properties> </persistence-unit> </persistence>
