GH-1491: index query methods and show them as child nodes for repository document symbols, incl. query strings when defined
This commit is contained in:
@@ -19,6 +19,7 @@ import java.util.Set;
|
||||
* @author Kris De Volder
|
||||
*/
|
||||
public class Annotations {
|
||||
|
||||
public static final String BEAN = "org.springframework.context.annotation.Bean";
|
||||
public static final String PROFILE = "org.springframework.context.annotation.Profile";
|
||||
public static final String CONDITIONAL = "org.springframework.context.annotation.Conditional";
|
||||
@@ -40,7 +41,9 @@ public class Annotations {
|
||||
public static final String JPA_JAVAX_ID_CLASS = "javax.persistence.IdClass";
|
||||
public static final String JPA_JAKARTA_NAMED_QUERY = "jakarta.persistence.NamedQuery";
|
||||
public static final String JPA_JAVAX_NAMED_QUERY = "javax.persistence.NamedQuery";
|
||||
public static final String DATA_QUERY = "org.springframework.data.jpa.repository.Query";
|
||||
|
||||
public static final String DATA_QUERY_META_ANNOTATION = "org.springframework.data.annotation.QueryAnnotation";
|
||||
public static final String DATA_JPA_QUERY = "org.springframework.data.jpa.repository.Query";
|
||||
|
||||
public static final String AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired";
|
||||
public static final String QUALIFIER = "org.springframework.beans.factory.annotation.Qualifier";
|
||||
|
||||
@@ -16,15 +16,25 @@ import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.core.dom.Annotation;
|
||||
import org.eclipse.jdt.core.dom.ITypeBinding;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.Modifier;
|
||||
import org.eclipse.jdt.core.dom.NormalAnnotation;
|
||||
import org.eclipse.jdt.core.dom.SimpleName;
|
||||
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import org.eclipse.lsp4j.Location;
|
||||
import org.eclipse.lsp4j.Range;
|
||||
import org.eclipse.lsp4j.SymbolKind;
|
||||
import org.eclipse.lsp4j.WorkspaceSymbol;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ide.vscode.boot.java.Annotations;
|
||||
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
|
||||
import org.springframework.ide.vscode.boot.java.beans.BeanUtils;
|
||||
import org.springframework.ide.vscode.boot.java.beans.CachedBean;
|
||||
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils;
|
||||
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils.EmbeddedQueryExpression;
|
||||
import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider;
|
||||
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
|
||||
import org.springframework.ide.vscode.boot.java.utils.CachedSymbol;
|
||||
@@ -62,6 +72,9 @@ public class DataRepositorySymbolProvider implements SymbolProvider {
|
||||
SymbolKind.Interface,
|
||||
Either.forLeft(location));
|
||||
|
||||
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
|
||||
|
||||
// index elements
|
||||
InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(typeDeclaration, doc);
|
||||
|
||||
ITypeBinding concreteBeanTypeBindung = typeDeclaration.resolveBinding();
|
||||
@@ -75,8 +88,8 @@ public class DataRepositorySymbolProvider implements SymbolProvider {
|
||||
AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(annotationsOnMethod, doc);
|
||||
|
||||
Bean beanDefinition = new Bean(beanName, concreteRepoType, location, injectionPoints, supertypes, annotations, false, symbol.getName());
|
||||
indexQueryMethods(beanDefinition, typeDeclaration, context, doc);
|
||||
|
||||
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
|
||||
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
|
||||
|
||||
} catch (BadLocationException e) {
|
||||
@@ -85,6 +98,59 @@ public class DataRepositorySymbolProvider implements SymbolProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private void indexQueryMethods(Bean beanDefinition, TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) {
|
||||
MethodDeclaration[] methods = typeDeclaration.getMethods();
|
||||
if (methods == null) return;
|
||||
|
||||
for (MethodDeclaration method : methods) {
|
||||
int modifiers = method.getModifiers();
|
||||
SimpleName nameNode = method.getName();
|
||||
|
||||
if (nameNode != null && (modifiers & Modifier.DEFAULT) == 0) {
|
||||
String methodName = nameNode.getFullyQualifiedName();
|
||||
DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, method);
|
||||
|
||||
try {
|
||||
Range range = doc.toRange(nodeRegion);
|
||||
|
||||
if (methodName != null) {
|
||||
String queryString = identifyQueryString(method);
|
||||
beanDefinition.addChild(new QueryMethodIndexElement(methodName, queryString, range));
|
||||
}
|
||||
|
||||
} catch (BadLocationException e) {
|
||||
log.error("query method range computation failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String identifyQueryString(MethodDeclaration method) {
|
||||
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(method);
|
||||
|
||||
EmbeddedQueryExpression queryExpression = null;
|
||||
|
||||
Collection<Annotation> annotations = ASTUtils.getAnnotations(method);
|
||||
for (Annotation annotation : annotations) {
|
||||
ITypeBinding typeBinding = annotation.resolveTypeBinding();
|
||||
|
||||
if (typeBinding != null && annotationHierarchies.isAnnotatedWith(typeBinding, Annotations.DATA_QUERY_META_ANNOTATION)) {
|
||||
if (annotation instanceof SingleMemberAnnotation) {
|
||||
queryExpression = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, (SingleMemberAnnotation)annotation);
|
||||
}
|
||||
else if (annotation instanceof NormalAnnotation) {
|
||||
queryExpression = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, (NormalAnnotation)annotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (queryExpression != null) {
|
||||
return queryExpression.query().getText();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String beanLabel(boolean isFunctionBean, String beanName, String beanType, String markerString) {
|
||||
StringBuilder symbolLabel = new StringBuilder();
|
||||
symbolLabel.append("@+");
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2025 Broadcom
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* https://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* Broadcom - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.boot.java.data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.lsp4j.DocumentSymbol;
|
||||
import org.eclipse.lsp4j.Range;
|
||||
import org.eclipse.lsp4j.SymbolKind;
|
||||
import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement;
|
||||
import org.springframework.ide.vscode.commons.protocol.spring.SymbolElement;
|
||||
|
||||
public class QueryMethodIndexElement extends AbstractSpringIndexElement implements SymbolElement {
|
||||
|
||||
private final String methodName;
|
||||
private final String queryString;
|
||||
private final Range range;
|
||||
|
||||
public QueryMethodIndexElement(String methodName, String queryString, Range range) {
|
||||
this.methodName = methodName;
|
||||
this.queryString = queryString;
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
public String getMethodName() {
|
||||
return methodName;
|
||||
}
|
||||
|
||||
public String getQueryString() {
|
||||
return queryString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentSymbol getDocumentSymbol() {
|
||||
DocumentSymbol symbol = new DocumentSymbol();
|
||||
|
||||
symbol.setName(methodName);
|
||||
symbol.setKind(SymbolKind.Method);
|
||||
symbol.setRange(range);
|
||||
symbol.setSelectionRange(range);
|
||||
|
||||
if (queryString != null) {
|
||||
DocumentSymbol querySymbol = new DocumentSymbol();
|
||||
querySymbol.setName(queryString);
|
||||
querySymbol.setKind(SymbolKind.Constant);
|
||||
querySymbol.setRange(range);
|
||||
querySymbol.setSelectionRange(range);
|
||||
|
||||
symbol.setChildren(List.of(querySymbol));
|
||||
}
|
||||
|
||||
return symbol;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -102,8 +102,8 @@ public class JdtQueryVisitorUtils {
|
||||
}
|
||||
|
||||
static boolean isQueryAnnotation(AnnotationHierarchies annotationHierarchies, Annotation a) {
|
||||
if (Annotations.DATA_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || QUERY.equals(a.getTypeName().getFullyQualifiedName())) {
|
||||
return annotationHierarchies.isAnnotatedWith(a.resolveAnnotationBinding(), Annotations.DATA_QUERY);
|
||||
if (Annotations.DATA_JPA_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || QUERY.equals(a.getTypeName().getFullyQualifiedName())) {
|
||||
return annotationHierarchies.isAnnotatedWith(a.resolveAnnotationBinding(), Annotations.DATA_JPA_QUERY);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2025 Broadcom
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* https://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* Broadcom - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.boot.java.data.test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.eclipse.lsp4j.TextDocumentIdentifier;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
|
||||
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
|
||||
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
|
||||
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
|
||||
import org.springframework.ide.vscode.boot.java.data.QueryMethodIndexElement;
|
||||
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
|
||||
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
|
||||
import org.springframework.ide.vscode.commons.protocol.spring.DocumentElement;
|
||||
import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement;
|
||||
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
|
||||
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
/**
|
||||
* @author Martin Lippert
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@BootLanguageServerTest
|
||||
@Import(SymbolProviderTestConf.class)
|
||||
public class DataRepositoryIndexElementsTest {
|
||||
|
||||
@Autowired private BootLanguageServerHarness harness;
|
||||
@Autowired private JavaProjectFinder projectFinder;
|
||||
@Autowired private SpringSymbolIndex indexer;
|
||||
@Autowired private SpringMetamodelIndex springIndex;
|
||||
|
||||
private File directory;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
harness.intialize(null);
|
||||
|
||||
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-data-symbols/").toURI());
|
||||
String projectDir = directory.toURI().toString();
|
||||
|
||||
// trigger project creation
|
||||
projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
|
||||
|
||||
CompletableFuture<Void> initProject = indexer.waitOperation();
|
||||
initProject.get(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimpleRepositoryElements() throws Exception {
|
||||
String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepository.java").toUri().toString();
|
||||
|
||||
DocumentElement document = springIndex.getDocument(docUri);
|
||||
List<SpringIndexElement> children = document.getChildren();
|
||||
Bean repositoryElement = (Bean) children.get(0);
|
||||
assertEquals("customerRepository", repositoryElement.getName());
|
||||
assertEquals(1, children.size());
|
||||
|
||||
Bean[] repoBean = this.springIndex.getBeansWithName("test-spring-data-symbols", "customerRepository");
|
||||
assertEquals(1, repoBean.length);
|
||||
assertEquals("customerRepository", repoBean[0].getName());
|
||||
assertEquals("org.test.CustomerRepository", repoBean[0].getType());
|
||||
|
||||
Bean[] matchingBeans = springIndex.getMatchingBeans("test-spring-data-symbols", "org.springframework.data.repository.CrudRepository");
|
||||
assertEquals(3, matchingBeans.length);
|
||||
ArrayUtils.contains(matchingBeans, repoBean[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimpleQueryMethodElements() throws Exception {
|
||||
String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepository.java").toUri().toString();
|
||||
|
||||
DocumentElement document = springIndex.getDocument(docUri);
|
||||
List<SpringIndexElement> children = document.getChildren();
|
||||
Bean repositoryElement = (Bean) children.get(0);
|
||||
|
||||
List<SpringIndexElement> queryMethods = repositoryElement.getChildren();
|
||||
assertEquals(1, queryMethods.size());
|
||||
|
||||
QueryMethodIndexElement queryMethod = (QueryMethodIndexElement) queryMethods.get(0);
|
||||
assertEquals("findByLastName", queryMethod.getMethodName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testQueryMethodElementWithQueryString() throws Exception {
|
||||
String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepositoryWithQuery.java").toUri().toString();
|
||||
|
||||
DocumentElement document = springIndex.getDocument(docUri);
|
||||
List<SpringIndexElement> children = document.getChildren();
|
||||
Bean repositoryElement = (Bean) children.get(0);
|
||||
|
||||
List<SpringIndexElement> queryMethods = repositoryElement.getChildren();
|
||||
assertEquals(1, queryMethods.size());
|
||||
|
||||
QueryMethodIndexElement queryMethod = (QueryMethodIndexElement) queryMethods.get(0);
|
||||
assertEquals("findPetTypes", queryMethod.getMethodName());
|
||||
assertEquals("SELECT ptype FROM PetType ptype ORDER BY ptype.name", queryMethod.getQueryString());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,7 +19,8 @@ import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.eclipse.lsp4j.DocumentSymbol;
|
||||
import org.eclipse.lsp4j.SymbolKind;
|
||||
import org.eclipse.lsp4j.TextDocumentIdentifier;
|
||||
import org.eclipse.lsp4j.WorkspaceSymbol;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -30,9 +31,7 @@ import org.springframework.context.annotation.Import;
|
||||
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
|
||||
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
|
||||
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
|
||||
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
|
||||
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
|
||||
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
|
||||
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
|
||||
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
@@ -48,7 +47,6 @@ public class DataRepositorySymbolProviderTest {
|
||||
@Autowired private BootLanguageServerHarness harness;
|
||||
@Autowired private JavaProjectFinder projectFinder;
|
||||
@Autowired private SpringSymbolIndex indexer;
|
||||
@Autowired private SpringMetamodelIndex springIndex;
|
||||
|
||||
private File directory;
|
||||
|
||||
@@ -72,15 +70,40 @@ public class DataRepositorySymbolProviderTest {
|
||||
List<? extends WorkspaceSymbol> symbols = indexer.getSymbols(docUri);
|
||||
assertEquals(1, symbols.size());
|
||||
assertTrue(containsSymbol(symbols, "@+ 'customerRepository' (Customer) Repository<Customer,Long>", docUri, 6, 17, 6, 35));
|
||||
}
|
||||
|
||||
Bean[] repoBean = this.springIndex.getBeansWithName("test-spring-data-symbols", "customerRepository");
|
||||
assertEquals(1, repoBean.length);
|
||||
assertEquals("customerRepository", repoBean[0].getName());
|
||||
assertEquals("org.test.CustomerRepository", repoBean[0].getType());
|
||||
@Test
|
||||
void testDocumentSymbolsForRepository() throws Exception {
|
||||
String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepository.java").toUri().toString();
|
||||
List<? extends DocumentSymbol> symbols = indexer.getDocumentSymbolsFromMetamodelIndex(docUri);
|
||||
assertEquals(1, symbols.size());
|
||||
assertTrue(containsDocumentSymbol(symbols, "@+ 'customerRepository' (Customer) Repository<Customer,Long>", docUri, 6, 17, 6, 35));
|
||||
|
||||
Bean[] matchingBeans = springIndex.getMatchingBeans("test-spring-data-symbols", "org.springframework.data.repository.CrudRepository");
|
||||
assertEquals(2, matchingBeans.length);
|
||||
ArrayUtils.contains(matchingBeans, repoBean[0]);
|
||||
DocumentSymbol documentSymbol = symbols.get(0);
|
||||
List<DocumentSymbol> children = documentSymbol.getChildren();
|
||||
DocumentSymbol childSymbol = children.get(0);
|
||||
assertEquals("findByLastName", childSymbol.getName());
|
||||
|
||||
assertEquals(1, children.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNestedDocumentSymbolsForRepositoryWithQuery() throws Exception {
|
||||
String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepositoryWithQuery.java").toUri().toString();
|
||||
List<? extends DocumentSymbol> symbols = indexer.getDocumentSymbolsFromMetamodelIndex(docUri);
|
||||
assertEquals(1, symbols.size());
|
||||
assertTrue(containsDocumentSymbol(symbols, "@+ 'customerRepositoryWithQuery' (Customer) Repository<Customer,Long>", docUri, 7, 17, 7, 44));
|
||||
|
||||
DocumentSymbol documentSymbol = symbols.get(0);
|
||||
List<DocumentSymbol> children = documentSymbol.getChildren();
|
||||
DocumentSymbol queryMethodSymbol = children.get(0);
|
||||
assertEquals("findPetTypes", queryMethodSymbol.getName());
|
||||
assertEquals(1, children.size());
|
||||
|
||||
List<DocumentSymbol> queryChildren = queryMethodSymbol.getChildren();
|
||||
DocumentSymbol queryStringSymbol = queryChildren.get(0);
|
||||
assertEquals("SELECT ptype FROM PetType ptype ORDER BY ptype.name", queryStringSymbol.getName());
|
||||
assertEquals(SymbolKind.Constant, queryStringSymbol.getKind());
|
||||
}
|
||||
|
||||
private boolean containsSymbol(List<? extends WorkspaceSymbol> symbols, String name, String uri, int startLine, int startCHaracter, int endLine, int endCharacter) {
|
||||
@@ -100,4 +123,20 @@ public class DataRepositorySymbolProviderTest {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean containsDocumentSymbol(List<? extends DocumentSymbol> symbols, String name, String uri, int startLine, int startCHaracter, int endLine, int endCharacter) {
|
||||
for (Iterator<? extends DocumentSymbol> iterator = symbols.iterator(); iterator.hasNext();) {
|
||||
DocumentSymbol symbol = iterator.next();
|
||||
|
||||
if (symbol.getName().equals(name)
|
||||
&& symbol.getRange().getStart().getLine() == startLine
|
||||
&& symbol.getRange().getStart().getCharacter() == startCHaracter
|
||||
&& symbol.getRange().getEnd().getLine() == endLine
|
||||
&& symbol.getRange().getEnd().getCharacter() == endCharacter) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,4 +7,7 @@ import org.springframework.data.repository.CrudRepository;
|
||||
public interface CustomerRepository extends CrudRepository<Customer, Long> {
|
||||
|
||||
List<Customer> findByLastName(String lastName);
|
||||
|
||||
default List<Customer> findByWhatever(String lastName) {return null;}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
public interface CustomerRepositoryWithQuery extends CrudRepository<Customer, Long> {
|
||||
|
||||
@Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name")
|
||||
List<Object> findPetTypes();
|
||||
}
|
||||
Reference in New Issue
Block a user