Skip to content
This repository has been archived by the owner on Mar 31, 2022. It is now read-only.

Commit

Permalink
UnfetchedAttributeException when query result contains same object in…
Browse files Browse the repository at this point in the history
… different layers with different plans #83
  • Loading branch information
dtaimanov committed Jul 27, 2021
1 parent e2e0704 commit fa94790
Show file tree
Hide file tree
Showing 5 changed files with 459 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public class FetchGroupManager {
@Autowired
private QueryTransformerFactory queryTransformerFactory;

@Autowired
private FetchPlans fetchPlans;

public void setFetchPlan(JpaQuery query, String queryString, @Nullable FetchPlan fetchPlan, boolean singleResultExpected) {
Preconditions.checkNotNullArgument(query, "query is null");
if (fetchPlan != null) {
Expand Down Expand Up @@ -135,6 +138,9 @@ public FetchGroupDescription calculateFetchGroup(String queryString,
boolean singleResultExpected,
boolean useFetchGroup) {
Set<FetchGroupField> fetchGroupFields = new LinkedHashSet<>();

fetchPlan = completeFetchPlan(fetchPlan);

processFetchPlan(fetchPlan, null, fetchGroupFields, useFetchGroup);

FetchGroupDescription description = new FetchGroupDescription();
Expand Down Expand Up @@ -372,6 +378,81 @@ private List<String> getMasterEntityAttributes(Set<FetchGroupField> fetchGroupFi
return result;
}

/**
* Looks for similar fetch plans on different layers with the same first-layer properties but different 2-nd and further
* layer properties.
* Entities loaded by such fetch plans may have unfetched attributes because treated as equals and cached instead of
* loading with each {@link FetchGroup}.
* Replaces fetch plans using union of all similar plans for each such entity.
* <p>
* <p>
* see also: {@link org.eclipse.persistence.internal.descriptors.ObjectBuilder}#isObjectValidForFetchGroup,
* <p>
* {@link org.eclipse.persistence.internal.descriptors.ObjectBuilder}#buildWorkingCopyCloneFromRow,
* <p>
* {@link org.eclipse.persistence.internal.queries.EntityFetchGroup}
*
* @param original plan to analyze
* @return completed FetchPlan or original if no updates needed.
*/
private FetchPlan completeFetchPlan(FetchPlan original) {
Map<MetaClass, List<OccurrenceDescription>> occurrences = new HashMap<>();
Map<String, List<FetchPlan>> absentProperties = new HashMap<>();

checkFetchPlan(original, occurrences, "", absentProperties);

if (absentProperties.isEmpty())
return original;

FetchPlanBuilder builder = fetchPlans.builder(original);

for (Map.Entry<String, List<FetchPlan>> entry : absentProperties.entrySet()) {

if (entry.getKey().isEmpty()) {
for (FetchPlan plan : entry.getValue()) {
builder.merge(plan);
}
} else {
for (FetchPlan plan : entry.getValue()) {
builder.mergeNestedProperty(entry.getKey(), plan);
}
}
}

return builder.build();

}

private void checkFetchPlan(FetchPlan subject, Map<MetaClass, List<OccurrenceDescription>> occurrences, String path, Map<String, List<FetchPlan>> absentProperties) {
MetaClass metaClass = metadata.getClass(subject.getEntityClass());
List<String> firstLayer = subject.getProperties().stream()
.map(FetchPlanProperty::getName)
.sorted()
.collect(Collectors.toList());

List<OccurrenceDescription> sameMetaclassOccurrences = occurrences.computeIfAbsent(metaClass, k -> new LinkedList<>());

for (OccurrenceDescription candidate : sameMetaclassOccurrences) {
if (candidate.firstLayer.containsAll(firstLayer)
&& !candidate.fetchPlan.isSupersetOf(subject)) {
absentProperties.computeIfAbsent(candidate.path, k -> new LinkedList<>()).add(subject);
}
if (firstLayer.containsAll(candidate.firstLayer)
&& !subject.isSupersetOf(candidate.fetchPlan)
&& !path.startsWith(candidate.path)) {//not need for wider child property - it will be initialized after parent anyway
absentProperties.computeIfAbsent(path, k -> new LinkedList<>()).add(candidate.fetchPlan);
}
}
sameMetaclassOccurrences.add(new OccurrenceDescription(subject, path, firstLayer));

for (FetchPlanProperty property : subject.getProperties()) {
if (property.getFetchPlan() != null) {
checkFetchPlan(property.getFetchPlan(), occurrences,
(path.length() > 0 ? path + "." : "") + property.getName(), absentProperties);
}
}
}

private void processFetchPlan(FetchPlan fetchPlan, @Nullable FetchGroupField parentField, Set<FetchGroupField> fetchGroupFields, boolean useFetchGroup) {
Class<?> entityClass = fetchPlan.getEntityClass();
MetaClass entityMetaClass = metadata.getClass(entityClass);
Expand Down Expand Up @@ -542,4 +623,18 @@ public String toString() {
return path();
}
}

private static class OccurrenceDescription {
private final FetchPlan fetchPlan;
private final String path;
private final List<String> firstLayer;

public OccurrenceDescription(FetchPlan fetchPlan, String path, List<String> firstLayer) {
this.fetchPlan = fetchPlan;
this.path = path;
this.firstLayer = firstLayer;
}

}

}
110 changes: 110 additions & 0 deletions eclipselink/src/test/groovy/entity_fetcher/EntityFetcherTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import org.eclipse.persistence.queries.FetchGroupTracker
import org.springframework.beans.factory.annotation.Autowired
import test_support.DataSpec
import test_support.entity.TestEntityWithNonPersistentRef
import test_support.entity.complex_references.Employee
import test_support.entity.complex_references.Position
import test_support.entity.complex_references.Unit
import test_support.entity.sales.Customer
import test_support.entity.sales.Order
import test_support.entity.sales.OrderLine
import test_support.entity.sales.Status

class EntityFetcherTest extends DataSpec {
Expand All @@ -37,6 +42,45 @@ class EntityFetcherTest extends DataSpec {
@Autowired
FetchPlans fetchPlans


private Order order;
private OrderLine orderLine;
private Employee selfSupervised;
private Position position;
private Unit unit;

def setup() {
order = dataManager.create(Order)
order.number = "1"
order.amount = BigDecimal.ONE
orderLine = dataManager.create(OrderLine)
orderLine.order = order
orderLine.quantity = 1
dataManager.save(orderLine, order)


unit = dataManager.create(Unit)
unit.title = "First"
unit.address = "561728"
dataManager.save(unit)
position = dataManager.create(Position)
position.title = "Main"
position.description = "-"
position.factor = 1d
position.defaultUnit = unit
dataManager.save(position)
selfSupervised = dataManager.create(Employee)
selfSupervised.name = "Twoflower"
selfSupervised.supervisor = selfSupervised
selfSupervised.position = position
selfSupervised.unit = unit
dataManager.save(selfSupervised)
}

def cleanup() {
dataManager.remove(order, orderLine, selfSupervised, position, unit)
}

def "fetching entity with non-persistent reference"() {
// setup the entity like it is stored in a custom datastore and linked as transient property
def npCustomer = dataManager.create(Customer)
Expand All @@ -58,4 +102,70 @@ class EntityFetcherTest extends DataSpec {
noExceptionThrown()
committed == entity
}

def "different views on different layers: case 1"() {
when: "same entity with different fetchPlans appears on different layers of fetch plan"
OrderLine orderLine = dataManager.load(OrderLine.class).query("select ol from sales_OrderLine ol")
.fetchPlan(fetchPlans.builder(OrderLine.class)
.add("order.orderLines.order.number")
.add("order.amount")
.build())
.one()

then: "each occurence of this entity loaded correctly and no 'Unfetched attribute' exception thrown"
orderLine.order.orderLines[0].order.number != null
orderLine.order.amount != null
}


def "different views on different layers: case 2 with self ref"() {
when: "same entity with different fetchPlans appears on different layers through self-reference"
Employee employee = dataManager.load(Employee.class)
.query("select e from test_Employee e")
.fetchPlan(fetchPlans.builder(Employee)
.add("supervisor.position.description")
.add("position.title")
.add("name")
.add("unit.title")
.add("unit.address")
.build())
.one()

then: "employee position loaded with description"
employee.supervisor.position.description != null
employee.name != null
}

def "different views on different layers: case 3 through different entities"() {
when: "same entity with different fetchPlans appears on different layers through other entity"
Employee employee = dataManager.load(Employee.class)
.query("select e from test_Employee e")
.fetchPlan(fetchPlans.builder(Employee)
.add("supervisor.position.description")
.add("unit.employees.position.title")
.build())
.one()

then: "employee position loaded with both name and description"
employee.supervisor.position.title != null
employee.supervisor.position.description != null
employee.unit.employees[0].position.title != null
employee.unit.employees[0].position.description != null
}

def "different views on different layers: case 4 complex property"() {
when:
Employee employee = dataManager.load(Employee.class)
.query("select e from test_Employee e")
.fetchPlan(fetchPlans.builder(Employee)
.add("supervisor.position.defaultUnit.title")
.add("supervisor.position.defaultUnit.address")
.add("position.title")
.build())
.one()

then:
employee.position.defaultUnit.title != null
employee.position.defaultUnit.address != null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2021 Haulmont.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package test_support.entity.complex_references;

import io.jmix.core.entity.annotation.JmixGeneratedValue;
import io.jmix.core.metamodel.annotation.InstanceName;
import io.jmix.core.metamodel.annotation.JmixEntity;

import javax.persistence.*;
import java.util.UUID;

@JmixEntity
@Table(name = "TEST_EMPLOYEE")
@Entity(name = "test_Employee")
public class Employee {

@JmixGeneratedValue
@Column(name = "ID", nullable = false)
@Id
private UUID id;

@Column(name = "NAME")
@InstanceName
private String name;

@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.DETACH)
@JoinColumn(name = "SUPERVISOR_ID")
private Employee supervisor;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "UNIT_ID")
private Unit unit;


@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.DETACH)
@JoinColumn(name = "POSITION_ID")
private Position position;

public UUID getId() {
return id;
}

public void setId(UUID id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Employee getSupervisor() {
return supervisor;
}

public void setSupervisor(Employee supervisor) {
this.supervisor = supervisor;
}

public Position getPosition() {
return position;
}

public void setPosition(Position position) {
this.position = position;
}

public Unit getUnit() {
return unit;
}

public void setUnit(Unit unit) {
this.unit = unit;
}
}

0 comments on commit fa94790

Please sign in to comment.