【问题标题】:EclipseLink not populating Lazy OneToOne with nested Lazy OneToMany RelationEclipseLink 没有使用嵌套的 Lazy OneToMany 关系填充 Lazy OneToOne
【发布时间】:2016-12-10 04:18:44
【问题描述】:

我从 Hibernate 迁移到 EclipseLink 因为我们需要复合主键,EclipseLink 可以很好地处理而 Hibernate 没有(真的没有!)。现在我正在修复我们的 JUnit 测试,我遇到了大量未加载的 OneToMany 关系的问题。

我有以下课程:

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。

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

}

我们在每个单元测试之前插入我们的条目,如下所示:

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

如果我以后尝试获得该项目,我会这样做:

@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 查询来找到它。

此数据库设置的主要要求是我们支持数据库内容的高并发编辑。由于数据库通过原子化提交来解决并发问题,我认为我在这里可以避免竞争。

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

如果您需要更多信息,请告诉我。

编辑:

这个问题似乎与我在这里进行的单元测试设置有关,内存中的 H2 数据库似乎与 eclipselink 混淆,但是以下注释在生产系统(MsSQL 上的 eclipselink)上运行良好:

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,我不得不关闭缓存。解决问题的选项列表也很有帮助。

【问题讨论】:

  • 如果集合为空,你如何访问集合的成员? set.get(someKey)?这对我来说听起来很奇怪。
  • 不,我现在觉得我错了

标签: java jpa mapping eclipselink entitymanager


【解决方案1】:

您提到的问题是 Project->PropertySet 关系,这是一个严格的 OneToOne 映射,并且显示的实体没有显示 OneToMany 涉及问题。由于不是双向的,所以和传统的不设置后向指针没什么关系,但是有点相关

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

有几种方法可以解决这个问题,最好的方法取决于您的应用程序的使用情况。

  1. 禁用共享缓存。 这可以为每个实体或特定实体完成。看 eclipseLink faq 了解详情。这是最简单的选择,并且 会给你类似于 Hibernate 的结果,它不会启用 默认情况下二级缓存,但不推荐这样做,除非有 是不使用二级缓存的其他考虑因素,因为它 以性能为代价。

  2. 将 Project 中的基本 ID 映射字段更改为使用 可插入=假,可更新=假。然后你删除 insertable=false, updatable=false 来自连接列,允许 OneToOne 映射来控制你的主键。功能上这个 不应以任何方式更改您的应用程序。如果你得到同样的 基本映射问题,原生 EclipseLink postClone 方法可用于设置引用映射中的字段,或 您的实体获取方法可以快速检查是否存在 PropertySet 并在返回 null 之前使用该值。

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

    @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;
    

【讨论】:

  • 感谢您的详尽解释。我会看看这些选项有什么作用并回发。我认为某些选项不起作用,因为我在项目中有多个类似的 OneToOne 映射,我将在我发布的代码中再添加一个以进行说明。
  • 它们都应该适用于设置与此 OneToOne 映射相同的任何 OneToOne 映射。至于您的双向关系 - 是的,如果您想避免数据库之旅为您更新它们,您必须绝对设置这两个方面。禁用实体的共享缓存与读取新的或清除的 EntityManager 时强制刷新几乎相同,所以不要轻易使用此选项。
  • 我更多地考虑选项二,但不确定控制主键的多个连接是否会导致多重映射问题。但这只是我的想法,我会检查的。
  • 可能我不是很清楚,但我的意思是我在一个类中有多个 OneToOne 映射(在这个例子中是类 Project),这可能会导致主键的多重映射问题。
  • 如果您有多个映射,关闭共享缓存可能是唯一的选择。尝试最新的 EclipseLink 版本,因为他们可能已经更改了在映射上设置只读的方式,或者尝试使用描述符定制器更改映射,但我不确定它会起作用,而且我找不到发布在上面的示例.
【解决方案2】:

我遇到大量 OneToMany 关系未加载的问题

它们可能无法加载的原因是因为默认行为是这些关系是延迟加载的,这意味着在加载父实体时不会加载相关实体。

在本例中,当您加载 Project 时,不会加载 PropertySet 及其子代,原因如下:

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

这里你告诉持久化提供者它应该延迟加载相关的PropertySet。所以你得到了预期的行为。如果您希望在加载 Project 的实例时加载相关实体,则必须从 @OneToOne 注释中删除 fetch 属性。

我可以通过在其上使用 em.refresh 来修复其他提取...

我不明白为什么您首先使用find() 调用加载实体,然后在下一行中使用refresh()refresh() 的目的是,如果您的实体长时间处于持久化上下文中,并且您预计数据库中可能会有新的更改,而您的示例代码中并非如此。

【讨论】:

  • 但是 fetch 类型不意味着它们是延迟加载的,因为它们只有在我访问它们时才会加载?我不确定稍后将如何加载惰性获取类型以及如何触发它。
  • 我没有理解问题的第一部分。正如您所写,当您访问惰性字段时,它们将被加载。 how to trigger this: 通过在同一事务中以 project.getPropertySet().someProperty 的形式访问字段,如果您使用事务范围的实体管理器;否则项目实体实例将被分离,据我所知,访问分离实体上的惰性字段未定义。
  • 我不确定我是否理解惰性获取类型注释属性的加载方式以及如何触发加载。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多