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>