2016-08-04 81 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