@BatchSize and hibernate.default_batch_fetch_size should affect eager loading of non-owning one-to-one relations

Description

When executing a jpql query FetchMode.JOIN (which is the default for eager to one relations) is ignored. This is reasonable, because it would alter the query executed by the user. The unfortunate side effect is that n+1 queries are executed to fill the eager relation. Making the relation lazy is not always possible, because a non-owning optional relation can't be lazy.
To prevent n+1 queries you can use the awesome feature @BatchSize or hibernate.default_batch_fetch_size. This works fine for both lazy and eager one-to-one relations on the owning side, but it does not work for non-owning eager (lazy is not possible due to ) one-to-one relations.
This is a big performance problem and I believe this is the main reason why people say one should avoid bidirectional one-to-one relations in hibernate.
Looking at the code this is because in EntityType.resolve it uses loadByUniqueKey for non-owning one-to-one realations, but unlike resolveIdentifier which internally calls DefaultLoadEventListener.loadFromDatasource which uses batch fetching, loadByUniqueKey uses Loader.doQuery which does not use batch fetching.

Testcase:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 package org.hibernate.bugs; import static org.assertj.core.api.Assertions.assertThat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.OneToOne; import javax.persistence.Persistence; import javax.persistence.Query; import org.apache.log4j.AppenderSkeleton; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.spi.LoggingEvent; import org.hibernate.annotations.BatchSize; import org.junit.After; import org.junit.Before; import org.junit.Test; /** * This template demonstrates how to develop a test case for Hibernate ORM, using the Java Persistence API. */ public class JPAUnitTestCase { private EntityManagerFactory entityManagerFactory; private Logger sqlLogger; private Level initialSqlLevel; @Before public void init() { sqlLogger = Logger.getLogger("org.hibernate.SQL"); initialSqlLevel = sqlLogger.getLevel(); sqlLogger.setLevel(Level.DEBUG); entityManagerFactory = Persistence.createEntityManagerFactory("templatePU"); } @After public void destroy() { entityManagerFactory.close(); sqlLogger.setLevel(initialSqlLevel); } // Entities are auto-discovered, so just add them anywhere on class-path // Add your tests, using standard JUnit. @Test public void hhh123Test() throws Exception { EntityManager entityManager = entityManagerFactory.createEntityManager(); int numberOfParents = 5; runInTransaction(entityManager, () -> { for (int id = 0; id < numberOfParents; id++) { Parent parent = new Parent(id); new Child(id + 100, parent); entityManager.persist(parent); } }); runInTransaction(entityManager, () -> { RecordingLog4jAppender appender = new RecordingLog4jAppender(); sqlLogger.addAppender(appender); Query query = entityManager.createQuery("from " + Parent.class.getSimpleName()); @SuppressWarnings("unchecked") List<Parent> parents = query.getResultList(); sqlLogger.removeAppender(appender); assertThat(parents).hasSize(numberOfParents); assertThat(appender.getLogEvents()).size().isLessThan(numberOfParents); }); entityManager.close(); } private void runInTransaction(EntityManager entityManager, Runnable runnable) { entityManager.getTransaction().begin(); runnable.run(); entityManager.getTransaction().commit(); entityManager.clear(); } } @Entity @BatchSize(size = 20) class Parent { @Id private long id; @OneToOne(mappedBy = "parent", cascade = CascadeType.ALL) private Child child; Parent() {} public Parent(long id) { this.id = id; } public Child getChild() { return child; } void setChild(Child child) { this.child = child; } @Override public String toString() { return "Parent [id=" + id + ", child=" + child + "]"; } } @Entity @BatchSize(size = 20) class Child { @Id private long id; @OneToOne(fetch = FetchType.LAZY) private Parent parent; Child() {} public Child(long id, Parent parent) { this.id = id; setParent(parent); } public Parent getParent() { return parent; } public void setParent(Parent parent) { this.parent = parent; parent.setChild(this); } @Override public String toString() { return "Child [id=" + id + "]"; } } class RecordingLog4jAppender extends AppenderSkeleton { private final List<LoggingEvent> logEvents = new ArrayList<>(); private boolean active = true; @Override public boolean requiresLayout() { return false; } @Override protected void append(LoggingEvent loggingEvent) { if (active) { logEvents.add(loggingEvent); } } @Override public void close() {} public void setActive(boolean value) { active = value; } public void clear() { logEvents.clear(); } public List<LoggingEvent> getLogEvents() { return Collections.unmodifiableList(logEvents); } }

Status

Assignee

Unassigned

Reporter

Adrodoc

Fix versions

None

Labels

backPortable

None

Suitable for new contributors

None

Requires Release Note

None

Pull Request

None

backportDecision

None

Components

Affects versions

5.3.1

Priority

Major