EclipseLink not populating Lazy OneToOne with nested Lazy OneToMany Relation

耗尽温柔 提交于 2019-12-25 06:25:36

问题


I migrated from Hibernate to EclipseLink because we needed composite primary keys which EclipseLink handles well and Hibernate doesn`t (really does not!). Now I am fixing our JUnit tests, I get issues with tons of OneToMany relations not loaded.

I have the following classes:

DatabaseSession.java

package platform.data;

import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.Metamodel;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import platform.accesscontrol.UserContext;
import pm.data.IndicatorSet;

/**
 * Provides easy to use database sessions and transactions.
 * <p>
 * The session and transaction is automatically opened in the constructor.
 * <p>
 * The session must be closed using close(), which should be done with a try(...) { ...} block. If data is modified,
 * the transaction must be committed explicitly using commit(), usually as the last statement in the
 * try(...) { ...} block. Uncommitted transactions are automatically rolled back when the session is closed.
 */
public final class DatabaseSession implements AutoCloseable {

    /**
     * Maximum latency in milliseconds for a JPA operation, after which a warning shall be logged.
     */
    private static final double MAX_LATENCY = 100.0;

    /**
     * Maximum duration in milliseconds for a session, after which a warning shall be logged.
     */
    private static final double MAX_LATENCY_TOT = 1000.0;

    /**
     * Our logger, never null.
     */
    private static final Logger log = LoggerFactory.getLogger(DatabaseSession.class);

    /**
     * The factory for creating EntityManager instances, created in initEntityManagerFactory() or in the constructor.
     */
    private static EntityManagerFactory factory;

    /**
     * The EntityManager instance to access the database, created from the factory in the constructor.
     */
    private EntityManager em;

    /**
     * The time when the instance was created, useful for measure total time of the session.
     */
    private final long ttot = System.nanoTime();

    /**
     * Indicates whether commit() as been called.
     */
    private boolean committed;

    /**
     * Initializes the EntityManagerFactory (optional, useful for testing).
     * <p>
     * If this method is not called, the EntityManagerFactory is initialized
     * automatically with persistence unit "default" when the first instance is created.
     * <p>
     * Persistence units are defined in conf/META-INF/persistence.xml.
     *
     * @param persistenceUnitName the name of the persistence unit to be used,
     *                            must match the XML attribute /persistence/persistence-unit/@name.
     */
    public static void initEntityManagerFactory(String persistenceUnitName) {
        synchronized(DatabaseSession.class) {
            factory = Persistence.createEntityManagerFactory(persistenceUnitName);
        }
    }

    public void shutdownDB(){
        em.close();
        em = null;
        DatabaseSession.factory.close();
        DatabaseSession.factory = null;
    }

    /**
     * Opens a new session and begins a new transaction.
     */
    public DatabaseSession() {
        synchronized(DatabaseSession.class) {
            if(factory == null) {
                factory = Persistence.createEntityManagerFactory("default");
            }
        }
        createEntityManager();
    }

    public void createEntityManager(){
        em = factory.createEntityManager();
        em.getTransaction().begin();
        EntityType<IndicatorSet> entity = factory.getMetamodel().entity(IndicatorSet.class);
        Set<Attribute<IndicatorSet, ?>> attrs = entity.getDeclaredAttributes();
        attrs.toString();
    }

    @Override
    public void close() {
        try {
            if (!committed) {
                if(em != null){
                    em.getTransaction().rollback();
                }
            }
        } finally {
            if (committed) {
                if(em != null){
                    em.close();
                }
            }

            double latency = (System.nanoTime() - ttot)/1000000.0;
            if(latency > MAX_LATENCY_TOT) {
                log.warn("Duration of session was " + latency + "ms.");
            } else {
                log.debug("Duration of session was " + latency + "ms.");
            }
        }
    }

    /**
     * Commits the transaction, must explicitly be done before the session is closed.
     */
    public void commit()
    {
        long t = System.nanoTime();
        em.flush();
        em.getTransaction().commit();
        committed = true;
        double latency = (System.nanoTime() - t)/1000000.0;
        if(latency > MAX_LATENCY) {
            warn("Latency of commit() was %sms.", latency);
        }
    }

    public <T extends PersistentRecord> List<T> loadAll(Class<T> clazz, String mandt) {
        return loadAll(clazz, mandt, true);
    }

    public <T extends PersistentRecord> List<T> loadAll(Class<T> clazz, String mandt, boolean filterDeleted) {
        log("loadAll(%s)", clazz.getSimpleName());
        long t = System.nanoTime();
        CriteriaBuilder b = em.getCriteriaBuilder();
        CriteriaQuery<T> q = b.createQuery(clazz);
        Metamodel m = em.getMetamodel();
        EntityType<T> et = m.entity(clazz);
        Root<T> r = q.from(clazz);
        q.select(r);
        if (mandt != null) {
            q.where(b.equal(r.get(et.getAttribute("mandt").getName()), mandt));
        }
        if (filterDeleted) {
            q.where(b.equal(r.get(et.getAttribute("deleted").getName()), 0));
        }
        List<T> result = em.createQuery(q).getResultList();
        double latency = (System.nanoTime() - t)/1000000.0;
        if(latency > MAX_LATENCY) {
            warn("Latency of loadAll(%s) was %sms.", clazz.getSimpleName(), latency);
        }
        return result;
    }

    public <T extends PersistentRecord> int count(Class<T> clazz, String mandt) {
        return count(clazz, mandt, true);
    }

    public <T extends PersistentRecord> int count(Class<T> clazz, String mandt, boolean filterDeleted) {
        log("count(%s)", clazz.getSimpleName());
        long t = System.nanoTime();
        CriteriaBuilder b = em.getCriteriaBuilder();
        CriteriaQuery<T> q = b.createQuery(clazz);
        Metamodel m = em.getMetamodel();
        EntityType<T> et = m.entity(clazz);
        Root<T> r = q.from(clazz);
        q.select(r);
        if (mandt != null) {
            q.where(b.equal(r.get(et.getAttribute("mandt").getName()), mandt));
        }
        if (filterDeleted) {
            q.where(b.equal(r.get(et.getAttribute("deleted").getName()), 0));
        }
        List<T> result = em.createQuery(q).getResultList();
        double latency = (System.nanoTime() - t)/1000000.0;
        if(latency > MAX_LATENCY) {
            warn("Latency of count(%s) was %sms.", clazz.getSimpleName(), latency);
        }
        return result.size();
    }

    public <T extends PersistentRecord> T load(Class<T> clazz, String mandt, String id) {
        return load(clazz, mandt, id, true);
    }

    public <T extends PersistentRecord> T load(Class<T> clazz, String mandt, String id, boolean filterDeleted) {
        log("load(%s, %s)", clazz.getSimpleName(), id);
        long t = System.nanoTime();
        T result = em.find(clazz, mandt != null ? new MandtId(mandt, id) : id);
        if(result != null){
            em.refresh(result); // TODO: This always results in a database hit, but relationship syncing is not meant to be done that way. Reduction of db hits can be achieved trough custom annotation or flag.
            //JPA does not maintain relationships for you, the application is required to set both sides to stay in sync (http://stackoverflow.com/questions/16762004/eclipselink-bidirectional-onetomany-relation)"
        }
        if(filterDeleted) {
            result = filterDeleted(result);
        }
        double latency = (System.nanoTime() - t)/1000000.0;
        if(latency > MAX_LATENCY) {
            warn("Latency of load(%s, %s) was %sms.", clazz.getSimpleName(), id, latency);
        }
        return result;
    }

    public <T extends PersistentRecord> List<T> loadByQuery(Class<T> clazz, String mandt, String query, Object... params) {
        log("loadByQuery(%s, '%s', %s)", clazz.getSimpleName(), query, format(params));
        long t = System.nanoTime();
        TypedQuery<T> q = em.createQuery(query, clazz);
        for(int i = 0; i < params.length; i++) {
            q.setParameter(i+1, params[i]);
        }
        List<T> result = q.getResultList();
        if (mandt != null) { // mandt can be null to allow queries without mandt
            result = filterMandt(result, mandt); // as a safety measure we ensure mandt separation in db and application layer
        }
        result = filterDeleted(result);
        double latency = (System.nanoTime() - t)/1000000.0;
        if(latency > MAX_LATENCY) {
            warn("Latency of loadByQuery(%s, '%s', %s) was %sms.", clazz.getSimpleName(), query, format(params), latency);
        }
        return result;
    }

    public <T extends PersistentRecord> T loadSingleByQuery(Class<T> clazz, String mandt, String query, Object... params) {
        log("loadSingleByQuery(%s, '%s', %s)", clazz.getSimpleName(), query, format(params));
        long t = System.nanoTime();
        TypedQuery<T> q = em.createQuery(query, clazz);
        for(int i = 0; i < params.length; i++) {
            q.setParameter(i+1, params[i]);
        }
        List<T> result = q.getResultList();
        if (mandt != null) { // mandt can be null to allow queries without mandt
            result = filterMandt(result, mandt); // as a safety measure we ensure mandt separation in db and application layer
        }
        result = filterDeleted(result);
        double latency = (System.nanoTime() - t)/1000000.0;
        if(latency > MAX_LATENCY) {
            warn("Latency of loadSingleByQuery(%s, '%s', %s) was %sms.", clazz.getSimpleName(), query, format(params), latency);
        }
        return result.size() > 0 ? result.get(0) : null;
    }

    /**
     * Stores a new or updated record (resulting in an INSERT or UPDATE statement)
     * @param record the record to be stored, must not be null.
     * @param uc the user that initiated the operation, can be null.
     * @return the given record, or another instance with the same ID if EntityManager.merge() was called.
     */
    public <T extends PersistentRecord> T store(T record, UserContext uc) {
        if(record == null) {
            return null;
        }
        log("update(%s, %s)", record.getClass().getSimpleName(), record.getId());
        if(record instanceof ReadWriteRecord) {
            ((ReadWriteRecord)record).touch(uc);
        }
        return add(record);
    }

    /**
     * Deletes a record or marks a record as deleted (resulting in an UPDATE or maybe an INSERT statement if T is a subclass of ReadWriteRecord, or resulting in a DELETE statement otherwise).
     * @param record the record to be deleted, must not be null.
     * @param uc the user that initiated the operation, can be null.
     * @return the given record, or another instance with the same ID if EntityManager.merge() was called.
     */
    public <T extends PersistentRecord> T delete(T record, UserContext uc) {
        if(record == null) {
            return null;
        }
        log("delete(%s, %s)", record.getClass().getSimpleName(), record.getId());
        if(record instanceof ReadWriteRecord) {
            ((ReadWriteRecord)record).setDeleted(true);
            ((ReadWriteRecord)record).touch(uc);
            return add(record); // same as store(), we _dont_ physically delete the record
        } else {
            em.remove(record);
            return null;
        }
    }

    /**
     * Physically deletes all records of a table, intended for JUnit tests only (unless you really want to get rid of your data).
     * @param clazz the DTO class of the table.
     */
    public <T extends PersistentRecord> void deleteAll(Class<T> clazz, String mandt) {
        log("deleteAll(%s)", clazz.getSimpleName());
        for(T rec : loadAll(clazz, mandt, false)) {
            em.remove(rec);
        }
    }

    /**
     * Forces lazy initialization of an entity.
     * @param record a record loaded from the database, can be null.
     * @return the record passed to this method.
     */
    public <T extends PersistentRecord> T fetch(T record) {
        if(record != null) {
            em.refresh(record);// TODO: This always results in a database hit, but relationship syncing is not meant to be done that way. Reduction of db hits can be achieved trough custom annotation or flag.
            //JPA does not maintain relationships for you, the application is required to set both sides to stay in sync (http://stackoverflow.com/questions/16762004/eclipselink-bidirectional-onetomany-relation)
            record.fetch();
        }
        return record;
    }

    /**
     * Forces lazy initialization of an entity.
     * @param record a record loaded from the database, can be null.
     * @param fetcher a method to be invoked on the record to lazy initialize nested fields.
     * @return the record passed to this method.
     */
    public <T extends PersistentRecord> T fetch(T record, BiConsumer<DatabaseSession, T> fetcher) {
        if(record != null) {
            em.refresh(record); // TODO: This always results in a database hit, but relationship syncing is not meant to be done that way. Reduction of db hits can be achieved trough custom annotation or flag.
            //JPA does not maintain relationships for you, the application is required to set both sides to stay in sync (http://stackoverflow.com/questions/16762004/eclipselink-bidirectional-onetomany-relation)
            record.fetch();
            fetcher.accept(this, record);
        }
        return record;
    }

    /**
     * Forces lazy initialization of multiple entities.
     * @param records a list of records loaded from the database, can be null.
     * @param fetcher a method to be invoked on the records to lazy initialize nested fields.
     * @return the list of records passed to this method.
     */
    public <T extends PersistentRecord> List<T> fetch(List<T> records, BiConsumer<DatabaseSession, T> fetcher) {
        if(records != null) {
            for(T record : records) {
                em.refresh(record); // TODO: This always results in a database hit, but relationship syncing is not meant to be done that way. Reduction of db hits can be achieved trough custom annotation or flag.
                //JPA does not maintain relationships for you, the application is required to set both sides to stay in sync (http://stackoverflow.com/questions/16762004/eclipselink-bidirectional-onetomany-relation)
                record.fetch();
                fetcher.accept(this, record);
            }
        }
        return records;
    }

    /**
     * Forces lazy initialization of a one-to-many relationship.
     * @param records a list representing a one-to-many relationship, can be null.
     * @return the relationship passed to this method.
     */
    public <T extends PersistentRecord> List<T> fetchCollection(List<T> records) {
        if(records != null) {
            records.size();
        }
        return records;
    }

    /**
     * Adds the given record to the EntityManager, called by store() and delete().
     * <p>
     * This method attempts to do something like Hibernate's saveOrUpdate(), which is not available in JPA:
     * <ul>
     * <li> For newly created records, EntityManager.persist() has to be called in order so insert the record.
     *      This case will be assumed when markNew() has been called on the record.
     * <li> For records that have been read from the database by _another_ session (so-called detached entities),
     *      EntityManager.merge() has to be called in order to update the record.
     *      This case will be assumed when markNew() has NOT been called on the record.
     * <li> For records that have been read from the database by this session, nothing has to be done because the
     *      EntityManager takes care of the entities it loaded. This case can be detected easily using contains().
     * </ul>
     * Note: EntityManager.merge() does not add the entity to the session.
     * Instead, a new entity is created and all properties are copied from the given record to the new entity.
     *
     * @param record the record to be added, can be null.
     * @return the given record, or another instance with the same ID if EntityManager.merge() was called.
     */
    private <T extends PersistentRecord> T add(T record) {
        long t = System.nanoTime();
        try {
            if (record == null || em.contains(record)) {
                return record;
            } else if(record.mustInsert) {
                em.persist(record); // will result in INSERT
                record.mustInsert = false;
                return record;
            } else {
                record = em.merge(record);
                return record;
            }
        } finally {
            double latency = (System.nanoTime() - t)/1000000.0;
            if(latency > MAX_LATENCY) {
                warn("Latency of add(%s, %s) was %sms.", record.getClass().getSimpleName(), record.getId(), latency);
            }
        }
    }

    private static <T extends PersistentRecord> List<T> filterDeleted(List<T> records) {
        if(records != null) {
            records = records.stream().
                    filter(record -> (record instanceof ReadWriteRecord) == false || ((ReadWriteRecord) record).getDeleted() == false).
                    collect(Collectors.toList());
        }
        return records;
    }

    private static <T extends PersistentRecord> List<T> filterMandt(List<T> records, String mandt) {
        if(records != null) {
            records = records.stream().
                    filter(record -> Objects.equals(record.getMandt(), mandt)).
                    collect(Collectors.toList());
        }
        return records;
    }

    private static <T extends PersistentRecord> T filterDeleted(T record) {
        if(record != null && record instanceof ReadWriteRecord) {
            if(((ReadWriteRecord) record).getDeleted()) {
                record = null;
            }
        }
        return record;
    }

    private void log(String format, Object... args) {
        if(log.isDebugEnabled()) {
            log.debug(String.format(format, args));
        }
    }

    private void warn(String format, Object... args) {
        if(log.isWarnEnabled()) {
            log.warn(String.format(format, args));
        }
    }

    private static String format(Object... args) {
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        for(Object arg: args) {
            if(sb.length() > 1)
                sb.append(", ");
            sb.append(arg);
        }
        sb.append("]");
        return sb.toString();
    }

    // For debugging
    public Query createQuery(String string) {
        return em.createQuery(string);
    }

}

Project.java

package pm.data;

...common imports...

import platform.data.DatabaseBindingIds;
import platform.data.MandtId;
import platform.data.PropertySet;
import platform.data.ReadWriteRecord;
import resource.data.Resource;

@Entity
@IdClass(MandtId.class)
public class Project extends ReadWriteRecord {

    @Id
    @Column(name=DatabaseBindingIds.PROJECT_TENANT)
    private String mandt;

    @Id
    @Column(name=DatabaseBindingIds.PROJECT_ID)
    private String entityId;

    @OneToOne(fetch=FetchType.LAZY, cascade= CascadeType.ALL) // one to one mappings are directly mapped using the project primary keys
    @JoinColumns( {
        @JoinColumn(name=DatabaseBindingIds.PROJECT_TENANT, referencedColumnName=DatabaseBindingIds.PROPERTYSET_TENANT, insertable=false, updatable=false),
        @JoinColumn(name=DatabaseBindingIds.PROJECT_ID, referencedColumnName=DatabaseBindingIds.PROPERTYSET_ID, insertable=false, updatable=false)
    } )
    private PropertySet propertySet;

    @OneToOne(fetch=FetchType.LAZY, cascade = CascadeType.ALL) // one to one mappings are directly mapped using the project report primary keys
    @JoinColumns( {
        @JoinColumn(name=DatabaseBindingIds.PROJECTREPORT_TENANT, referencedColumnName=DatabaseBindingIds.INDICATORSET_TENANT, insertable=false, updatable=false),
        @JoinColumn(name=DatabaseBindingIds.PROJECTREPORT_ID, referencedColumnName=DatabaseBindingIds.INDICATORSET_ID, insertable=false, updatable=false)
    } )
    private IndicatorSet indicatorSet; // SAMPLE NOTE: The indicator set is essentially the same thing as the property set. 


    ...other member variables...

    @Override
    public String getMandt() {
        return mandt;
    }

    @Override
    public String getId() {
        return entityId;
    }

    @Override
    public void setId(MandtId x) {
        markNew();
        mandt = x != null ? x.getMandt() : null;
        entityId = x != null ? x.getId() : null;
        propertySet = new PropertySet();
        propertySet.setId(x);
    }

    public PropertySet getPropertySet() {
        return propertySet;
    }


    ...getters and setters for other member variables...
}

PropertySet.java

package platform.data;

import java.util.ArrayList;
import java.util.List;

...common imports...

@Entity
@IdClass(MandtId.class)
public class PropertySet extends ReadWriteRecord {

    @Id
    @Column(name=DatabaseBindingIds.PROPERTYSET_TENANT)
    private String mandt;

    @Id
    @Column(name=DatabaseBindingIds.PROPERTYSET_ID)
    private String entityId;

    @OneToMany(mappedBy="propertySet", fetch=FetchType.EAGER)
    @OrderBy("sortIndex")
    private List<Property> properties;

    @Override
    public String getMandt() {
        return mandt;
    }

    @Override
    public String getId() {
        return entityId;
    }

    @Override
    public void setId(MandtId x) {
        markNew();
        mandt = x != null ? x.getMandt() : null;
        entityId = x != null ? x.getId() : null;
    }

    public List<Property> getProperties() {
        if(properties == null) {
            properties = new ArrayList<>();
        }
        return properties;
    }
}

Property.java

package platform.data;

...common imports...

@Entity
@IdClass(MandtId.class)
public class Property extends ReadWriteRecord {

    @Id
    @Column(name=DatabaseBindingIds.PROPERTY_TENANT)
    private String mandt;

    @Id
    @Column(name=DatabaseBindingIds.PROPERTY_ID)
    private String entityId;

    @ManyToOne(fetch=FetchType.EAGER, optional=false)
    @JoinColumns( {
        @JoinColumn(name=DatabaseBindingIds.PROPERTY_TENANT, referencedColumnName=DatabaseBindingIds.PROPERTYSET_TENANT, insertable=false, updatable=false),
        @JoinColumn(name=DatabaseBindingIds.PROPERTY_PROPERTYSET_ID, referencedColumnName=DatabaseBindingIds.PROPERTYSET_ID, insertable=true, updatable=true)
    } )
    private PropertySet propertySet;

    @Column
    private Integer sortIndex;

    @Column
    private String key;

    @Column
    @Convert(converter = IntlStringConverter.class)
    private IntlString label;

    @Column
    private String type;

    @Column
    private String value;

    @Override
    public String getMandt() {
        return mandt;
    }

    @Override
    public String getId() {
        return entityId;
    }

    @Override
    public void setId(MandtId x) {
        markNew();
        mandt = x != null ? x.getMandt() : null;
        entityId = x != null ? x.getId() : null;
    }

    public void setPropertySet(PropertySet x) {
        propertySet = x;
    }

    public PropertySet getPropertySet() {
        return propertySet;
    }

    public int getSortIndex() {
        return sortIndex == null ? 0 : sortIndex;
    }

    public void setSortIndex(int x) {
        sortIndex = x;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String x) {
        key = x;
    }

    public IntlString getLabel() {
        return label;
    }

    public void setLabel(IntlString x) {
        label = x;
    }

    public String getType() {
        return type;
    }

    public void setType(String x) {
        type = x;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String x) {
        value = x;
    }
}

MandtId.java The composite primary key IDClass.

package platform.data;

import java.io.Serializable;
import java.util.Objects;

/**
 * @author sm
 * Class to map MANDT and *ID field as composite key
 */
@SuppressWarnings("serial")
public class MandtId implements Serializable {

    private String mandt;
    private String entityId;

    ...setters and getters...

    @Override
    public int hashCode()
    ...

    @Override
    public boolean equals(Object other)
    ...

    @Override
    public String toString()
    ...

}

We insert our entries before each unit test like this:

try(DatabaseSession db = new DatabaseSession()) {


    Project prjT = createProject(db, UUID_PROJECT_NEW, "<New Project>");
    createProperty(db, prjT.getPropertySet(), "prj-prop1", "Property 1", "string", "<New Value 1>", 2);
    createProperty(db, prjT.getPropertySet(), "prj-prop2", "Property 2", "string", "<New Value 2>", 1);

    db.commit();
}

public static Project createProject(DatabaseSession db, String id, String name) {
    Project prj = new Project();
    prj.setId(new MandtId(MANDT, id));
    prj.setName(name);
    prj.setStatus(UUID_PROJECT_STATUS_ACTIVE);
    db.store(prj.getPropertySet(), null); // workaround: persist child first (otherwise PropertySet will stay marked as new)
    db.store(prj, null);
    return prj;
}

    public static Property createProperty(DatabaseSession db, PropertySet ps, String key, String label, String type, String value, int sortIndex) {
    Property rec = new Property();
    rec.setId(new MandtId(MANDT, UuidString.generateNew()));
    rec.setPropertySet(ps);
    rec.setKey(key);
    rec.setLabel(IntlStrings.wrap(label));
    rec.setType(type);
    rec.setValue(value);
    rec.setSortIndex(sortIndex);
    ps.getProperties().add(rec);
    db.store(rec.getPropertySet(), null);
    db.store(rec, null);
    // rec.properties.add(p);
    return rec;
}

If I later try to get the project, I do:

@Override
public Project loadProject(String projectId) throws DataAccessException {
    try(DatabaseSession session = new DatabaseSession()) {
        return session.fetch(session.load(Project.class, mandt, projectId), (s, r) -> {
            s.fetch(r.getPropertySet());
            s.fetch(r.getOwner());
            s.fetch(r.getResponsibility());
            s.fetch(r.getProjectGuideline());
        });
    } catch(RuntimeException e) {
        throw new DataAccessException(e);
    }
}

But the propertyset stays null in this case. It is not even initialized. And when I initialize it, it stays empty. I could fix other fetches by using em.refresh on it, but I already added a TODO, because the refresh always results in a db hit. The property entities are in the database which I could find by separate specific SELECT queries to it.

The main requirement of this database setting is that we support highly concurrent editing of the database content. Since the db fixes the concurrency problems by atomizing the commits, I think I am safe here from races.

One issue I see is that on adding entities with bidirectional relationships, I do not add them to both sides, but shouldn´t this be fixed again when I load them again later (probably not because they are cached)? Also it does not fix any of the other issues I had with direct OneToMany relationships (in contrast to the OneToOne with nested OneToMany here), I still need the em.refresh(...). Does the em maintain the entities in a racefree manner if it is in a server environment?

Tell me if you need more information.

Edit:

The problem seems to be related to my setup of the unit tests I am doing here, the in-memory H2 database seems to mess with eclipselink, however the following annotations work fine with the productive system (eclipselink on MsSQL):

Project.java

package pm.data;

...common imports...

import platform.data.DatabaseBindingIds;
import platform.data.MandtId;
import platform.data.PropertySet;
import platform.data.ReadWriteRecord;
import resource.data.Resource;

@Entity
@IdClass(MandtId.class)
public class Project extends ReadWriteRecord {

    @Id
    @Column(name=DatabaseBindingIds.PROJECT_TENANT)
    private String mandt;

    @Id
    @Column(name=DatabaseBindingIds.PROJECT_ID)
    private String entityId;

    @OneToOne(fetch=FetchType.LAZY, cascade= CascadeType.ALL) // one to one mappings are directly mapped using the project primary keys
    @JoinColumns( {
        @JoinColumn(name=DatabaseBindingIds.PROJECT_TENANT, referencedColumnName=DatabaseBindingIds.PROPERTYSET_TENANT, insertable=false, updatable=true),
        @JoinColumn(name=DatabaseBindingIds.PROJECT_ID, referencedColumnName=DatabaseBindingIds.PROPERTYSET_ID, insertable=false, updatable=true)
    } )
    private PropertySet propertySet;

    @OneToOne(fetch=FetchType.LAZY, cascade = CascadeType.ALL) // one to one mappings are directly mapped using the project report primary keys
    @JoinColumns( {
        @JoinColumn(name=DatabaseBindingIds.PROJECTREPORT_TENANT, referencedColumnName=DatabaseBindingIds.INDICATORSET_TENANT, insertable=false, updatable=false),
        @JoinColumn(name=DatabaseBindingIds.PROJECTREPORT_ID, referencedColumnName=DatabaseBindingIds.INDICATORSET_ID, insertable=false, updatable=false)
    } )
    private IndicatorSet indicatorSet; // NOTE: Yes, the updatable are false here and are only true in one set.


    ...other member variables...

    ...same as above...


    ...getters and setters for other member variables...
}

PropertySet.java

package platform.data;

import java.util.ArrayList;
import java.util.List;

...common imports...

@Entity
@IdClass(MandtId.class)
@Cache(isolation=CacheIsolationType.ISOLATED) // Fix turns off EclipseLink cache for PropertySet
public class PropertySet extends ReadWriteRecord {

    ...same as above...

I accepted Chris answer because it helped me to understand the problem that occurs and how the cache works. For PropertySet I had to turn off the cache. The listing of options to fix the issue was also very helpful.


回答1:


The problem you mention is with the Project->PropertySet relationship, which is a strict OneToOne mapping, and the entities shown do not show a OneToMany being involved in the problem. Since it isn't bidirectional, it has nothing to do with the traditional not setting the back pointer, but it is somewhat related

The issue is because this OneToOne mapping's foreign key is also the Projects ID fields, which are mapped as writable basic mappings. To get around the multiple writable mapping exception, you've marked the Project.propertySet mapping's join columns as insertable=false, updatable=false, essentially telling EclipseLink this mapping is read-only. So when you set or change the relationship, this 'change' is ignored and not merged into the cache. This causes the entity you created to always have a null for this reference when it is read from the cache, unless it is refreshed/reloaded from the database. This only affects the second level cache, and so will not show up in the EntityManager it was created in unless it is cleared.

There are a few ways around this, and what is best depends on your application's usage.

  1. Disable the shared cache. This can be done for every entity, or for specific entities. See the eclipseLink faq for details. This is the easiest option and will give you results similar to Hibernate which doesn't enable a second level cache by default, but don't recommend this unless there are other considerations for not using a second level cache, as it comes at a cost to performance.

  2. Change the basic ID mapping fields in Project to use insertable=false, updatable=false. You then remove the insertable=false, updatable=false from the join-columns, allowing the OneToOne mapping to control your primary key. Functionally this should not change your application in any way. If you get the same issue with the basic mappings, an native EclipseLink postClone method can be used to set the fields from the referenced mapping, or your entity get methods can quickly check if there is a PropertySet and use that value before returning null.

  3. Use JPA 2.0's derived IDs. JPA allows marking relationships as the ID, removing the need to have those two basic mappings for the same value. Or you can use the @MapsId on the relationship to tell JPA the relationship controls the value, and JPA will set those fields for you. Using @MapsId would require using your pk class as an embedded ID, and would look like:

    @Entity
    public class Project extends ReadWriteRecord {
    
        @EmbeddedId
        private MandtId mandtId;
    
        @MapsId("mandtId")
        @OneToOne(fetch=FetchType.LAZY, cascade= CascadeType.ALL) // one to one mappings are directly mapped using the project primary keys
        @JoinColumns( {
            @JoinColumn(name=DatabaseBindingIds.PROJECT_TENANT, referencedColumnName=DatabaseBindingIds.PROPERTYSET_TENANT, insertable=false, updatable=false),
            @JoinColumn(name=DatabaseBindingIds.PROJECT_ID, referencedColumnName=DatabaseBindingIds.PROPERTYSET_ID, insertable=false, updatable=false)
        } )
        private PropertySet propertySet;
    



回答2:


I get issues with tons of OneToMany relations not loaded

The reason why they might not be loaded is because the default behavior is that these relationships are loaded lazily which means the related entities will not be loaded when the parent entity is loaded.

In the case of this example, the PropertySet and its children will not be loaded when you load a Project because of the following line:

@OneToOne(fetch=FetchType.LAZY, cascade= CascadeType.ALL)

Here you are telling the persistence provider that it should load the related PropertySet lazily. So you are getting the expected behavior. If you want that the related entities are loaded when an instance of Project is loaded, you have to remove the fetch attribute fron the @OneToOne annotation.

I could fix other fetches by using em.refresh on it ...

I don't understand why you load an entity first with the find() call and then use refresh() in the following line. The purpose of refresh() is if your entity is in the persistence context for a long time and you expect that there might be new changes in the database which is not the case in your example code.



来源:https://stackoverflow.com/questions/38760611/eclipselink-not-populating-lazy-onetoone-with-nested-lazy-onetomany-relation

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!