Updating JDO M-N relation in Datanucleus is not persisted

98 views Asked by At

I have been exploring the Datanucleus framework and was trying some of the examples provided in the documentation. I got stuck in the M-N relation example using Sets. When I try to remove an object from the Set it is removed from the Set, but once I persist the container objects, the entry in the join table is not removed. This results in my container objects still holding the removed object.

I have a unit test that exposes the issue:

    @Test
public void testMNRelation() {
    final Product product = new Product();
    product.setName("Product 1");
    product.setPrice(100);

    final Product product2 = new Product();
    product2.setName("Product 2");
    product2.setPrice(130);

    final Supplier supplier = new Supplier();
    supplier.setName("Supplier 1");

    Set<Supplier> suppliers = product.getSuppliers();
    suppliers.add(supplier);

    Set<Product> products = supplier.getProducts();
    products.add(product2);

    final PersistenceManagerFactory pmf = JDOHelper.getPersistenceManagerFactory("MyStore");
    PersistenceManager pm = pmf.getPersistenceManager();

    Transaction tx = pm.currentTransaction();
    try {
        tx.begin();
        pm.makePersistent(product);
        pm.makePersistent(product2);
        tx.commit();
    } finally {
        if (tx.isActive()) {
            tx.rollback();
        }
    }

    pm.close();

    pm = pmf.getPersistenceManager();

    tx = pm.currentTransaction();
    try {
        tx.begin();
        System.out.println();
        System.out.println("Fetch from store (before removal)");
        Supplier s = pm.getObjectById(Supplier.class, supplier.getId());
        System.out.println("supplier name: " + s.getName());
        System.out.println("# products: " + s.getProducts().size());
        Product p = pm.getObjectById(Product.class, product.getId());
        System.out.println("product name: " + p.getName());
        System.out.println("# suppliers: " + p.getSuppliers().size());
        Product p2 = pm.getObjectById(Product.class, product2.getId());
        System.out.println("product name: " + p2.getName());
        System.out.println("# suppliers: " + p2.getSuppliers().size());
        tx.commit();
    } finally {
        if (tx.isActive()) {
            tx.rollback();
        }
    }

    pm.close();

    Set<Product> updatedProducts = supplier.getProducts();
    updatedProducts.remove(product);
    Set<Supplier> updatedSuppliers = product.getSuppliers();
    updatedSuppliers.remove(supplier);

    System.out.println();
    System.out.println("Before persist (after removal)");
    System.out.println("supplier name: " + supplier.getName());
    System.out.println("# products: " + supplier.getProducts().size());
    System.out.println("product name: " + product.getName());
    System.out.println("# suppliers: " + product.getSuppliers().size());
    System.out.println("product name: " + product2.getName());
    System.out.println("# suppliers: " + product2.getSuppliers().size());

    pm = pmf.getPersistenceManager();

    tx = pm.currentTransaction();
    try {
        tx.begin();
        pm.makePersistent(supplier);
        pm.makePersistent(product);
        tx.commit();
    } finally {
        if (tx.isActive()) {
            tx.rollback();
        }
    }

    pm.close();

    System.out.println();
    System.out.println("After persist");
    System.out.println("supplier name: " + supplier.getName());
    System.out.println("# products: " + supplier.getProducts().size());
    System.out.println("product name: " + product.getName());
    System.out.println("# suppliers: " + product.getSuppliers().size());
    System.out.println("product name: " + product2.getName());
    System.out.println("# suppliers: " + product2.getSuppliers().size());

    pm = pmf.getPersistenceManager();

    tx = pm.currentTransaction();
    try {
        tx.begin();
        System.out.println();
        System.out.println("Fetch from store");
        Supplier s = pm.getObjectById(Supplier.class, supplier.getId());
        System.out.println("supplier name: " + s.getName());
        System.out.println("# products: " + s.getProducts().size());
        Product p = pm.getObjectById(Product.class, product.getId());
        System.out.println("product name: " + p.getName());
        System.out.println("# suppliers: " + p.getSuppliers().size());
        Product p2 = pm.getObjectById(Product.class, product2.getId());
        System.out.println("product name: " + p2.getName());
        System.out.println("# suppliers: " + p2.getSuppliers().size());
        tx.commit();
    } finally {
        if (tx.isActive()) {
            tx.rollback();
        }
    }

    pm.close();
}

And the 2 types used in the example from http://www.datanucleus.org/products/datanucleus/jdo/orm/many_to_many.html

@PersistenceCapable(detachable="true")
public class Supplier {

@PrimaryKey
@Persistent(valueStrategy=IdGeneratorStrategy.INCREMENT)
private long id;

@Persistent
private String name;

@Persistent(mappedBy="suppliers")
private Set<Product> products = new HashSet<Product>();

public long getId() {
    return id;
}

public void setId(long id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public Set<Product> getProducts() {
    return products;
}

public void setProducts(Set<Product> products) {
    this.products = products;
}

@Override
public boolean equals(Object o) {
    if(this.getId() == 0) {
        return false;
    }
    if(o instanceof Supplier) {
        Supplier other = (Supplier) o;
        return this.getId() == other.getId();
    }
    return false;
}

@Override
public int hashCode() {
    return Long.toString(this.getId()).hashCode();
}
}

And

@PersistenceCapable(detachable="true")
public class Product {


@PrimaryKey
@Persistent(valueStrategy=IdGeneratorStrategy.INCREMENT)
private long id;


@Persistent
private String name;

@Persistent
private double price;

@Persistent(table="PRODUCTS_SUPPLIERS")
@Join(column="PRODUCT_ID")
@Element(column="SUPPLIER_ID")
private Set<Supplier> suppliers = new HashSet<Supplier>();


public long getId() {
    return id;
}

public void setId(long id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public double getPrice() {
    return price;
}

public void setPrice(double price) {
    this.price = price;
}

public Set<Supplier> getSuppliers() {
    return suppliers;
}

public void setSuppliers(Set<Supplier> suppliers) {
    this.suppliers = suppliers;
}

@Override
public boolean equals(Object o) {
    if(getId() == 0) {
        return false;
    }
    if(o instanceof Product) {
        Product other = (Product) o;
        return this.getId() == other.getId();
    }
    return false;
}

@Override
public int hashCode() {
    return Long.toString(this.getId()).hashCode();
}

}

Now, in the console I get

Fetch from store (before removal)
supplier name: Supplier 1
# products: 2
product name: Product 1
# suppliers: 1
product name: Product 2
# suppliers: 1

Before persist (after removal)
supplier name: Supplier 1
# products: 1
product name: Product 1
# suppliers: 0
product name: Product 2
# suppliers: 1

After persist
supplier name: Supplier 1
# products: 1
product name: Product 1
# suppliers: 1
product name: Product 2
# suppliers: 1

Fetch from store
supplier name: Supplier 1
# products: 1
product name: Product 1
# suppliers: 1
product name: Product 2
# suppliers: 1

I would expect something like

Fetch from store (before removal)
supplier name: Supplier 1
# products: 2
product name: Product 1
# suppliers: 1
product name: Product 2
# suppliers: 1

Before persist (after removal)
supplier name: Supplier 1
# products: 1
product name: Product 1
# suppliers: 0
product name: Product 2
# suppliers: 1

After persist
supplier name: Supplier 1
# products: 1
product name: Product 1
# suppliers: 0
product name: Product 2
# suppliers: 1

Fetch from store
supplier name: Supplier 1
# products: 1
product name: Product 1
# suppliers: 0
product name: Product 2
# suppliers: 1

My persistence.xml includes:

<property name="datanucleus.DetachAllOnCommit" value="true" />
<property name="datanucleus.attachSameDatastore" value="true" />
<property name="datanucleus.CopyOnAttach" value="false" />
<property name="datanucleus.cache.collections.lazy" value="true" />
<property name="datanucleus.manageRelationships" value="true" />
<property name="datanucleus.manageRelationshipsChecks" value="true" />

Any idea on how to solve this?

1

There are 1 answers

0
Lynge On BEST ANSWER

I've got a solution to the issue. The issue seems to relate to my persistence settings. I changed the settings to

<property name="datanucleus.DetachAllOnCommit" value="true" />
<property name="datanucleus.attachSameDatastore" value="true" />
<property name="datanucleus.CopyOnAttach" value="true" />
<property name="datanucleus.cache.collections.lazy" value="true" />
<property name="datanucleus.manageRelationships" value="false" />
<property name="datanucleus.manageRelationshipsChecks" value="false" />
<property name="datanucleus.persistenceByReachabilityAtCommit" value="false" />

as I now handle both sides of the relation in my implementation.

Furthermore I implemented a Factory to simplify my test

public class Factory {

  private static final PersistenceManagerFactory pmf = get();

  private static PersistenceManagerFactory get() {
    return JDOHelper.getPersistenceManagerFactory("MyStore");
  }

  public static Product buildProduct(String name, double price) {
    Product product = new Product();
    product.setName(name);
    product.setPrice(price);
    save(product);
    return product;
  }

  public static Supplier buildSupplier(String name) {
    Supplier supplier = new Supplier();
    supplier.setName(name);
    save(supplier);
    return supplier;
  }

  public static void addRelation(final Supplier supplier, final Product product) {
    product.getSuppliers().add(supplier);
    supplier.getProducts().add(product);
    save(product, supplier);
  }

  public static void removeRelation(Supplier supplier, Product product) {
    product.getSuppliers().remove(supplier);
    supplier.getProducts().remove(product);
    save(product, supplier);
  }

  public static Product loadProduct(long id) {
    return (Product) load(Product.class, id);
  }

  public static Supplier loadSupplier(long id) {
    return (Supplier) load(Supplier.class, id);
  }

  private static void save(final Object... objects) {
    PersistenceManager pm = pmf.getPersistenceManager();
    Transaction tx = pm.currentTransaction();
    try {
      tx.begin();
      pm.makePersistentAll(objects);
      tx.commit();
    } finally {
      if (tx.isActive()) {
        tx.rollback();
      }
    }
    pm.close();
  }

  private static Object load(Class<?> clazz, long id) {
    final Object[] result = new Object[1];
    PersistenceManager pm = pmf.getPersistenceManager();
    Transaction tx = pm.currentTransaction();
    try {
      tx.begin();
      result[0] = pm.getObjectById(clazz, id);
      tx.commit();
    } finally {
      if (tx.isActive()) {
        tx.rollback();
      }
    }
    pm.close();
    if (result[0] == null) {
      throw new NoSuchElementException(clazz.getSimpleName() + " with id=" + id + " cannot be found");
    }
    return result[0];
  }
}

So the test is simply expressed as

@Test
public void testFactoryApproach() {
    Product p1 = Factory.buildProduct("product1", 50);
    Product p2 = Factory.buildProduct("product2", 70);
    Supplier s1 = Factory.buildSupplier("Supplier1");

    System.out.println("Initial:");
    System.out.println(p1);
    System.out.println(p2);
    System.out.println(s1);
    System.out.println();

    Factory.addRelation(s1, p1);
    Factory.addRelation(s1, p2);

    System.out.println("Relations after add:");
    System.out.println(p1);
    System.out.println(p2);
    System.out.println(s1);
    System.out.println();

    Factory.removeRelation(s1, p1);

    System.out.println("Relations after remove:");
    System.out.println(p1);
    System.out.println(p2);
    System.out.println(s1);
    System.out.println();

    System.out.println("Relations after load:");
    System.out.println(Factory.loadProduct(p1.getId()));
    System.out.println(Factory.loadProduct(p2.getId()));
    System.out.println(Factory.loadSupplier(s1.getId()));
}

Which result in the following output

Initial:
Product:11 (product1) - # suppliers = 0 []
Product:12 (product2) - # suppliers = 0 []
Supplier:11 (Supplier1) - # products = 0 []

Relations after add:
Product:11 (product1) - # suppliers = 1 [11:Supplier1]
Product:12 (product2) - # suppliers = 1 [11:Supplier1]
Supplier:11 (Supplier1) - # products = 2 [11:product1, 12:product2]

Relations after remove:
Product:11 (product1) - # suppliers = 0 []
Product:12 (product2) - # suppliers = 1 [11:Supplier1]
Supplier:11 (Supplier1) - # products = 1 [12:product2]

Relations after load:
Product:11 (product1) - # suppliers = 0 []
Product:12 (product2) - # suppliers = 1 [11:Supplier1]
Supplier:11 (Supplier1) - # products = 1 [12:product2]