diff --git a/jmix-data/eclipselink/src/main/java/io/jmix/eclipselink/impl/lazyloading/ValueHoldersSupport.java b/jmix-data/eclipselink/src/main/java/io/jmix/eclipselink/impl/lazyloading/ValueHoldersSupport.java
index f4dcfacef3..a0772b4e52 100644
--- a/jmix-data/eclipselink/src/main/java/io/jmix/eclipselink/impl/lazyloading/ValueHoldersSupport.java
+++ b/jmix-data/eclipselink/src/main/java/io/jmix/eclipselink/impl/lazyloading/ValueHoldersSupport.java
@@ -27,6 +27,8 @@
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;
@@ -34,13 +36,26 @@
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.
+ *
+ * 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);
diff --git a/jmix-data/eclipselink/src/test/groovy/lazy_loading/EagerMappedByOneToOneLazyLoadingTest.groovy b/jmix-data/eclipselink/src/test/groovy/lazy_loading/EagerMappedByOneToOneLazyLoadingTest.groovy
new file mode 100644
index 0000000000..198eed3b8a
--- /dev/null
+++ b/jmix-data/eclipselink/src/test/groovy/lazy_loading/EagerMappedByOneToOneLazyLoadingTest.groovy
@@ -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}.
+ *
+ * Such attributes are EAGER at enhancing time, so no {@code _persistence__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
+ }
+}
diff --git a/jmix-data/eclipselink/src/test/java/test_support/entity/lazyloading/eager_mapped_by/EagerMappedByPassport.java b/jmix-data/eclipselink/src/test/java/test_support/entity/lazyloading/eager_mapped_by/EagerMappedByPassport.java
new file mode 100644
index 0000000000..6d99e6c502
--- /dev/null
+++ b/jmix-data/eclipselink/src/test/java/test_support/entity/lazyloading/eager_mapped_by/EagerMappedByPassport.java
@@ -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;
+ }
+}
diff --git a/jmix-data/eclipselink/src/test/java/test_support/entity/lazyloading/eager_mapped_by/EagerMappedByPerson.java b/jmix-data/eclipselink/src/test/java/test_support/entity/lazyloading/eager_mapped_by/EagerMappedByPerson.java
new file mode 100644
index 0000000000..717a438a2d
--- /dev/null
+++ b/jmix-data/eclipselink/src/test/java/test_support/entity/lazyloading/eager_mapped_by/EagerMappedByPerson.java
@@ -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;
+ }
+}
diff --git a/jmix-data/eclipselink/src/test/java/test_support/entity/lazyloading/eager_mapped_by/EagerMappedByVisit.java b/jmix-data/eclipselink/src/test/java/test_support/entity/lazyloading/eager_mapped_by/EagerMappedByVisit.java
new file mode 100644
index 0000000000..e9b7a5483f
--- /dev/null
+++ b/jmix-data/eclipselink/src/test/java/test_support/entity/lazyloading/eager_mapped_by/EagerMappedByVisit.java
@@ -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;
+ }
+}