2016-08-04 41 views
0

我从Hibernate迁移到EclipseLink是因为我们需要EclipseLink处理良好的组合主键,而Hibernate并不(不是!)。现在我正在修复我们的JUnit测试,我遇到了大量OneToMany关系未加载的问题。EclipseLink无法使用嵌套的Lazy OneToMany关系填充Lazy OneToOne

我有以下类:

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

该数据库设置的主要要求是我们支持高度并发地编辑数据库内容。由于db通过将提交进行原子化来修复并发问题,因此我认为我在比赛中是安全的。

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

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

编辑:

这个问题似乎涉及到我的单元测试,我在这里做的设置,在内存中的H2数据库似乎惹的EclipseLink,但下面的注释正常工作与生产系统(MsSQL上的eclipselink):

项目。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... 

我接受克里斯的答案,因为它帮助我了解,发生的问题以及如何缓存的作品。对于PropertySet,我不得不关闭缓存。解决该问题的选项列表也非常有帮助。

+0

你如何访问组的成员,如果设定为空? set.get(someKey)?这听起来很奇怪。 – Ben

+0

不,我现在认为我错了 –

回答

1

您提到的问题是Project-> PropertySet关系,它是严格的OneToOne映射,并且显示的实体不显示涉及该问题的OneToMany。由于它不是双向的,它与传统的没有设置背面指针无关,但它有点相关

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

有几种解决方法,最好取决于应用程序的使用情况。

  1. 禁用共享缓存。 这可以为每个实体或特定实体完成。有关详细信息,请参阅 eclipseLink faq。这是最简单的选项, 会给你类似于Hibernate的结果,默认情况下它不会启用 二级高速缓存,但不建议这样做,除非 是不使用二级高速缓存的其他考虑因素,因为它 对性能造成代价。

  2. 将项目中的基本ID映射字段更改为使用 insertable = false,updatable = false。然后,您从联接列中删除 insertable = false,updatable = false,允许OneToOne映射控制您的主键。在功能上,这个 不应该以任何方式改变你的应用程序。如果在基本映射中获得相同的 问题,则可以使用本地EclipseLink postClone 方法来设置引用的映射中的字段,或者您的实体get方法可以快速检查是否存在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; 
    
+0

感谢您的广泛解释。我会看到这些选项有什么作用并发回。我认为有些选项不起作用,因为我在项目中有多个类似的OneToOne映射,我会在我的发布代码中添加一个以用于说明。 – Ben

+0

它们应该都适用于与此OneToOne映射设置相同的任何OneToOne映射。至于你的双向关系 - 是的,如果你想避免数据库之旅为你更新它们,你必须绝对设置它们的双方。禁用该实体的共享缓存与读取新的或清除的EntityManager时强制刷新几乎相同,因此不要轻易使用此选项。 – Chris

+0

我更想到选项二,但不确定是否多个连接控制主键会导致多重映射问题。但那只是我的想法,我会检查。 – Ben

1

我得到的问题有没有加载

吨一对多关系

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

在这个例子的情况下,PropertySet当你加载Project因为下面一行的孩子就不会被加载:

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

在这里,您说的是持久性提供商,它应该加载相关PropertySet懒洋洋地。所以你正在得到预期的行为。如果您希望在加载Project实例时加载相关实体,则必须从@OneToOne注释中删除fetch属性。

我可以通过使用em.refresh就可以解决其他取...

我不明白你为什么用find()呼叫首先加载一个实体,然后在下面一行用refresh()refresh()的用途是,如果您的实体长时间处于持久化上下文中,并且您希望数据库中可能发生新的更改,而您的示例代码中则不会出现这种情况。

+0

但是,取出类型是不是意味着它们是懒惰加载的,因为它们只在我访问时才加载?我不确定稍后如何加载提取类型的懒惰以及如何触发它。 – Ben

+0

我还没有理解问题的第一部分。正如你写的,懒惰的领域将被加载时,你访问它们。 '如何触发这个:'如果您使用的是事务范围的实体管理器,则通过在同一个事务中访问该字段,例如'project.getPropertySet()。someProperty';否则项目实体实例将被分离,并且据我所知,未分离实体上的懒惰字段未被定义。 – ujulu

+0

我不确定是否了解加载惰性获取类型注释属性的方式以及如何触发加载。 – Ben