diff --git a/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQuery.java b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQuery.java
new file mode 100644
index 00000000000..28e9dfe5342
--- /dev/null
+++ b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQuery.java
@@ -0,0 +1,50 @@
+/*
+ * *****************************************************************************
+ * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Distribution License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * *****************************************************************************
+ */
+
+package org.eclipse.rdf4j.spring.dao.support.join;
+
+import java.util.function.Supplier;
+
+import org.eclipse.rdf4j.sparqlbuilder.core.SparqlBuilder;
+import org.eclipse.rdf4j.sparqlbuilder.core.Variable;
+import org.eclipse.rdf4j.spring.support.RDF4JTemplate;
+
+/**
+ * Creates a reusable {@link org.eclipse.rdf4j.query.TupleQuery} (and takes care of it getting reused properly using
+ * {@link RDF4JTemplate#tupleQuery(Class, String, Supplier)}).
+ *
+ *
+ * The JoinQuery is created using the {@link JoinQueryBuilder}.
+ *
+ *
+ * To set bindings and execute a {@link JoinQuery}, obtain the {@link JoinQueryEvaluationBuilder} via
+ * {@link #evaluationBuilder(RDF4JTemplate)}.
+ */
+public class JoinQuery {
+
+ public static final Variable _sourceEntity = SparqlBuilder.var("sourceEntity");
+ public static final Variable _targetEntity = SparqlBuilder.var("targetEntity");
+
+ private final String queryString;
+
+ JoinQuery(JoinQueryBuilder joinQueryBuilder) {
+ this.queryString = joinQueryBuilder.makeQueryString();
+ }
+
+ public JoinQueryEvaluationBuilder evaluationBuilder(RDF4JTemplate rdf4JTemplate) {
+ return new JoinQueryEvaluationBuilder(
+ rdf4JTemplate.tupleQuery(
+ getClass(),
+ this.getClass().getName() + "@" + this.hashCode(),
+ () -> this.queryString));
+ }
+}
diff --git a/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQueryBuilder.java b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQueryBuilder.java
new file mode 100644
index 00000000000..034a75aa673
--- /dev/null
+++ b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQueryBuilder.java
@@ -0,0 +1,189 @@
+/*
+ * *****************************************************************************
+ * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Distribution License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * *****************************************************************************
+ */
+package org.eclipse.rdf4j.spring.dao.support.join;
+
+import static org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf.iri;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.eclipse.rdf4j.model.IRI;
+import org.eclipse.rdf4j.sparqlbuilder.constraint.propertypath.builder.EmptyPropertyPathBuilder;
+import org.eclipse.rdf4j.sparqlbuilder.constraint.propertypath.builder.PropertyPathBuilder;
+import org.eclipse.rdf4j.sparqlbuilder.core.Projectable;
+import org.eclipse.rdf4j.sparqlbuilder.core.Variable;
+import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries;
+import org.eclipse.rdf4j.sparqlbuilder.graphpattern.GraphPattern;
+import org.eclipse.rdf4j.sparqlbuilder.rdf.RdfPredicate;
+import org.eclipse.rdf4j.spring.support.RDF4JTemplate;
+
+/**
+ * Builder for the {@link JoinQuery}. Allows for building the JoinQuery object directly via
+ * {@link #build(RDF4JTemplate)}, and for building a lazy initizalizer via {@link #buildLazyInitializer()}.
+ *
+ *
+ * You would use the lazy initializer like so:
+ *
+ *
+ * public class MyDao extends RDF4JDAO {
+ * // ...
+ *
+ * private static final LazyJoinQueryInitizalizer lazyJoinQuery = JoinQueryBuilder.of(SKOS.broader)
+ * // .. configure your join
+ * .buildLazyInitializer();
+ *
+ * public Map> getJoinedData(IRI sourceEntityId) {
+ * return lazyJoinQuery.get(getRdf4JTemplate())
+ * .withSourceEntityIdBinding(sourceEntityId)
+ * .buildOneToMany();
+ * }
+ *
+ * }
+ *
+ *
+ */
+public class JoinQueryBuilder {
+
+ private final RdfPredicate predicate;
+ private GraphPattern subjectConstraints = null;
+ private GraphPattern objectConstraints = null;
+ private JoinType joinType = JoinType.INNER;
+
+ private JoinQueryBuilder(Consumer propertyPathConfigurer) {
+ EmptyPropertyPathBuilder propertyPathBuilder = new EmptyPropertyPathBuilder();
+ propertyPathConfigurer.accept(propertyPathBuilder);
+ this.predicate = propertyPathBuilder.build();
+ }
+
+ private JoinQueryBuilder(RdfPredicate predicate) {
+ this.predicate = predicate;
+ }
+
+ private JoinQueryBuilder(IRI predicate) {
+ this.predicate = iri(predicate);
+ }
+
+ public static JoinQueryBuilder of(RdfPredicate rdfPredicate) {
+ return new JoinQueryBuilder(rdfPredicate);
+ }
+
+ public static JoinQueryBuilder of(IRI predicate) {
+ return new JoinQueryBuilder(predicate);
+ }
+
+ public static JoinQueryBuilder of(Consumer propertyPathConfigurer) {
+ return new JoinQueryBuilder(propertyPathConfigurer);
+ }
+
+ public static JoinQueryBuilder of(
+ RDF4JTemplate rdf4JTemplate, PropertyPathBuilder propertyPathBuilder) {
+ return new JoinQueryBuilder(() -> propertyPathBuilder.build().getQueryString());
+ }
+
+ public JoinQueryBuilder sourceEntityConstraints(
+ Function constraintBuilder) {
+ this.subjectConstraints = constraintBuilder.apply(JoinQuery._sourceEntity);
+ return this;
+ }
+
+ public JoinQueryBuilder targetEntityConstraints(
+ Function constraintBuilder) {
+ this.objectConstraints = constraintBuilder.apply(JoinQuery._targetEntity);
+ return this;
+ }
+
+ /**
+ * Return only results where the relation is present and subjectConstraints and objectConstraints are satisfied.
+ *
+ * @return
+ */
+ public JoinQueryBuilder innerJoin() {
+ this.joinType = JoinType.INNER;
+ return this;
+ }
+
+ /**
+ * Return results where subjectConstraints are satisfied. The existence of the relation is optional, but
+ * objectConstraints must be satisfied where the relation exists.
+ *
+ * @return
+ */
+ public JoinQueryBuilder leftOuterJoin() {
+ this.joinType = JoinType.LEFT_OUTER;
+ return this;
+ }
+
+ /**
+ * Return results where objectConstraints are satisfied, The existence of the relation is optional, and
+ * subjectConstraints are satisfied where the relation exists.
+ *
+ * @return
+ */
+ public JoinQueryBuilder rightOuterJoin() {
+ this.joinType = JoinType.RIGHT_OUTER;
+ return this;
+ }
+
+ public JoinQuery build() {
+ return new JoinQuery(this);
+ }
+
+ public LazyJoinQueryInitizalizer buildLazyInitializer() {
+ return new LazyJoinQueryInitizalizer(this);
+ }
+
+ String makeQueryString() {
+ return Queries.SELECT(getProjection()).where(getWhereClause()).distinct().getQueryString();
+ }
+
+ private Projectable[] getProjection() {
+ return new Projectable[] { JoinQuery._sourceEntity, JoinQuery._targetEntity };
+ }
+
+ private GraphPattern andIfPresent(GraphPattern leftOrNull, GraphPattern rightOrNull) {
+ if (rightOrNull == null) {
+ return leftOrNull;
+ }
+ if (leftOrNull == null) {
+ if (rightOrNull == null) {
+ throw new UnsupportedOperationException("left or right parameter must be non-null");
+ }
+ return rightOrNull;
+ }
+ return leftOrNull.and(rightOrNull);
+ }
+
+ private GraphPattern optionalIfPresent(GraphPattern patternOrNull) {
+ if (patternOrNull == null) {
+ return null;
+ }
+ return patternOrNull.optional();
+ }
+
+ private GraphPattern getWhereClause() {
+ GraphPattern relation = JoinQuery._sourceEntity.has(predicate, JoinQuery._targetEntity);
+ switch (this.joinType) {
+ case INNER:
+ return andIfPresent(
+ andIfPresent(relation, this.subjectConstraints), this.objectConstraints);
+ case LEFT_OUTER:
+ return andIfPresent(
+ this.subjectConstraints,
+ andIfPresent(relation, this.objectConstraints).optional());
+ case RIGHT_OUTER:
+ return andIfPresent(
+ this.objectConstraints,
+ andIfPresent(relation, this.subjectConstraints).optional());
+ }
+ throw new UnsupportedOperationException("Join type Not supported: " + this.joinType);
+ }
+}
diff --git a/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQueryEvaluationBuilder.java b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQueryEvaluationBuilder.java
new file mode 100644
index 00000000000..deb12cecddf
--- /dev/null
+++ b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQueryEvaluationBuilder.java
@@ -0,0 +1,318 @@
+/*
+ * *****************************************************************************
+ * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Distribution License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * *****************************************************************************
+ */
+
+package org.eclipse.rdf4j.spring.dao.support.join;
+
+import static org.eclipse.rdf4j.spring.util.QueryResultUtils.getIRI;
+import static org.eclipse.rdf4j.spring.util.QueryResultUtils.getIRIOptional;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.eclipse.rdf4j.model.IRI;
+import org.eclipse.rdf4j.model.Value;
+import org.eclipse.rdf4j.sparqlbuilder.core.Variable;
+import org.eclipse.rdf4j.spring.dao.support.bindingsBuilder.BindingsBuilder;
+import org.eclipse.rdf4j.spring.dao.support.opbuilder.TupleQueryEvaluationBuilder;
+import org.eclipse.rdf4j.spring.dao.support.operation.TupleQueryResultConverter;
+import org.eclipse.rdf4j.spring.support.RDF4JTemplate;
+
+/**
+ * Encapsulates all the state required for one execution of the query and provides methods for obtaining the result in
+ * different forms
+ *
+ *
+ * Obtained via {@link JoinQuery#evaluationBuilder(RDF4JTemplate)}. To use a {@link JoinQueryEvaluationBuilder}:
+ *
+ *
+ * - set its bindings
+ *
+ * - use {@link #withSourceEntityId(IRI)} and {@link #withTargetEntityId(IRI)} to set either side
+ *
- use any `withBinding` method to set other variables you may have used
+ *
+ * - obtain the results by using any of the `as` methods, such as {@link #asOneToMany()}, {@link #asOneToOne()}
+ * {@link #asIsPresent()}, etc.
+ *
+ */
+public class JoinQueryEvaluationBuilder {
+ private TupleQueryEvaluationBuilder tupleQueryEvaluationBuilder;
+ private final BindingsBuilder bindingsBuilder = new BindingsBuilder();
+
+ JoinQueryEvaluationBuilder(TupleQueryEvaluationBuilder tupleQueryEvaluationBuilder) {
+ this.tupleQueryEvaluationBuilder = tupleQueryEvaluationBuilder;
+ }
+
+ /**
+ * Builds a List of 2-component arrays, with the source entity Id in position 0 and the target entity id in position
+ * 1. Depending on the configuration of the {@link JoinQuery}, either position may be null
+ *
+ * @return a list of (source, target) id pairs.
+ */
+ public List asIdPairList() {
+ return makeTupleQueryBuilder()
+ .evaluateAndConvert()
+ .toList(
+ b -> new IRI[] {
+ getIRIOptional(b, JoinQuery._sourceEntity).orElse(null),
+ getIRIOptional(b, JoinQuery._targetEntity).orElse(null)
+ });
+ }
+
+ /** Builds a One-to-One Map using the configuration of this JoinQuery. */
+ public Map> asOneToOne() {
+ return makeTupleQueryBuilder()
+ .evaluateAndConvert()
+ .toMap(
+ b -> getIRI(b, JoinQuery._sourceEntity),
+ b -> getIRIOptional(b, JoinQuery._targetEntity));
+ }
+
+ /** Builds a One-to-Many Map using the configuration of this JoinQuery. */
+ public Map> asOneToMany() {
+ return makeTupleQueryBuilder()
+ .evaluateAndConvert()
+ .mapAndCollect(
+ Function.identity(),
+ Collectors.toMap(
+ b -> getIRI(b, JoinQuery._sourceEntity),
+ b -> getIRIOptional(b, JoinQuery._targetEntity)
+ .map(Set::of)
+ .orElseGet(Set::of),
+ JoinQueryEvaluationBuilder::mergeSets));
+ }
+
+ /**
+ * Returns only the left column of the join, i.e. all source entity ids, as a {@link Set}
+ *
+ * @return
+ */
+ public Set asSourceEntityIdSet() {
+ return makeTupleQueryBuilder()
+ .evaluateAndConvert()
+ .toStream(b -> getIRIOptional(b, JoinQuery._sourceEntity))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Returns only the right column of the join, i.e. all target entity ids, as a {@link Set}
+ *
+ * @return
+ */
+ public Set asTargetEntityIdSet() {
+ return makeTupleQueryBuilder()
+ .evaluateAndConvert()
+ .toStream(b -> getIRIOptional(b, JoinQuery._targetEntity))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Returns true if the join has no results, false otherwise.
+ *
+ * @return
+ */
+ public boolean asIsEmpty() {
+ return makeTupleQueryBuilder()
+ .evaluateAndConvert()
+ .toSingletonOfWholeResult(result -> !result.hasNext());
+ }
+
+ /**
+ * Returns true if the join has one or more results, fals otherwise.
+ *
+ * @return
+ */
+ public boolean asIsPresent() {
+ return makeTupleQueryBuilder()
+ .evaluateAndConvert()
+ .toSingletonOfWholeResult(result -> result.hasNext());
+ }
+
+ /**
+ * Returns a {@link TupleQueryResultConverter} so the client can convert the result as needed.
+ *
+ * @return
+ */
+ public TupleQueryResultConverter evaluateAndConvert() {
+ return makeTupleQueryBuilder().evaluateAndConvert();
+ }
+
+ private static Set mergeSets(Set left, Set right) {
+ Set merged = new HashSet<>(left);
+ merged.addAll(right);
+ return merged;
+ }
+
+ public JoinQueryEvaluationBuilder withSourceEntityId(IRI value) {
+ return withBinding(JoinQuery._sourceEntity, value);
+ }
+
+ public JoinQueryEvaluationBuilder withTargetEntityId(IRI value) {
+ return withBinding(JoinQuery._targetEntity, value);
+ }
+
+ public JoinQueryEvaluationBuilder withObjectBinding(IRI value) {
+ return withBinding(JoinQuery._sourceEntity, value);
+ }
+
+ public JoinQueryEvaluationBuilder withSubjectBindingMaybe(IRI value) {
+ return withBindingMaybe(JoinQuery._sourceEntity, value);
+ }
+
+ public JoinQueryEvaluationBuilder withBinding(Variable key, Value value) {
+ bindingsBuilder.add(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBinding(String key, Value value) {
+ bindingsBuilder.add(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBindingMaybe(Variable key, Value value) {
+ bindingsBuilder.addMaybe(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBindingMaybe(String key, Value value) {
+ bindingsBuilder.addMaybe(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBinding(Variable key, IRI value) {
+ bindingsBuilder.add(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBinding(String key, IRI value) {
+ bindingsBuilder.add(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBindingMaybe(Variable key, IRI value) {
+ bindingsBuilder.addMaybe(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBindingMaybe(Variable key, String value) {
+ bindingsBuilder.addMaybe(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBindingMaybe(String key, IRI value) {
+ bindingsBuilder.addMaybe(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBinding(Variable key, String value) {
+ bindingsBuilder.add(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBinding(String key, String value) {
+ bindingsBuilder.add(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBindingMaybe(String key, String value) {
+ bindingsBuilder.addMaybe(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBinding(Variable key, Integer value) {
+ bindingsBuilder.add(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBinding(String key, Integer value) {
+ bindingsBuilder.add(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBindingMaybe(Variable key, Integer value) {
+ bindingsBuilder.addMaybe(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBindingMaybe(String key, Integer value) {
+ bindingsBuilder.addMaybe(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBinding(Variable key, Boolean value) {
+ bindingsBuilder.add(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBinding(String key, Boolean value) {
+ bindingsBuilder.add(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBindingMaybe(Variable key, Boolean value) {
+ bindingsBuilder.addMaybe(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBindingMaybe(String key, Boolean value) {
+ bindingsBuilder.addMaybe(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBinding(Variable key, Float value) {
+ bindingsBuilder.add(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBinding(String key, Float value) {
+ bindingsBuilder.add(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBindingMaybe(Variable key, Float value) {
+ bindingsBuilder.addMaybe(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBindingMaybe(String key, Float value) {
+ bindingsBuilder.addMaybe(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBinding(Variable key, Double value) {
+ bindingsBuilder.add(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBinding(String key, Double value) {
+ bindingsBuilder.add(key, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBindingMaybe(Variable var, Double value) {
+ bindingsBuilder.addMaybe(var, value);
+ return this;
+ }
+
+ public JoinQueryEvaluationBuilder withBindingMaybe(String key, Double value) {
+ bindingsBuilder.addMaybe(key, value);
+ return this;
+ }
+
+ private TupleQueryEvaluationBuilder makeTupleQueryBuilder() {
+ return this.tupleQueryEvaluationBuilder.withBindings(bindingsBuilder.build());
+ }
+}
diff --git a/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinType.java b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinType.java
new file mode 100644
index 00000000000..39a6d4f9c14
--- /dev/null
+++ b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinType.java
@@ -0,0 +1,18 @@
+/*
+ * *****************************************************************************
+ * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Distribution License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * *****************************************************************************
+ */
+package org.eclipse.rdf4j.spring.dao.support.join;
+
+public enum JoinType {
+ INNER,
+ LEFT_OUTER,
+ RIGHT_OUTER;
+}
diff --git a/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/LazyJoinQueryInitizalizer.java b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/LazyJoinQueryInitizalizer.java
new file mode 100644
index 00000000000..9f1edc71468
--- /dev/null
+++ b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/LazyJoinQueryInitizalizer.java
@@ -0,0 +1,49 @@
+/*
+ * *****************************************************************************
+ * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Distribution License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * *****************************************************************************
+ */
+package org.eclipse.rdf4j.spring.dao.support.join;
+
+import org.eclipse.rdf4j.spring.support.RDF4JTemplate;
+
+/**
+ * Holds a fully configured {@link JoinQueryBuilder} until the time comes at which a {@link RDF4JTemplate} is available
+ * and the {@link JoinQuery} can be instantiated via {@link #get(RDF4JTemplate)}. Subsequent calls to
+ * {@link #get(RDF4JTemplate)} will always use the same {@link JoinQuery} object.
+ *
+ *
+ * This construct is thread-safe because the JoinQuery's internal state does not change after initialization; rather,
+ * when used to perform actual queries, it re-uses or creates a reusable TupleQuery using the {@link RDF4JTemplate}, and
+ * uses a {@link JoinQueryEvaluationBuilder} object to encapsulate per-evaluation state.
+ *
+ *
+ * Usually, you would assign this to a static member of a class and obtain the value in an instance method.
+ */
+public class LazyJoinQueryInitizalizer {
+ private JoinQueryBuilder joinQueryBuilder;
+ private JoinQuery joinQuery;
+
+ LazyJoinQueryInitizalizer(JoinQueryBuilder joinQueryBuilder) {
+ this.joinQueryBuilder = joinQueryBuilder;
+ }
+
+ public JoinQueryEvaluationBuilder get(RDF4JTemplate rdf4JTemplate) {
+ if (joinQuery != null) {
+ return joinQuery.evaluationBuilder(rdf4JTemplate);
+ } else {
+ synchronized (this) {
+ if (this.joinQuery == null) {
+ this.joinQuery = this.joinQueryBuilder.build();
+ }
+ }
+ }
+ return joinQuery.evaluationBuilder(rdf4JTemplate);
+ }
+}
diff --git a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/dao/support/ServiceLayerTests.java b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/dao/support/ServiceLayerTests.java
index 9cfc3b007d3..69813c31051 100644
--- a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/dao/support/ServiceLayerTests.java
+++ b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/dao/support/ServiceLayerTests.java
@@ -17,8 +17,12 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.util.Set;
+
+import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.spring.RDF4JSpringTestBase;
import org.eclipse.rdf4j.spring.domain.model.Artist;
+import org.eclipse.rdf4j.spring.domain.model.EX;
import org.eclipse.rdf4j.spring.domain.model.Painting;
import org.eclipse.rdf4j.spring.domain.service.ArtService;
import org.eclipse.rdf4j.spring.support.RDF4JTemplate;
@@ -30,6 +34,8 @@
import org.springframework.transaction.support.DefaultTransactionStatus;
import org.springframework.transaction.support.TransactionTemplate;
+import shaded_package.org.bouncycastle.asn1.tsp.ArchiveTimeStamp;
+
/**
* @author Florian Kleedorfer
* @since 4.0.0
@@ -97,4 +103,26 @@ public void testRollbackOnException() {
return null;
});
}
+
+ @Test
+ public void testGetPaintingsOfArtist() {
+ transactionTemplate.execute(status -> {
+ Set paintings = artService.getPaintingsOfArtist(EX.VanGogh);
+ assertEquals(3, paintings.size());
+ assertTrue(paintings.stream().anyMatch(p -> p.getId().equals(EX.starryNight)));
+ assertTrue(paintings.stream().anyMatch(p -> p.getId().equals(EX.potatoEaters)));
+ assertTrue(paintings.stream().anyMatch(p -> p.getId().equals(EX.sunflowers)));
+ return null;
+ });
+ }
+
+ @Test
+ public void testGetArtistOfPainting() {
+ transactionTemplate.execute(status -> {
+ Set artists = artService.getArtistsOfPainting(EX.guernica);
+ assertEquals(1, artists.size());
+ assertTrue(artists.stream().anyMatch(p -> p.getId().equals(EX.Picasso)));
+ return null;
+ });
+ }
}
diff --git a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/ArtistDao.java b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/ArtistDao.java
index bbe68a2738d..cfa2e2cd2c2 100644
--- a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/ArtistDao.java
+++ b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/ArtistDao.java
@@ -16,12 +16,17 @@
import static org.eclipse.rdf4j.spring.domain.model.Artist.ARTIST_ID;
import static org.eclipse.rdf4j.spring.domain.model.Artist.ARTIST_LAST_NAME;
+import java.util.Map;
+import java.util.Set;
+
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.vocabulary.FOAF;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries;
import org.eclipse.rdf4j.spring.dao.SimpleRDF4JCRUDDao;
import org.eclipse.rdf4j.spring.dao.support.bindingsBuilder.MutableBindings;
+import org.eclipse.rdf4j.spring.dao.support.join.JoinQueryBuilder;
+import org.eclipse.rdf4j.spring.dao.support.join.LazyJoinQueryInitizalizer;
import org.eclipse.rdf4j.spring.dao.support.sparql.NamedSparqlSupplier;
import org.eclipse.rdf4j.spring.domain.model.Artist;
import org.eclipse.rdf4j.spring.domain.model.EX;
@@ -40,6 +45,18 @@ public ArtistDao(RDF4JTemplate rdf4JTemplate) {
super(rdf4JTemplate);
}
+ private static final LazyJoinQueryInitizalizer getPaintingsIdsOfArtistQuery = JoinQueryBuilder.of(EX.creatorOf)
+ .sourceEntityConstraints(artist -> artist.isA(EX.Artist))
+ .targetEntityConstraints(painting -> painting.isA(EX.Painting))
+ .leftOuterJoin()
+ .buildLazyInitializer();
+
+ public Set getPaintingsIdsOfArtist(IRI artistId) {
+ return getPaintingsIdsOfArtistQuery.get(getRdf4JTemplate())
+ .withSourceEntityId(artistId)
+ .asTargetEntityIdSet();
+ }
+
@Override
protected void populateIdBindings(MutableBindings bindingsBuilder, IRI iri) {
bindingsBuilder.add(ARTIST_ID, iri);
diff --git a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/PaintingDao.java b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/PaintingDao.java
index 0cd1bd72034..5d3e70b3a9e 100644
--- a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/PaintingDao.java
+++ b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/PaintingDao.java
@@ -17,12 +17,16 @@
import static org.eclipse.rdf4j.spring.domain.model.Painting.PAINTING_LABEL;
import static org.eclipse.rdf4j.spring.domain.model.Painting.PAINTING_TECHNIQUE;
+import java.util.Set;
+
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.vocabulary.RDFS;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries;
import org.eclipse.rdf4j.spring.dao.SimpleRDF4JCRUDDao;
import org.eclipse.rdf4j.spring.dao.support.bindingsBuilder.MutableBindings;
+import org.eclipse.rdf4j.spring.dao.support.join.JoinQueryBuilder;
+import org.eclipse.rdf4j.spring.dao.support.join.LazyJoinQueryInitizalizer;
import org.eclipse.rdf4j.spring.dao.support.sparql.NamedSparqlSupplier;
import org.eclipse.rdf4j.spring.domain.model.EX;
import org.eclipse.rdf4j.spring.domain.model.Painting;
@@ -41,6 +45,19 @@ public PaintingDao(RDF4JTemplate rdf4JTemplate) {
super(rdf4JTemplate);
}
+ private static final LazyJoinQueryInitizalizer getArtistIdsOfPaintingQuery = JoinQueryBuilder
+ .of(p -> p.pred(EX.creatorOf).inv())
+ .sourceEntityConstraints(artist -> artist.isA(EX.Painting))
+ .targetEntityConstraints(painting -> painting.isA(EX.Artist))
+ .leftOuterJoin()
+ .buildLazyInitializer();
+
+ public Set getArtistIdsOfPainting(IRI paintingId) {
+ return getArtistIdsOfPaintingQuery.get(getRdf4JTemplate())
+ .withSourceEntityId(paintingId)
+ .asTargetEntityIdSet();
+ }
+
@Override
protected void populateIdBindings(MutableBindings bindingsBuilder, IRI iri) {
bindingsBuilder.add(PAINTING_ID, iri);
diff --git a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/service/ArtService.java b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/service/ArtService.java
index 6dd563489c5..a91836cb7b3 100644
--- a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/service/ArtService.java
+++ b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/service/ArtService.java
@@ -11,6 +11,9 @@
package org.eclipse.rdf4j.spring.domain.service;
+import java.util.Set;
+import java.util.stream.Collectors;
+
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.spring.domain.dao.ArtistDao;
import org.eclipse.rdf4j.spring.domain.dao.PaintingDao;
@@ -46,4 +49,15 @@ public Painting createPainting(String title, String technique, IRI artist) {
return paintingDao.save(painting);
}
+ @Transactional(propagation = Propagation.REQUIRED)
+ public Set getPaintingsOfArtist(IRI artistId) {
+ Set paintingIds = artistDao.getPaintingsIdsOfArtist(artistId);
+ return paintingIds.stream().map(paintingDao::getById).collect(Collectors.toSet());
+ }
+
+ @Transactional(propagation = Propagation.REQUIRED)
+ public Set getArtistsOfPainting(IRI paintingId) {
+ Set artistIds = paintingDao.getArtistIdsOfPainting(paintingId);
+ return artistIds.stream().map(artistDao::getById).collect(Collectors.toSet());
+ }
}