Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,35 @@
import org.eclipse.persistence.internal.indirection.UnitOfWorkQueryValueHolder;
import io.jmix.core.common.util.ReflectionHelper;
import org.eclipse.persistence.internal.indirection.WrappingValueHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicReference;

public class ValueHoldersSupport {

private static final Logger log = LoggerFactory.getLogger(ValueHoldersSupport.class);

/**
* Returns a value holder of the given single-value reference attribute, woven into the entity class
* by the enhancing process.
* <p>
* Returns {@code null} if no value holder field is woven for the attribute, which means the attribute
* was not lazy at enhancing time (e.g. a {@code @OneToOne(mappedBy = ...)} attribute declared without
* {@code fetch = FetchType.LAZY}). Such attributes are always loaded eagerly by the ORM and do not
* participate in lazy loading.
*/
@Nullable
public static Object getSingleValueHolder(Object entity, String propertyName) {
Object valueHolder;
try {
Field valueHolderField = ReflectionHelper.findField(entity.getClass(), String.format("_persistence_%s_vh", propertyName));
if (valueHolderField == null) {
throw new RuntimeException(String.format("Unable to access value holder for property: %s on entity %s",
propertyName, entity.getClass().getName()));
log.trace("No value holder field is woven for property '{}' of entity {} - the attribute is not lazy",
propertyName, entity.getClass().getName());
return null;
}

ReflectionUtils.makeAccessible(valueHolderField);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright 2026 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 lazy_loading

import io.jmix.core.DataManager
import io.jmix.core.EntityStates
import org.springframework.beans.factory.annotation.Autowired
import spock.lang.IgnoreIf
import test_support.DataSpec
import test_support.entity.lazyloading.eager_mapped_by.EagerMappedByPassport
import test_support.entity.lazyloading.eager_mapped_by.EagerMappedByPerson
import test_support.entity.lazyloading.eager_mapped_by.EagerMappedByVisit

/**
* Covers "Unable to access value holder for property" error for {@code @OneToOne(mappedBy = ...)}
* attributes declared without {@code fetch = FetchType.LAZY}.
* <p>
* Such attributes are EAGER at enhancing time, so no {@code _persistence_<name>_vh} field is woven
* for them, and lazy loading post-processing must skip them instead of failing.
*/
@IgnoreIf({ Boolean.valueOf(System.getenv("JMIX_ECLIPSELINK_DISABLELAZYLOADING")) })
class EagerMappedByOneToOneLazyLoadingTest extends DataSpec {

@Autowired
DataManager dataManager
@Autowired
EntityStates entityStates

EagerMappedByPassport passport
EagerMappedByPerson person
EagerMappedByVisit visit

void setup() {
passport = dataManager.create(EagerMappedByPassport)
passport.number = '123'

person = dataManager.create(EagerMappedByPerson)
person.name = 'Bob'
person.passport = passport

visit = dataManager.create(EagerMappedByVisit)
visit.passport = passport

dataManager.save(passport, person, visit)
}

void cleanup() {
jdbc.update('delete from TEST_EAGER_MAPPED_BY_VISIT')
jdbc.update('delete from TEST_EAGER_MAPPED_BY_PERSON')
jdbc.update('delete from TEST_EAGER_MAPPED_BY_PASSPORT')
}

def "direct load of entity with non-lazy mappedBy one-to-one attribute"() {
when:
EagerMappedByPassport loadedPassport = dataManager.load(EagerMappedByPassport).id(passport.id).one()

then:
loadedPassport.number == '123'
loadedPassport.person.id == person.id
}

def "lazy loading a reference to entity with non-lazy mappedBy one-to-one, owner is of the same class as the attribute"() {
// the value holder post-processing traversal takes the 'replaceToExistingReferences' branch
when:
EagerMappedByPerson loadedPerson = dataManager.load(EagerMappedByPerson).id(person.id).one()

then:
!entityStates.isLoaded(loadedPerson, 'passport')

when:
EagerMappedByPassport loadedPassport = loadedPerson.passport

then:
loadedPassport.number == '123'
loadedPassport.person.id == person.id
}

def "lazy loading a reference to entity with non-lazy mappedBy one-to-one, owner is of an unrelated class"() {
// the value holder post-processing traversal takes the 'replaceLoadOptions' branch
when:
EagerMappedByVisit loadedVisit = dataManager.load(EagerMappedByVisit).id(visit.id).one()

then:
!entityStates.isLoaded(loadedVisit, 'passport')

when:
EagerMappedByPassport loadedPassport = loadedVisit.passport

then:
loadedPassport.number == '123'
loadedPassport.person.id == person.id
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2026 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.lazyloading.eager_mapped_by;

import io.jmix.core.metamodel.annotation.JmixEntity;
import jakarta.persistence.*;
import test_support.entity.BaseEntity;

@Table(name = "TEST_EAGER_MAPPED_BY_PASSPORT")
@JmixEntity
@Entity(name = "test_EagerMappedByPassport")
public class EagerMappedByPassport extends BaseEntity {
@Column(name = "NUMBER_")
protected String number;

// intentionally declared without fetch = FetchType.LAZY: the attribute is EAGER at enhancing time,
// so no '_persistence_person_vh' field is woven for it
@OneToOne(mappedBy = "passport")
protected EagerMappedByPerson person;

public String getNumber() {
return number;
}

public void setNumber(String number) {
this.number = number;
}

public EagerMappedByPerson getPerson() {
return person;
}

public void setPerson(EagerMappedByPerson person) {
this.person = person;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2026 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.lazyloading.eager_mapped_by;

import io.jmix.core.metamodel.annotation.JmixEntity;
import jakarta.persistence.*;
import test_support.entity.BaseEntity;

@Table(name = "TEST_EAGER_MAPPED_BY_PERSON")
@JmixEntity
@Entity(name = "test_EagerMappedByPerson")
public class EagerMappedByPerson extends BaseEntity {
@Column(name = "NAME")
protected String name;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "PASSPORT_ID")
protected EagerMappedByPassport passport;

public String getName() {
return name;
}

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

public EagerMappedByPassport getPassport() {
return passport;
}

public void setPassport(EagerMappedByPassport passport) {
this.passport = passport;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2026 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.lazyloading.eager_mapped_by;

import io.jmix.core.metamodel.annotation.JmixEntity;
import jakarta.persistence.*;
import test_support.entity.BaseEntity;

@Table(name = "TEST_EAGER_MAPPED_BY_VISIT")
@JmixEntity
@Entity(name = "test_EagerMappedByVisit")
public class EagerMappedByVisit extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "PASSPORT_ID")
protected EagerMappedByPassport passport;

public EagerMappedByPassport getPassport() {
return passport;
}

public void setPassport(EagerMappedByPassport passport) {
this.passport = passport;
}
}