Richard Zowalla created OPENJPA-3019:
----------------------------------------

             Summary: In-place change to an element of a collection converted 
as a whole is not written
                 Key: OPENJPA-3019
                 URL: https://issues.apache.org/jira/browse/OPENJPA-3019
             Project: OpenJPA
          Issue Type: Bug
            Reporter: Richard Zowalla


A change made in place to an element of a collection that is converted as a 
whole is not written to the database. Given

{code:java}
@Convert(converter = ItemListConverter.class)   // 
AttributeConverter<List<Item>, String>
private List<Item> items = new ArrayList<>();
{code}

the following commits without an {{UPDATE}}, and the change is lost:

{code:java}
em.getTransaction().begin();
em.find(InPlaceConvertedEntity.class, 1L).getItems().get(0).setName("changed");
em.getTransaction().commit();
{code}

Adding or removing an element is written. So is a change made in place to a 
converted attribute that is not a collection, e.g. {{@Convert(converter = 
ItemConverter.class) private Item item}} followed by 
{{getItem().setName("changed")}}. Only the collection case is affected. The 
collection is wrapped in a proxy that tracks adds and removes, and the field is 
only marked dirty through that proxy, so a change inside an element is never 
seen.

The specification requires the change to be written. Section "Type Conversion 
of Basic Attributes": "A converted attribute is considered a basic attribute". 
Section "Synchronization to the Database": "A modification to the state of an 
entity might include an assignment of a new value to a persistent property or 
field of the entity, or even modification of a mutable value of a persistent 
property or field."

Mapping a collection converted as a whole only works with OPENJPA-3017 (PR 
#220). Reported there by Francesco Chicchiriccò while testing Apache Syncope 
against 4.2.0-SNAPSHOT: changing a value of a {{PlainAttr}} held in a converted 
{{List<PlainAttr>}} and merging the user produced no {{UPDATE}}, while removing 
the attribute, changing it and adding it back did.

Test case, on top of OPENJPA-3017, package 
{{org.apache.openjpa.persistence.conversion}}:

{code:java}
@Converter
public class ItemConverter implements AttributeConverter<Item, String> {
    public String convertToDatabaseColumn(Item attribute) {
        return attribute == null ? null : attribute.getName();
    }
    public Item convertToEntityAttribute(String dbData) {
        return dbData == null ? null : new Item(dbData);
    }
}

@Entity
public class InPlaceConvertedEntity {
    @Id
    private long id;

    @Convert(converter = ItemListConverter.class)
    private List<Item> items = new ArrayList<>();

    @Convert(converter = ItemConverter.class)
    private Item item;
    // getters and setters
}

public class TestConvertedInPlaceChange extends SingleEMFTestCase {

    public void setUp() {
        setUp(InPlaceConvertedEntity.class, CLEAR_TABLES);
        try (EntityManager em = emf.createEntityManager()) {
            em.getTransaction().begin();
            InPlaceConvertedEntity e = new InPlaceConvertedEntity();
            e.setId(1L);
            e.getItems().add(new Item("one"));
            e.getItems().add(new Item("two"));
            e.setItem(new Item("single"));
            em.persist(e);
            em.getTransaction().commit();
        }
    }

    // fails: expected:<[changed]> but was:<[one]>
    public void testElementOfConvertedListChangedInPlace() {
        try (EntityManager em = emf.createEntityManager()) {
            em.getTransaction().begin();
            em.find(InPlaceConvertedEntity.class, 
1L).getItems().get(0).setName("changed");
            em.getTransaction().commit();
        }
        try (EntityManager em = emf.createEntityManager()) {
            assertEquals("changed",
                em.find(InPlaceConvertedEntity.class, 
1L).getItems().get(0).getName());
        }
    }

    // passes
    public void testConvertedValueChangedInPlace() {
        try (EntityManager em = emf.createEntityManager()) {
            em.getTransaction().begin();
            em.find(InPlaceConvertedEntity.class, 
1L).getItem().setName("changed");
            em.getTransaction().commit();
        }
        try (EntityManager em = emf.createEntityManager()) {
            assertEquals("changed", em.find(InPlaceConvertedEntity.class, 
1L).getItem().getName());
        }
    }

    // passes
    public void testConvertedListChangedStructurally() {
        try (EntityManager em = emf.createEntityManager()) {
            em.getTransaction().begin();
            em.find(InPlaceConvertedEntity.class, 1L).getItems().add(new 
Item("three"));
            em.getTransaction().commit();
        }
        try (EntityManager em = emf.createEntityManager()) {
            assertEquals(3, em.find(InPlaceConvertedEntity.class, 
1L).getItems().size());
        }
    }
}
{code}

{{Item}} is a plain {{Serializable}} bean with a {{name}} property, and 
{{ItemListConverter}} joins the names with commas, as in the tests of 
OPENJPA-3017.

Possible fix: at flush, compare the converted database value of such a field 
with the value it had when it was loaded, and mark the field dirty when they 
differ. Alternatively, do not proxy a collection that is converted as a whole 
and treat it like the non-collection case.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to