Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I use JPA to persist a Map (java.util.Map) object inside an entity and ensure the persistence cascades?

I have recently started playing around with the Play! Framework for Java, version 1.2.3 (the latest). While testing out the framework, I came across a strange problem when trying to persist a Map object inside a Hibernate entity called FooSystem. The Map object maps a long to a Hibernate entity I have called Foo, with the declaration Map<Long, Foo> fooMap;

My problem is as follows: The correct tables are created as I have annotated them. However, when the FooSystem object fs is persisted, the data in fs.fooMap is not!

Here is the code I am using for the entities. First is Foo:

package models.test;

import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import play.db.jpa.Model;

@Entity
public class Foo extends Model
{
    @ManyToOne
    private FooSystem foosystem;

    public Foo(FooSystem foosystem)
    {
        this.foosystem = foosystem;
    }
}

And here is FooSystem:

package models.test;

import java.util.HashMap;
import java.util.Map;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import play.db.jpa.Model;

@Entity
public class FooSystem extends Model
{
    @ManyToMany(cascade = {CascadeType.ALL, CascadeType.PERSIST})
    @JoinTable(
        name = "fooMap",
        joinColumns = @JoinColumn(name = "foosystem"),
        inverseJoinColumns = @JoinColumn(name = "foo")
    )
    private Map<Long, Foo> fooMap = new HashMap<Long, Foo>();

    public FooSystem()
    {
        Foo f1 = new Foo(this);
        Foo f2 = new Foo(this);
        fooMap.put(f1.getId(), f1);
        fooMap.put(f2.getId(), f2);
    }

    public Map<Long, Foo> getFooMap()
    {
        return fooMap;
    }
}

Here is the Controller class I am using to test my set-up:

package controllers;

import javax.persistence.EntityManager;
import models.test.FooSystem;
import play.db.jpa.JPA;
import play.mvc.Controller;

public class TestController extends Controller
{
    public static void index() {
        EntityManager em = JPA.em();
        FooSystem fs = new FooSystem();
        em.persist(fs);
        render();
    }
}

The Play! framework automatically created a transaction for the HTTP request. Although data is inserted into the foo and foosystem tables, nothing is ever inserted into the foomap table, which is the desired result. What can I do about this? What am I missing?

like image 756
fruchtose Avatar asked Sep 22 '11 20:09

fruchtose


1 Answers

I managed to solve this problem using the advice of Java Ka Baby. The issue was actually not in my Model classes; the problem lay within the Controller. Specifically, I was saving the entities in the wrong order. Once I realized that using the @ElementCollection annotation on the Map<Long, Foo> produced the same effects as the join table I was manually specifying, I tried I thought experiment where I re-thought how I was saving my entities.

In the code I posted above, you can see in the FooSystem constructor that two Foo objects, f1 and f2, are put into fooMap before the Foo objects are persisted. I realized that if f1 is not in the database when it is put into the map, how is JPA able to use its ID as a foreign key in the join table?

If you can see where I'm going with this line of reasoning, you can see that the obvious answer is that JPA is not able to accomplish this amazing feat of using a foreign key to reference a nonexistent key. The bizarre thing is that the Play! console did not note any errors at all for the original code I posted, even though it was not correct at all. Either the framework swallowed every Exception thrown during the process, or I've written code that should produce an Exception.

So to fix the problem, I persisted the Foo entities before any operations were performed on them. Only then did I put them into fooMap. Finally, once fooMap was populated, I persisted the FooSystem entity.

Here is the corrected TestController class:

package controllers;

import javax.persistence.EntityManager;
import models.test.Foo;
import models.test.FooSystem;
import play.db.jpa.JPA;
import play.mvc.Controller;

public class TestController extends Controller
{
    public static void index() {
        EntityManager em = JPA.em();
        FooSystem fs = new FooSystem();
        Foo f1 = new Foo(fs);
        Foo f2 = new Foo(fs);
        f1.save();
        f2.save();
        fs.put(f1.getId(), f1);
        fs.put(f2.getId(), f2);
        fs.save();
        render();
    }
}

And, since I changed FooSystem, here is the final code for that class:

package models.test;

import java.util.HashMap;
import java.util.Map;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import play.db.jpa.Model;

@Entity
public class FooSystem extends Model
{
    @ElementCollection
    private Map<Long, Foo> fooMap = new HashMap<Long, Foo>();

    public FooSystem()
    {
    }

    public Map<Long, Foo> getFooMap()
    {
        return fooMap;
    }

    public void put(Long l, Foo f)
    {
        fooMap.put(l, f);
    }
}
like image 128
fruchtose Avatar answered Oct 14 '22 00:10

fruchtose