mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
Improved Managed M2M relations (#72)
Co-authored-by: Roland Praml <roland.praml@foconis.de>
This commit is contained in:
co-authored by
Roland Praml
parent
a749954cdd
commit
c0066e9a73
@@ -0,0 +1,29 @@
|
||||
package io.ebean.annotation.ext;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.ElementType.TYPE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* Annotation to define a factory for an intersection model. This class MUST have a constructor or factory method with two parameters that accepts parent and property type.
|
||||
* @author Roland Praml, FOCONIS AG
|
||||
*/
|
||||
@Documented
|
||||
@Target({ FIELD, TYPE })
|
||||
@Retention(RUNTIME)
|
||||
public @interface IntersectionFactory {
|
||||
|
||||
/**
|
||||
* The intersection model class.
|
||||
*/
|
||||
Class value();
|
||||
|
||||
/**
|
||||
* An optional factory method.
|
||||
*/
|
||||
String factoryMethod() default "";
|
||||
}
|
||||
@@ -41,6 +41,7 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
|
||||
private final String intersectionPublishTable;
|
||||
private final String intersectionDraftTable;
|
||||
private final boolean orphanRemoval;
|
||||
private final IntersectionFactoryHelp intersectionFactory;
|
||||
private IntersectionTable intersectionTable;
|
||||
/**
|
||||
* For ManyToMany this is the Inverse join used to build reference queries.
|
||||
@@ -99,6 +100,7 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
|
||||
this.mapKey = deploy.getMapKey();
|
||||
this.fetchOrderBy = deploy.getFetchOrderBy();
|
||||
this.intersectionJoin = deploy.createIntersectionTableJoin();
|
||||
this.intersectionFactory = deploy.getIntersectionFactory();
|
||||
if (intersectionJoin != null) {
|
||||
this.tableManaged = deploy.isTableManaged();
|
||||
this.intersectionPublishTable = intersectionJoin.getTable();
|
||||
@@ -1059,4 +1061,13 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isTableManaged() {
|
||||
return tableManaged;
|
||||
}
|
||||
|
||||
public IntersectionFactoryHelp getIntersectionFactory() {
|
||||
return intersectionFactory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package io.ebeaninternal.server.deploy;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Helper class to construct intersection beans.
|
||||
*
|
||||
* @author Roland Praml, FOCONIS AG
|
||||
*/
|
||||
public class IntersectionFactoryHelp {
|
||||
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
|
||||
private final MethodHandle handle;
|
||||
|
||||
public IntersectionFactoryHelp(Class clazz, Class leftSide, Class rightSide, String factoryMethod) {
|
||||
try {
|
||||
if (factoryMethod.isEmpty()) {
|
||||
Constructor ctor = findCtor(clazz, leftSide, rightSide);
|
||||
handle = LOOKUP.findConstructor(clazz, MethodType.methodType(void.class, ctor.getParameterTypes()));
|
||||
} else {
|
||||
Method method = findMethod(clazz, factoryMethod, leftSide, rightSide);
|
||||
handle = LOOKUP.findStatic(clazz, method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes()));
|
||||
}
|
||||
} catch (NoSuchMethodException | IllegalAccessException e) {
|
||||
throw new RuntimeException("The factory" + clazz.getName()
|
||||
+ " must define a public constructor or static factory method that accepts (" + leftSide.getName() + ", " + rightSide.getName() + ")", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Constructor findCtor(Class clazz, Class leftSide, Class rightSide) throws NoSuchMethodException {
|
||||
for (Constructor constructor : clazz.getConstructors()) {
|
||||
Class[] types = constructor.getParameterTypes();
|
||||
if (types.length == 2) {
|
||||
// we are only interested in ctors with 2 arguments
|
||||
if (types[0].isAssignableFrom(leftSide) && types[1].isAssignableFrom(rightSide)) {
|
||||
return constructor;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new NoSuchMethodException("Could not find valid constructor");
|
||||
}
|
||||
|
||||
private Method findMethod(Class clazz, String methodName, Class leftSide, Class rightSide) throws NoSuchMethodException {
|
||||
for (Method method : clazz.getMethods()) {
|
||||
Class[] types = method.getParameterTypes();
|
||||
if (types.length == 2 && method.getName().equals(methodName)) {
|
||||
// we are only interested in ctors with 2 arguments
|
||||
if (types[0].isAssignableFrom(leftSide) && types[1].isAssignableFrom(rightSide)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new NoSuchMethodException("Could not find valid method");
|
||||
}
|
||||
|
||||
public Object invoke(Object left, Object right) {
|
||||
try {
|
||||
return handle.invoke(left, right);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Unexpected error creating Intersection bean", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ public final class IntersectionRow {
|
||||
return new DefaultSqlUpdate(server, sb.toString(), bindParams);
|
||||
}
|
||||
|
||||
public SpiSqlUpdate createDelete(SpiEbeanServer server, DeleteMode deleteMode) {
|
||||
public SpiSqlUpdate createDelete(SpiEbeanServer server, DeleteMode deleteMode, String extraWhere) {
|
||||
BindParams bindParams = new BindParams();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (deleteMode.isHard()) {
|
||||
@@ -89,17 +89,34 @@ public final class IntersectionRow {
|
||||
bindParams.setParameter(++count, bindValue);
|
||||
}
|
||||
}
|
||||
addExtraWhere(sb, extraWhere);
|
||||
|
||||
return new DefaultSqlUpdate(server, sb.toString(), bindParams);
|
||||
}
|
||||
|
||||
public SpiSqlUpdate createDeleteChildren(SpiEbeanServer server) {
|
||||
|
||||
public SpiSqlUpdate createDeleteChildren(SpiEbeanServer server, String extraWhere) {
|
||||
BindParams bindParams = new BindParams();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("delete from ").append(tableName).append(" where ");
|
||||
setBindParams(bindParams, sb);
|
||||
addExtraWhere(sb, extraWhere);
|
||||
return new DefaultSqlUpdate(server, sb.toString(), bindParams);
|
||||
}
|
||||
|
||||
private void addExtraWhere(StringBuilder sb, String extraWhere) {
|
||||
if (extraWhere != null) {
|
||||
if (extraWhere.indexOf("${ta}") == -1) {
|
||||
// no table alias append ${mta} to query.
|
||||
sb.append(" and ").append(extraWhere.replace("${mta}", tableName));
|
||||
} else if (extraWhere.indexOf("${mta}") != -1) {
|
||||
// we have a table alias - this is not interesting for deletion.
|
||||
// but if have also a m2m table alias - this is a problem now!
|
||||
throw new UnsupportedOperationException("extraWhere \'" + extraWhere + "\' has both ${ta} and ${mta} - this is not yet supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int setBindParams(BindParams bindParams, StringBuilder sb) {
|
||||
int count = 0;
|
||||
for (Map.Entry<String, Object> entry : values.entrySet()) {
|
||||
|
||||
+21
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.deploy.meta;
|
||||
|
||||
import io.ebean.bean.BeanCollection.ModifyListenMode;
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
import io.ebeaninternal.server.deploy.IntersectionFactoryHelp;
|
||||
import io.ebeaninternal.server.deploy.ManyType;
|
||||
import io.ebeaninternal.server.deploy.TableJoin;
|
||||
import io.ebeaninternal.server.type.TypeReflectHelper;
|
||||
@@ -32,6 +33,12 @@ public class DeployBeanPropertyAssocMany<T> extends DeployBeanPropertyAssoc<T> {
|
||||
* Join for manyToMany intersection table.
|
||||
*/
|
||||
private DeployTableJoin intersectionJoin;
|
||||
|
||||
/**
|
||||
* Factory to create intersection beans (instead of rows). For managed intersections.
|
||||
*/
|
||||
private IntersectionFactoryHelp intersectionFactory;
|
||||
|
||||
/**
|
||||
* For ManyToMany this is the Inverse join used to build reference queries.
|
||||
*/
|
||||
@@ -153,6 +160,20 @@ public class DeployBeanPropertyAssocMany<T> extends DeployBeanPropertyAssoc<T> {
|
||||
this.inverseJoin = inverseJoin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the intersection factory.
|
||||
*/
|
||||
public IntersectionFactoryHelp getIntersectionFactory() {
|
||||
return intersectionFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the intersection factory.
|
||||
*/
|
||||
public void setIntersectionFactory(IntersectionFactoryHelp intersectionFactory) {
|
||||
this.intersectionFactory = intersectionFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the order by clause used to order the fetching of the data for
|
||||
* this list, set or map.
|
||||
|
||||
+13
-4
@@ -4,15 +4,13 @@ import io.ebean.annotation.DbForeignKey;
|
||||
import io.ebean.annotation.FetchPreference;
|
||||
import io.ebean.annotation.HistoryExclude;
|
||||
import io.ebean.annotation.Where;
|
||||
import io.ebean.annotation.ext.IntersectionFactory;
|
||||
import io.ebean.bean.BeanCollection.ModifyListenMode;
|
||||
import io.ebean.config.NamingConvention;
|
||||
import io.ebean.config.TableName;
|
||||
import io.ebean.core.type.ScalarType;
|
||||
import io.ebean.util.CamelCaseHelper;
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptorManager;
|
||||
import io.ebeaninternal.server.deploy.BeanProperty;
|
||||
import io.ebeaninternal.server.deploy.BeanTable;
|
||||
import io.ebeaninternal.server.deploy.PropertyForeignKey;
|
||||
import io.ebeaninternal.server.deploy.*;
|
||||
import io.ebeaninternal.server.deploy.meta.DeployBeanDescriptor;
|
||||
import io.ebeaninternal.server.deploy.meta.DeployBeanProperty;
|
||||
import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany;
|
||||
@@ -128,6 +126,11 @@ final class AnnotationAssocManys extends AnnotationAssoc {
|
||||
prop.getTableJoin().addJoinColumn(util, true, joinColumns, beanTable);
|
||||
}
|
||||
|
||||
IntersectionFactory intersectionFactory = get(prop, IntersectionFactory.class);
|
||||
if (intersectionFactory != null) {
|
||||
readIntersectionFactory(prop, intersectionFactory);
|
||||
}
|
||||
|
||||
JoinTable joinTable = get(prop, JoinTable.class);
|
||||
if (joinTable != null) {
|
||||
if (prop.isManyToMany()) {
|
||||
@@ -165,6 +168,12 @@ final class AnnotationAssocManys extends AnnotationAssoc {
|
||||
}
|
||||
}
|
||||
|
||||
private void readIntersectionFactory(DeployBeanPropertyAssocMany<?> prop, IntersectionFactory factory) {
|
||||
Class<?> leftSide = descriptor.getBeanType();
|
||||
Class<?> rightSide = prop.getPropertyType();
|
||||
prop.setIntersectionFactory(new IntersectionFactoryHelp(factory.value(), leftSide, rightSide, factory.factoryMethod()));
|
||||
}
|
||||
|
||||
private void checkSelfManyToMany(DeployBeanPropertyAssocMany<?> prop) {
|
||||
if (prop.getTargetType().equals(descriptor.getBeanType())) {
|
||||
throw new IllegalStateException("@ManyToMany mapping for " + prop.getFullBeanName() + " requires explicit @JoinTable with joinColumns & inverseJoinColumns. Refer issue #2157");
|
||||
|
||||
@@ -913,7 +913,7 @@ public final class DefaultPersister implements Persister {
|
||||
|
||||
private SpiSqlUpdate deleteAllIntersection(EntityBean bean, BeanPropertyAssocMany<?> many, boolean publish) {
|
||||
IntersectionRow intRow = many.buildManyToManyDeleteChildren(bean, publish);
|
||||
return intRow.createDeleteChildren(server);
|
||||
return intRow.createDeleteChildren(server, many.extraWhere());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1010,7 +1010,7 @@ public final class DefaultPersister implements Persister {
|
||||
if (targetDesc.isDeleteByStatement()) {
|
||||
// Just delete all the children with one statement
|
||||
IntersectionRow intRow = many.buildManyDeleteChildren(parentBean, excludeDetailIds);
|
||||
SqlUpdate sqlDelete = intRow.createDelete(server, deleteMode);
|
||||
SqlUpdate sqlDelete = intRow.createDelete(server, deleteMode, many.extraWhere());
|
||||
executeSqlUpdate(sqlDelete, t);
|
||||
|
||||
} else {
|
||||
|
||||
@@ -6,18 +6,10 @@ import io.ebean.bean.EntityBeanIntercept;
|
||||
import io.ebeaninternal.api.CoreLog;
|
||||
import io.ebeaninternal.api.SpiSqlUpdate;
|
||||
import io.ebeaninternal.server.core.PersistRequestBean;
|
||||
import io.ebeaninternal.server.deploy.BeanCollectionUtil;
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
import io.ebeaninternal.server.deploy.BeanProperty;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
|
||||
import io.ebeaninternal.server.deploy.IntersectionRow;
|
||||
import io.ebeaninternal.server.deploy.*;
|
||||
|
||||
import javax.persistence.PersistenceException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
import static io.ebeaninternal.server.persist.DmlUtil.isNullOrZero;
|
||||
|
||||
@@ -305,7 +297,7 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
// the object from the 'other' side of the ManyToMany
|
||||
// build a intersection row for 'delete'
|
||||
IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherDelete, publish);
|
||||
SpiSqlUpdate sqlDelete = intRow.createDelete(server, DeleteMode.HARD);
|
||||
SpiSqlUpdate sqlDelete = intRow.createDelete(server, DeleteMode.HARD, many.extraWhere());
|
||||
persister.executeOrQueue(sqlDelete, transaction, queue);
|
||||
}
|
||||
}
|
||||
@@ -322,6 +314,18 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
} else {
|
||||
if (!many.hasImportedId(otherBean)) {
|
||||
throw new PersistenceException("ManyToMany bean " + otherBean + " does not have an Id value.");
|
||||
} else if (many.getIntersectionFactory() != null) {
|
||||
// build a intersection bean for 'insert'
|
||||
// They need to be executed very late and would normally go to Queue#2, but we do not have
|
||||
// a SpiSqlUpdate for now.
|
||||
if (queue) {
|
||||
transaction.depth(+100);
|
||||
}
|
||||
Object intersectionBean = many.getIntersectionFactory().invoke(parentBean, otherBean);
|
||||
persister.saveRecurse((EntityBean) intersectionBean, transaction, parentBean, request.flags());
|
||||
if (queue) {
|
||||
transaction.depth(-100);
|
||||
}
|
||||
} else {
|
||||
// build a intersection row for 'insert'
|
||||
IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherBean, publish);
|
||||
|
||||
@@ -7,8 +7,8 @@ import javax.persistence.Id;
|
||||
import javax.persistence.ManyToOne;
|
||||
|
||||
@Entity
|
||||
@Index(unique = true, columnNames = { "from_id", "to_id" })
|
||||
@Index(unique = true, columnNames = { "to_id", "from_id" })
|
||||
@Index(unique = true, columnNames = {"from_id", "to_id"})
|
||||
@Index(unique = true, columnNames = {"to_id", "from_id"})
|
||||
public class MnyEdge {
|
||||
|
||||
@Id
|
||||
@@ -20,6 +20,22 @@ public class MnyEdge {
|
||||
@ManyToOne
|
||||
private MnyNode to;
|
||||
|
||||
|
||||
public MnyEdge() {
|
||||
// default
|
||||
}
|
||||
|
||||
public MnyEdge(Object from, Object to) {
|
||||
this.from = (MnyNode) from;
|
||||
this.to = (MnyNode) to;
|
||||
this.id = this.from.id * 10000 + this.to.id;
|
||||
this.flags = this.from.id + this.to.id;
|
||||
}
|
||||
|
||||
public static MnyEdge createReverseRelation(Object to, MnyNode from) {
|
||||
return new MnyEdge(from, to);
|
||||
}
|
||||
|
||||
private int flags;
|
||||
|
||||
public Integer getId() {
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.tests.model.m2m;
|
||||
import io.ebean.annotation.Identity;
|
||||
import io.ebean.annotation.Platform;
|
||||
import io.ebean.annotation.Where;
|
||||
import io.ebean.annotation.ext.IntersectionFactory;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.List;
|
||||
@@ -16,16 +17,19 @@ public class MnyNode {
|
||||
|
||||
String name;
|
||||
|
||||
@ManyToMany(cascade = CascadeType.REFRESH)
|
||||
@ManyToMany(cascade = CascadeType.ALL)
|
||||
@JoinTable(name = "mny_edge",
|
||||
joinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id"))
|
||||
@IntersectionFactory(MnyEdge.class)
|
||||
@Where(clause = "${mta}.flags != 12345 and '${dbTableName}' = 'mny_node'")
|
||||
List<MnyNode> allRelations;
|
||||
|
||||
@ManyToMany
|
||||
@ManyToMany(cascade = CascadeType.ALL)
|
||||
@JoinTable(name = "mny_edge",
|
||||
joinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id"))
|
||||
@IntersectionFactory(value =MnyEdge.class, factoryMethod = "createReverseRelation")
|
||||
List<MnyNode> allReverseRelations;
|
||||
|
||||
@ManyToMany
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.tests.model.m2m;
|
||||
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import io.ebean.DB;
|
||||
import io.ebean.test.LoggedSql;
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -14,27 +14,72 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* Tests M2M with complex where queries.
|
||||
*
|
||||
* @author Roland Praml, FOCONIS AG
|
||||
*
|
||||
*/
|
||||
public class TestM2MWithWhere extends BaseTestCase {
|
||||
|
||||
@Test
|
||||
public void testModify() throws Exception {
|
||||
|
||||
MnyNode node1 = new MnyNode();
|
||||
node1.setName("node1");
|
||||
node1.setId(111);
|
||||
MnyNode node2 = new MnyNode();
|
||||
node2.setName("node2");
|
||||
node2.setId(222);
|
||||
MnyNode node3 = new MnyNode();
|
||||
node3.setName("node3");
|
||||
node3.setId(333);
|
||||
MnyNode node4 = new MnyNode();
|
||||
node4.setName("node4");
|
||||
node4.setId(444);
|
||||
|
||||
node1.getAllReverseRelations().add(node2);
|
||||
node1.getAllRelations().add(node2);
|
||||
node2.getAllRelations().add(node3);
|
||||
node3.getAllRelations().add(node4);
|
||||
DB.save(node1);
|
||||
DB.save(node1);
|
||||
|
||||
DB.refresh(node2);
|
||||
DB.refresh(node3);
|
||||
assertThat(node2.getAllRelations()).containsExactlyInAnyOrder(node1, node3);
|
||||
assertThat(node3.getAllReverseRelations()).containsExactlyInAnyOrder(node2);
|
||||
|
||||
DB.refresh(node1);
|
||||
node1.getAllReverseRelations().clear();
|
||||
System.out.println("Clearing");
|
||||
DB.save(node1);
|
||||
DB.refresh(node2);
|
||||
assertThat(node2.getAllRelations()).containsExactlyInAnyOrder(node3);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAccessAndModify() throws Exception {
|
||||
createTestData();
|
||||
|
||||
MnyNode node = DB.find(MnyNode.class, 1);
|
||||
node.setName("fooBarBaz");
|
||||
MnyNode removed = node.getAllRelations().remove(0);
|
||||
|
||||
LoggedSql.start();
|
||||
DB.save(node);
|
||||
List<String> sql = LoggedSql.stop();
|
||||
sql.forEach(System.out::println);
|
||||
assertThat(sql).hasSize(3);
|
||||
assertThat(sql.get(0)).contains("update mny_node set name=? where id=?; -- bind(fooBarBaz");
|
||||
assertThat(sql.get(1)).contains("delete from mny_edge where from_id = ? and to_id = ? and mny_edge.flags != 12345 and 'mny_node' = 'mny_node'");
|
||||
assertThat(sql.get(2)).contains("-- bind");
|
||||
|
||||
node.getAllRelations().add(removed);
|
||||
LoggedSql.start();
|
||||
DB.save(node);
|
||||
sql = LoggedSql.stop();
|
||||
sql.forEach(System.out::println);
|
||||
assertThat(sql).hasSize(2);
|
||||
assertThat(sql.get(0)).contains("insert into mny_edge (id, flags, from_id, to_id) values (?,?,?,?)");
|
||||
assertThat(sql.get(1)).contains("-- bind");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQuery() throws Exception {
|
||||
createTestData();
|
||||
@@ -87,9 +132,9 @@ public class TestM2MWithWhere extends BaseTestCase {
|
||||
// prefetch everything
|
||||
LoggedSql.start();
|
||||
node = DB.find(MnyNode.class)
|
||||
.fetch("bit1Relations","*")
|
||||
.fetch("bit1ReverseRelations","*")
|
||||
.where().idEq(3).findOne();
|
||||
.fetch("bit1Relations", "*")
|
||||
.fetch("bit1ReverseRelations", "*")
|
||||
.where().idEq(3).findOne();
|
||||
sqls = LoggedSql.stop();
|
||||
assertThat(sqls).hasSize(2);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user