JPA Many-To-Many relationship with an extra enum column

121 views
Skip to first unread message

naXa via StackOverflow

unread,
Apr 13, 2015, 11:19:10 AM4/13/15
to google-appengin...@googlegroups.com

I'm trying to persist an entity that has a Map of Objects to Enum into a Google App Engine datastore. The entity classes are annotated using JPA.

Event class

import com.google.appengine.datanucleus.annotations.Unowned;
import com.google.appengine.api.datastore.Key;
import java.util.Map;
import javax.persistence.*;
import lombok.Builder;
import lombok.Data;

@Entity
@Builder
public @Data class Event {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

    // I want a map belonging to event in order to query a particular user whether he confirmed his participation in the event
    // All addressees are initially present in this map with response set to UNDEFINED
    // If user has received and read notification, than the response is updated to YES, NO, or MAYBE
    @Unowned
    @ElementCollection
    @CollectionTable(name = "user_response")
    @MapKeyJoinColumn(name = "user_id")
    @Enumerated 
    @Column(name = "response")
    private Map<User, Response> addressees;

}

Response class

public enum Response {
    UNDEFINED, YES, NO, MAYBE
}

I haven't defined any references in User class to this map. It's a unidirectional relationship.

User class

@Entity
public @Data class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;
}

The Event.addressees column seems pretty tricky. So I ran my test to check if everything was working correctly. Well, it was not. I got an exception when I tried to save an Event entity to the datastore:

java.lang.IllegalArgumentException: addressees: Response is not a supported property type.
    at com.google.appengine.api.datastore.DataTypeUtils.checkSupportedSingleValue(DataTypeUtils.java:235)
    at com.google.appengine.api.datastore.DataTypeUtils.checkSupportedValue(DataTypeUtils.java:199)
    at com.google.appengine.api.datastore.DataTypeUtils.checkSupportedValue(DataTypeUtils.java:173)
    at com.google.appengine.api.datastore.DataTypeUtils.checkSupportedValue(DataTypeUtils.java:148)
    at com.google.appengine.api.datastore.PropertyContainer.setProperty(PropertyContainer.java:101)

According to DataNucleus Enum is a persistable data type by default. So I don't understand why I get the error message saying "Response is not a supported property type".
I suspected that the problem was with the User class. Maybe the association from Event to Users was not enough, and User should also have an association to Events. So I've added the events field to User as follows:

@Unowned
@ElementCollection
@CollectionTable(name = "user_event_responses")
@ManyToMany(mappedBy="addressees", targetEntity = Event.class)
@MapKeyJoinColumn
@Enumerated 
@Column(name = "response")
private Map<Event, Response> events;

It didn't work anyway. Then I've read similar questions, and found no quick answer.
Please, show me an example of many-to-many relationship with an extra column in DataNucleus / JPA!



Please DO NOT REPLY directly to this email but go to StackOverflow:
http://stackoverflow.com/questions/29608946/jpa-many-to-many-relationship-with-an-extra-enum-column

naXa via StackOverflow

unread,
Apr 13, 2015, 11:19:12 AM4/13/15
to google-appengin...@googlegroups.com

Problem of creating two classes that have a Many-To-Many relationship, but the relational join table has additional data, is a frequent problem.

Good examples on this topic I've found at WikiBooks - Java Persistence / Many-To-Many and in the article Mapping a Many-To-Many Join Table with extra column using JPA by Giovanni Gargiulo. References in the official documentation I've found much, much later: Unowned Entity Relationships in JDO and Unsupported Features of JPA 2.0 in AppEngine.

In this case the best solution is to create a class that models the join table.

So an EventUserResponse class would be created. It would have a Many-To-One to Event and User, and an attribute for the additional data. Event and User would have a One-To-Many to the EventUserResponse. Unfortunately I didn't managed how to map a composite primary key for this class. And DataNucleus Enhancer refused to enhance an entity class without primary key. So I've used a simple auto-generated ID.

The result should be like
ER diagram

Here are the sources:

EventUserAssociation class

@Entity 
@Table(name = "event_user_response")
@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter
@EqualsAndHashCode(callSuper = true, exclude = {"attendee", "event"})
public class EventUserAssociation extends AbstractEntity {

    @Unowned
    @ManyToOne
    @PrimaryKeyJoinColumn(name = "eventId", referencedColumnName = "_id")
    private Event event;

    @Unowned
    @ManyToOne
    @PrimaryKeyJoinColumn(name = "attendeeId", referencedColumnName = "_id")
    private User attendee;

    @Enumerated
    private Response response;

}

If Lombok annotations (@NoArgsConstructor for example) seem unfamiliar to you, you may want to take a look at the ProjectLombok. It does a great job to save us from a boilerplate code.

Event class

@Entity
@Builder
@EqualsAndHashCode(callSuper = false)
public @Data class Event extends AbstractEntity {

    /* attributes are omitted */

    // all addressees are initially present in this map with response set to UNDEFINED
    // if user has received and read notification, than the response is updated to YES, NO, or MAYBE
    @Singular
    @Setter(AccessLevel.PRIVATE)
    @OneToMany(mappedBy="event", cascade = CascadeType.ALL)
    private List<EventUserAssociation> addressees = new ArrayList<>();

    /**
     * Add an addressee to the event.
     * Create an association object for the relationship and set its data.
     *
     * @param addressee a user to whom this event notification is addressed
     * @param response  his response.
     */
    public boolean addAddressee(User addressee, Response response) {
        EventUserAssociation association = new EventUserAssociation(this, addressee, response);
            // Add the association object to this event
        return this.addressees.add(association) &&
                // Also add the association object to the addressee.
                addressee.getEvents().add(association);
    }

    public List<User> getAddressees() {
        List<User> result = new ArrayList<>();
        for (EventUserAssociation association : addressees)
            result.add(association.getAttendee());
        return result;
    }

}

User class

@Entity
@NoArgsConstructor
@RequiredArgsConstructor
@Getter @Setter
public class User extends AbstractEntity {

    /* non-significant attributes are omitted */

    @Setter(AccessLevel.PRIVATE)
    @Unowned
    @OneToMany(mappedBy="attendee", cascade = CascadeType.ALL)
    private List<EventUserAssociation> events = new ArrayList<>();

    public static User find(String attribute, EntityManager em) {
        /* implementation omitted */
    }

}

AbstractEntity class

@MappedSuperclass
@NoArgsConstructor
@EqualsAndHashCode
public abstract class AbstractEntity {

    @Id 
    @Column(name = "_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Getter
    protected Key id;

}

EMFService class

public abstract class EMFService {
    @Getter
    private static final EntityManagerFactory emfInstance = Persistence.createEntityManagerFactory("transactions-optional");
}

Example of usage:

EntityManager em = EMFService.getFactory().createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();

try {
    User fromContact = User.find(fromId, em);

    Event event = Event.builder()
            /* attributes initialization */
            .build();
    em.persist(event);

    User toUser = User.find(toId, em);
    event.addAddressee(toUser, Response.UNDEFINED);

    tx.commit();
} finally {
    if (tx.isActive()) tx.rollback();
    em.close();
}

Cross-group transactions should be allowed for this to work (what if they aren't?). Add the following property to persistence.xml:

<property name="datanucleus.appengine.datastoreEnableXGTransactions" value="true" />

At last, regarding the code in the question, it is not allowed to have a primary key with the name key in AppEngine.



Please DO NOT REPLY directly to this email but go to StackOverflow:
http://stackoverflow.com/questions/29608946/jpa-many-to-many-relationship-with-an-extra-enum-column/29608947#29608947
Reply all
Reply to author
Forward
0 new messages