EclipseLink未使用嵌套的Lazy OneToMany关系填充Lazy OneToOne [英] EclipseLink not populating Lazy OneToOne with nested Lazy OneToMany Relation

查看:128
本文介绍了EclipseLink未使用嵌套的Lazy OneToMany关系填充Lazy OneToOne的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我从Hibernate迁移到EclipseLink,因为我们需要EclipseLink能够很好处理的复合主键,而Hibernate则不能(真的不行!).现在,我正在修复我们的JUnit测试,遇到了很多吨的ToToMany关系未加载的问题.

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.

我有以下课程:

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 复合主键IDClass.

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);
    }
}

但是在这种情况下,属性集保持为空.它甚至没有初始化.当我初始化它时,它保持为空.我可以通过在其上使用em.refresh来修复其他访存,但是我已经添加了TODO,因为刷新始终会导致数据库命中.属性实体位于数据库中,我可以通过对数据库进行单独的特定SELECT查询来找到它们.

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.

我看到的一个问题是,在添加具有双向关系的实体时,我没有将它们同时添加到双方,但是在以后再次加载它们时是否应该再次解决此问题(可能不是因为它们被缓存了)?同样,它也不能解决我与直接的OneToMany关系所遇到的任何其他问题(与此处嵌套OneToMany的OneToOne相反),我仍然需要em.refresh(...).如果在服务器环境中,em是否以无竞争的方式维护实体?

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.

问题似乎与我在此处进行的单元测试的设置有关,内存中的H2数据库似乎与eclipselink混为一谈,但是以下注释在生产型系统(MsSQL上的eclipselink)上可以正常工作:

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...

我接受Chris的回答,因为它帮助我了解了发生的问题以及缓存的工作方式.对于PropertySet,我必须关闭缓存.列出解决此问题的选项也非常有帮助.

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.

推荐答案

您提到的问题与Project-> PropertySet关系有关,该关系是严格的OneToOne映射,显示的实体不显示与问题.由于它不是双向的,因此与传统的不设置后退指针无关,但是有些相关

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

问题是因为此OneToOne映射的外键也是Projects ID字段,它们被映射为可写的基本映射.为了避开多个可写映射异常,您已将Project.propertySet映射的连接列标记为insertable = false,updatable = false,从本质上告诉EclipseLink此映射是只读的.因此,当您设置或更改关系时,此更改"将被忽略并且不会合并到缓存中.这将导致您创建的实体在从缓存中读取时,对于此引用始终为null,除非从数据库中刷新/重新加载了该引用.这只会影响二级缓存,因此除非清除它,否则不会在创建它的EntityManager中显示.

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. 禁用共享缓存. 可以对每个实体或特定实体执行此操作.看 有关详细信息,请参见eclipseLink 常见问题解答.这是最简单的选择, 将为您提供类似于Hibernate的结果,该结果不会启用 二级缓存默认情况下,但除非存在,否则不建议这样做 不使用二级缓存的其他注意事项,因为它 会牺牲性能.

  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.

更改要使用的Project中的基本ID映射字段 insertable = false,可更新= false.然后,您删除 来自连接列的insertable = false,可更新= false,允许 OneToOne映射来控制您的主键.从功能上来说 不应以任何方式更改您的应用程序.如果你得到相同的 基本映射问题,本机EclipseLink postClone 方法可用于设置引用映射中的字段,或者 您的实体的get方法可以快速检查是否有PropertySet 并在返回空值之前使用该值.

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.

使用JPA 2.0的派生ID. JPA允许将关系标记为ID,从而无需具有相同值的这两个基本映射.或者,您可以在关系上使用@MapsId来告诉JPA,该关系控制值,并且JPA将为您设置这些字段.使用@MapsId将需要使用您的pk类作为嵌入式ID,并且看起来像:

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;

这篇关于EclipseLink未使用嵌套的Lazy OneToMany关系填充Lazy OneToOne的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆