我们在使用eclipselink的应用程序中遇到了一个奇怪的问题。我们构建了一个多租户应用程序,它将几个租户保留在一个表中,由租用行分隔。租户和单独的实体形成每个条目的复合键。我们配置了共享缓存,一切都运行良好。但是,在使用应用程序之前,我们迟早会面临急切加载相关实体并不总是正常工作的情况。问题显示在以下示例中:
应用程序通过另一个实体的id加载宏分配:
@Entity
@Multitenant(value=MultitenantType.SINGLE_TABLE)
@TenantDiscriminatorColumn(name = DatabaseBindingIds.MACROALLOCATION_TENANT, contextProperty = MULTITENANT_PROPERTY_DEFAULT, primaryKey = true)
@Cache(type=CacheType.SOFT,size=100000)
public class MacroAllocation extends ReadWriteRecord {
/**
* The entity id of this macroallocation.
*/
@Id
@Column(name=DatabaseBindingIds.MACROALLOCATION_ID)
private String entityId;
/**
* The phase this macroallocation belongs to.
*/
@ManyToOne(fetch=FetchType.EAGER, optional=false)
//@BatchFetch(BatchFetchType.JOIN)
@JoinColumn(name=DatabaseBindingIds.MACROALLOCATION_PHASE_ID,referencedColumnName=DatabaseBindingIds.PHASE_ID, insertable=true, updatable=true)
private Phase phase;
/**
* The resource this macroallocation is assigned to.
*/
@ManyToOne(fetch=FetchType.EAGER, optional=false)
//@BatchFetch(BatchFetchType.JOIN)
@JoinColumn(name=DatabaseBindingIds.MACROALLOCATION_RESOURCE_ID, referencedColumnName=DatabaseBindingIds.RESOURCE_ID, insertable=true, updatable=true)
private Resource resource;
/**
* The duration of the allocation.
*/
@Column
@Convert(converter = DurationConverter.class)
private Duration duration;
/**
* Get the macroallocation id.
* @exception IllegalStateException EntityId can never be null.
*/
@Override
public String getId() {
if(entityId == null){
throw new IllegalStateException("[Constraint violation] entityId can not be null in " + this.getClass().getSimpleName());
}
return entityId;
}
/**
* Set the full id of this macroallocation.
* @exception IllegalStateException EntityId can never be null.
*/
@Override
public void setId(String entityId) {
markNew();
this.entityId = entityId;
if(entityId == null){
throw new IllegalStateException("[Constraint violation] entityId can not be null in " + this.getClass().getSimpleName());
}
}
/**
* Get the phase to which the macroallocation belongs.
* @exception IllegalStateException Phase can never be null.
*/
public Phase getPhase() {
if(phase == null){
throw new IllegalStateException("[Constraint violation] phase can not be null in " + this.getClass().getSimpleName());
}
return phase;
}
/**
* Set the phase to which the macroallocation belongs.
* @exception IllegalStateException Phase can never be null.
*/
public void setPhase(Phase x) {
phase = x;
if(phase == null){
throw new IllegalStateException("[Constraint violation] phase can not be null in " + this.getClass().getSimpleName());
}
}
/**
* Get the resource this macroallocation is assigned to.
* @exception IllegalStateException Resource can never be null.
*/
public Resource getResource() {
if(resource == null){
throw new IllegalStateException("[Constraint violation] resource can not be null in " + this.getClass().getSimpleName());
}
return resource;
}
/**
* Set the resource this macroallocation is assigned to.
* @exception IllegalStateException Resource can never be null.
*/
public void setResource(Resource x) {
resource = x;
if(resource == null){
throw new IllegalStateException("[Constraint violation] resource can not be null in " + this.getClass().getSimpleName());
}
}
/**
* Get the duration of this macroallocation.
* @return duration - can be null.
*/
public Duration getDuration() {
return duration;
}
/**
* Set the duration of this macroallocation.
* @param duration - can be null.
*/
public void setDuration(Duration x) {
duration = x;
}
}
要根据数据库层实体填充应用层对象,我们通常会通过以下方式从相关实体获取更多信息:
macroAllocation.getPhase().getScenario().getProject().getId()
阶段应该热切地加载宏分配,并且应该热切地加载场景,如下面阶段的定义所示:
@Entity
@Multitenant(value=MultitenantType.SINGLE_TABLE)
@TenantDiscriminatorColumn(name = DatabaseBindingIds.PHASE_TENANT, contextProperty = MULTITENANT_PROPERTY_DEFAULT, primaryKey = true)
@Cache(type=CacheType.SOFT,size=10000)
public class Phase extends ReadWriteRecord {
/**
* The entity id of this phase.
*/
@Id
@Column(name=DatabaseBindingIds.PHASE_ID)
private String entityId;
@OneToOne(fetch=FetchType.LAZY, cascade= CascadeType.PERSIST) // one to one mappings are directly mapped using the phase primary keys
@BatchFetch(BatchFetchType.JOIN)
@JoinColumn(name=DatabaseBindingIds.PHASE_ID, referencedColumnName=DatabaseBindingIds.PROPERTYSET_ID, insertable=false, updatable=false)
private PropertySet propertySet;
@OneToOne(fetch=FetchType.LAZY, cascade= CascadeType.PERSIST) // one to one mappings are directly mapped using the phase primary keys
@BatchFetch(BatchFetchType.JOIN)
@JoinColumn(name=DatabaseBindingIds.PHASE_ID, referencedColumnName=DatabaseBindingIds.LOGSET_ID, insertable=false, updatable=false)
private LogSet logSet;
@OneToOne(fetch=FetchType.LAZY, cascade= CascadeType.PERSIST) // one to one mappings are directly mapped using the phase primary keys
@BatchFetch(BatchFetchType.JOIN)
@JoinColumn(name=DatabaseBindingIds.PHASE_ID, referencedColumnName=DatabaseBindingIds.COSTSSET_ID, insertable=false, updatable=false)
private CostsSet costsSet;
@OneToOne(fetch=FetchType.LAZY, cascade= CascadeType.PERSIST) // one to one mappings are directly mapped using the phase primary keys
@BatchFetch(BatchFetchType.JOIN)
@JoinColumn(name=DatabaseBindingIds.PHASE_ID, referencedColumnName=DatabaseBindingIds.TODOSET_ID, insertable=false, updatable=false)
private TodoSet todoSet;
@ManyToOne(fetch=FetchType.EAGER, optional=false)
@JoinColumn(name=DatabaseBindingIds.PHASE_SCENARIO_ID, referencedColumnName=DatabaseBindingIds.SCENARIO_ID, insertable=true, updatable=true)
private Scenario scenario;
@ManyToOne(fetch=FetchType.EAGER)
@JoinColumn(name=DatabaseBindingIds.PHASE_PARENTPHASE_ID, referencedColumnName=DatabaseBindingIds.PHASE_ID, insertable=true, updatable=true)
private Phase parentPhase;
@Column
private Double sortIndex;
@Column
private String name;
@Column
private String description;
@Column
private String imageUrl;
@Column
@Convert(converter = DurationConverter.class)
private Duration budget;
@Column
@Convert(converter = DurationConverter.class)
private Duration planned;
@Column
@Convert(converter = PointInTimeConverter.class)
private PointInTime.Utc beg;
@Column//(name="\"End\"") // If you think you want to add this, check first why they are not escaped by the EclipseLink SessionCustomizer
@Convert(converter = PointInTimeConverter.class)
private PointInTime.Utc end;
@Column
private Boolean fixed;
@Column
private Integer progress;
@Column
private Boolean autoProgress;
@Column
private String costCenter;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name=DatabaseBindingIds.PHASE_OWNER_ID, referencedColumnName=DatabaseBindingIds.RESOURCE_ID, insertable=true, updatable=true)
private Resource owner;
@Column
private String color;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name=DatabaseBindingIds.PHASE_REQUIRED_SKILL_ID, referencedColumnName=DatabaseBindingIds.RESOURCE_ID, insertable=true, updatable=true)
private Resource requiredSkill;
@OneToMany(mappedBy="fromPhase", fetch=FetchType.LAZY)
private List<PhaseDependency> forwardDependencies;
@OneToMany(mappedBy="toPhase", fetch=FetchType.LAZY)
private List<PhaseDependency> reverseDependencies;
/**
* Get the phase id.
* @exception IllegalStateException EntityId can never be null.
*/
@Override
public String getId() {
if(entityId == null){
throw new IllegalStateException("[Constraint violation] entityId can not be null in " + this.getClass().getSimpleName());
}
return entityId;
}
/**
* Set the full id of this phase.
* @exception IllegalStateException EntityId can never be null.
*/
@Override
public void setId(String entityId) {
markNew();
this.entityId = entityId;
if(entityId == null){
throw new IllegalStateException("[Constraint violation] entityId can not be null in " + this.getClass().getSimpleName());
}
propertySet = new PropertySet();
propertySet.setId(entityId);
logSet = new LogSet();
logSet.setId(entityId);
costsSet = new CostsSet();
costsSet.setId(entityId);
todoSet = new TodoSet();
todoSet.setId(entityId);
}
/**
* Get the property set of the phase.
* @exception IllegalStateException propertySet can never be null.
*/
public PropertySet getPropertySet() {
if(propertySet == null){
throw new IllegalStateException("[Constraint violation] propertySet can not be null in " + this.getClass().getSimpleName());
}
return propertySet;
}
/**
* Get the log set of the phase.
* @exception IllegalStateException logSet can never be null.
*/
public LogSet getLogSet() {
if(logSet == null){
throw new IllegalStateException("[Constraint violation] logSet can not be null in " + this.getClass().getSimpleName());
}
return logSet;
}
/**
* Get the costs set of the phase.
* @exception IllegalStateException costsSet can never be null.
*/
public CostsSet getCostsSet() {
if(costsSet == null){
throw new IllegalStateException("[Constraint violation] costsSet can not be null in " + this.getClass().getSimpleName());
}
return costsSet;
}
/**
* Get the todo set of the phase.
* @exception IllegalStateException todoSet can never be null.
*/
public TodoSet getTodoSet() {
if(todoSet == null){
throw new IllegalStateException("[Constraint violation] todoSet can not be null in " + this.getClass().getSimpleName());
}
return todoSet;
}
/**
* Get the scenario of the phase.
* @exception IllegalStateException scenario can never be null.
*/
public Scenario getScenario() {
if(scenario == null){
throw new IllegalStateException("[Constraint violation] scenario can not be null in " + this.getClass().getSimpleName());
}
return scenario;
}
/**
* Set the scenario of the phase.
* @exception IllegalStateException scenario can never be null.
*/
public void setScenario(Scenario x) {
scenario = x;
if(scenario == null){
throw new IllegalStateException("[Constraint violation] scenario can not be null in " + this.getClass().getSimpleName());
}
}
/**
* Get the parent phase of this phase.
* @return parentPhase - can be null.
*/
public Phase getParentPhase() {
return parentPhase;
}
/**
* Set the parent phase of this phase.
* @return parentPhase - can be null.
*/
public void setParentPhase(Phase x) {
parentPhase = x;
}
/**
* Get the sort index of the phase.
* @return
*/
public double getSortIndex() {
return sortIndex == null ? 0.0 : sortIndex;
}
/**
* Set the sort index of the phase.
* @param x
*/
public void setSortIndex(double x) {
sortIndex = x;
}
/**
* Get the name of the phase.
* @return name - can be null.
*/
public String getName() {
return name;
}
/**
* Set the name of this phase.
* @param name - can be null.
*/
public void setName(String x) {
name = x;
}
/**
* Get the description of the phase.
* @return description - can be null.
*/
public String getDescription() {
return description;
}
/**
* Set the description of this phase.
* @param description - can be null.
*/
public void setDescription(String x) {
description = x;
}
/**
* Get the image url of the phase.
* @return imageUrl - can be null.
*/
public String getImageUrl() {
return imageUrl;
}
/**
* Set the imag url of this phase.
* @param imageUrl - can be null.
*/
public void setImageUrl(String x) {
imageUrl = x;
}
/**
* Get the budget of the phase.
* @return budget - can be null.
*/
public Duration getBudget() {
return budget;
}
/**
* Set the budget of this phase.
* @param budget - can be null.
*/
public void setBudget(Duration x) {
budget = x;
}
/**
* Get the planned duration of the phase.
* @return planned - can be null.
*/
public Duration getPlanned() {
return planned;
}
/**
* Set the planned duration of this phase.
* @param planned - can be null.
*/
public void setPlanned(Duration x) {
planned = x;
}
/**
* Get the beginning of the phase.
* @return beg - can be null.
*/
public PointInTime.Utc getBeg() {
return beg;
}
/**
* Set the beginning of this phase.
* @param beg - can be null.
*/
public void setBeg(PointInTime.Utc x) {
beg = x;
}
/**
* Get the ending of the phase.
* @return end - can be null.
*/
public PointInTime.Utc getEnd() {
return end;
}
/**
* Set the ending of this phase.
* @param end - can be null.
*/
public void setEnd(PointInTime.Utc x) {
end = x;
}
/**
* Get if the phase is fixed.
* @return
*/
public boolean getFixed() {
return fixed == null ? false : fixed;
}
/**
* Set if the phase is fixed.
* @param x
*/
public void setFixed(boolean x) {
fixed = x;
}
/**
* Get the progress of the phase.
* @return
*/
public int getProgress() {
return progress == null ? 0 : progress;
}
/**
* Set the progress of this phase.
* @param
*/
public void setProgress(int x) {
progress = x;
}
/**
* Get if the phase progresses automatically.
* @exception IllegalStateException autoProgress can never be null.
*/
public boolean getAutoProgress() {
return autoProgress == null ? false : autoProgress;
}
/**
* Get if the phase progresses automatically.
* @exception IllegalStateException autoProgress can never be null.
*/
public void setAutoProgress(boolean x) {
autoProgress = x;
}
... not relevant getters and setters...
}
为了在我们的问题发生时获得正确的异常,我们添加了IllegalStateExceptions,它告诉我们类似于db约束,违反了一个急切的加载约束。当问题发生时,我们得到:
java.lang.IllegalStateException: [Constraint violation] scenario can not be null in Phase
这意味着在加载宏分配期间,未加载场景。
我们发现的唯一解决方法是在数据库会话仍处于打开状态时访问场景,但根据我们的理解,这只需要加载延迟加载的实体关系。问题是什么?下面是宏分配加载方法:
@Override
public List<MacroAllocation> loadMacroAllocationsByResource(String resourceId) throws DataAccessException {
try(DatabaseSession session = new DatabaseSession(tenant)) {
List<MacroAllocation> macroAllocations = session.loadByQuery(MacroAllocation.class,
"SELECT ma FROM MacroAllocation AS ma "
+ "WHERE ma.resource.entityId = ?1 " // with matching primary key in resource
+ "AND ma.deleted = 0", // and not deleted
resourceId);
//Workaround: Some objects are not eagerly fetched in some cases. Here we do that explicitly.
macroAllocations.stream().forEach((m) -> {
session.fetch(m.getPhase().getScenario());
session.fetch(m.getPhase().getScenario().getProject());
});
return macroAllocations;
} catch(RuntimeException e) {
throw new DataAccessException(e);
}
}
以及我们在此方法中使用的数据库会话:
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 String persistenceUnitName;
/**
* 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;
private String tenant;
private static final Cache<String, EntityManagerFactory> emfs = new Cache<>(tenant -> { // create if absent
synchronized (DatabaseSession.class) {
HashMap<String,String> properties = new HashMap<>();
properties.put(SESSION_NAME, (tenant!=null && tenant != "")?tenant+"-session":"non-tenant-session");
properties.put(MULTITENANT_PROPERTY_DEFAULT, tenant);
if(persistenceUnitName == null){
log.debug("Persistence Unit Name defaults to: default");
persistenceUnitName = "default";
}
return Persistence.createEntityManagerFactory(persistenceUnitName, properties);
}
},
(entityManagerFactory) -> { // on remove
entityManagerFactory.close();
});
/**
* Opens a new non-tenant specific session and begins a new transaction.
*
* Only shared, non-tenant specific entities can be retrieved. Multitenancy entities can not be retrieved using this session. To retrieve tenant specific entities, create a tenant session using <b>new DataSession(tenant)</b>
*/
public DatabaseSession() {
this.tenant = "";
synchronized (DatabaseSession.class) {
emfs.get(tenant);
}
createEntityManager();
}
/**
* Opens a new tenant session and begins a new transaction.
*
* Multitenancy entities can be retrieved using this session.
*/
public DatabaseSession(String tenant) {
if(tenant == null || tenant.equals("")){
log.error("Trying to create a non-tenant database session with tenant specific constructor? Use constructor DatabaseSession() instead.");
tenant = "";
}
this.tenant = tenant;
synchronized (DatabaseSession.class) {
emfs.get(tenant); // creates a new factory in a synchronized manner.
}
createEntityManager();
}
/**
* @note: Should only be called publicly by unit tests.
*/
public void createEntityManager() {
em = emfs.get(tenant).createEntityManager();
em.getTransaction().begin();
}
/**
* 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 pun) {
persistenceUnitName = pun;
}
/**
* Sets up all factories to prevent eclipselink from drop and creating the db.
*
* @note: Should only be called by unit tests.
*/
public void setupEntityManagerFactories(List<String> tenants) {
log.warn("SetupEntityManagerFactories should only be called by Unit tests.");
for(String tenant: tenants){
emfs.get(tenant);
}
}
/**
* Closes the connection to the database completely. For Unit tests, this drops and recreates the database to advance to the next unit test with a fresh one.
*
* @note: Should only be called by unit tests.
*/
public void shutdownDB() {
log.warn("ShutdownDB should only be called by Unit tests.");
em.close();
em = null;
DatabaseSession.emfs.clear(); // closes entity manager factory on close
}
@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) {
return loadAll(clazz, true);
}
public <T extends PersistentRecord> List<T> loadAll(Class<T> clazz, 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 (filterDeleted) {
q.where(b.equal(r.get(et.getAttribute("deleted").getName()), 0));
}
List<T> results = 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 results;
}
public <T extends PersistentRecord> int count(Class<T> clazz) {
return count(clazz, true);
}
public <T extends PersistentRecord> int count(Class<T> clazz, 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 (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 id) {
return load(clazz, id, true);
}
public <T extends PersistentRecord> T load(Class<T> clazz, String id, boolean filterDeleted) {
log("load(%s, %s)", clazz.getSimpleName(), id);
long t = System.nanoTime();
T result = em.find(clazz, id);
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 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();
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 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();
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;
}
... storing methods not relevant here...
}