BAEL-6129: SpEL in @Query (#15449)

* BAEL-6129: SpEL in @Query

* BAEL-6129: Fix Naming

* BAEL-6129: Test Cleanup
This commit is contained in:
Eugene Kovko 2023-12-21 05:53:57 +01:00 committed by GitHub
parent 390b2324bb
commit 22c187747e
13 changed files with 583 additions and 2 deletions

View File

@ -8,8 +8,20 @@
<properties>
<javafaker.version>0.15</javafaker.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>9</source>
<target>9</target>
</configuration>
</plugin>
</plugins>
</build>
<parent>
<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-2</artifactId>
<version>0.0.1-SNAPSHOT</version>
@ -34,12 +46,16 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,23 @@
package com.baeldung.spring.data.jpa.spel;
import java.util.Locale;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
@SpringBootApplication
public class NewsApplication {
public static void main(String[] args) {
SpringApplication.run(NewsApplication.class, args);
}
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.US);
return slr;
}
}

View File

@ -0,0 +1,17 @@
package com.baeldung.spring.data.jpa.spel.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("locale");
registry.addInterceptor(localeChangeInterceptor);
}
}

View File

@ -0,0 +1,26 @@
package com.baeldung.spring.data.jpa.spel.controller;
import com.baeldung.spring.data.jpa.spel.entity.Article;
import com.baeldung.spring.data.jpa.spel.repository.ArticleRepository;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/articles")
public class ArticleController {
private final ArticleRepository articleRepository;
@Autowired
public ArticleController(final ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
@GetMapping
List<Article> getAllArticlesWithNativeQuery() {
return articleRepository.findAllArticlesUsingLocaleWithNativeQuery();
}
}

View File

@ -0,0 +1,111 @@
package com.baeldung.spring.data.jpa.spel.entity;
import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity(name = "articles")
@Table(name = "articles")
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String title;
private String content;
private String language;
public Article() {
}
public Article(final String title, final String content, final String language) {
this.title = title;
this.content = content;
this.language = language;
}
public Article(final Long id, final String title, final String content, final String language) {
this.id = id;
this.title = title;
this.content = content;
this.language = language;
}
public Long getId() {
return id;
}
public void setId(final Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(final String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(final String content) {
this.content = content;
}
public String getLanguage() {
return language;
}
public void setLanguage(final String language) {
this.language = language;
}
@Override
public String toString() {
return "News{" +
"id=" + id +
", title='" + title + '\'' +
", content='" + content + '\'' +
", language=" + language +
'}';
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final Article article = (Article) o;
if (!Objects.equals(id, article.id)) {
return false;
}
if (!Objects.equals(title, article.title)) {
return false;
}
if (!Objects.equals(content, article.content)) {
return false;
}
return Objects.equals(language, article.language);
}
@Override
public int hashCode() {
int result = id != null ? id.hashCode() : 0;
result = 31 * result + (title != null ? title.hashCode() : 0);
result = 31 * result + (content != null ? content.hashCode() : 0);
result = 31 * result + (language != null ? language.hashCode() : 0);
return result;
}
}

View File

@ -0,0 +1,14 @@
package com.baeldung.spring.data.jpa.spel.entity;
public class ArticleWrapper {
private final Article article;
public ArticleWrapper(Article article) {
this.article = article;
}
public Article getArticle() {
return article;
}
}

View File

@ -0,0 +1,56 @@
package com.baeldung.spring.data.jpa.spel.entity;
import java.util.Objects;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Language {
@Id
@Column(name = "iso_code")
private String isoCode;
public Language() {
}
public Language(final String isoCode) {
this.isoCode = isoCode;
}
public String getIsoCode() {
return isoCode;
}
public void setIsoCode(final String isoCode) {
this.isoCode = isoCode;
}
@Override
public String toString() {
return "Language{" +
"isoCode='" + isoCode + '\'' +
'}';
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final Language language = (Language) o;
return Objects.equals(isoCode, language.isoCode);
}
@Override
public int hashCode() {
return isoCode != null ? isoCode.hashCode() : 0;
}
}

View File

@ -0,0 +1,20 @@
package com.baeldung.spring.data.jpa.spel.extension;
import java.util.Locale;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.stereotype.Component;
@Component
public class LocaleContextHolderExtension implements EvaluationContextExtension {
@Override
public String getExtensionId() {
return "locale";
}
@Override
public Locale getRootObject() {
return LocaleContextHolder.getLocale();
}
}

View File

@ -0,0 +1,78 @@
package com.baeldung.spring.data.jpa.spel.repository;
import com.baeldung.spring.data.jpa.spel.entity.Article;
import com.baeldung.spring.data.jpa.spel.entity.ArticleWrapper;
import java.util.List;
import javax.transaction.Transactional;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface ArticleRepository extends BaseNewsApplicationRepository<Article, Long> {
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (?1, ?2, ?3, ?4)",
nativeQuery = true)
void saveWithPositionalArguments(Long id, String title, String content, String language);
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (?#{[0]}, ?#{[1]}, ?#{[2]}, ?#{[3]})",
nativeQuery = true)
void saveWithPositionalSpELArguments(long id, String title, String content, String language);
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (?#{[0]}, ?#{[1]}, ?#{[2] ?: 'Empty Article'}, ?#{[3]})",
nativeQuery = true)
void saveWithPositionalSpELArgumentsWithEmptyCheck(long id, String title, String content, String language);
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (:id, :title, :content, :language)",
nativeQuery = true)
void saveWithNamedArguments(@Param("id") long id, @Param("title") String title,
@Param("content") String content, @Param("language") String language);
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (:#{#id}, :#{#title}, :#{#content}, :#{#language})",
nativeQuery = true)
void saveWithNamedSpELArguments(@Param("id") long id, @Param("title") String title,
@Param("content") String content, @Param("language") String language);
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (:#{#id}, :#{#title}, :#{#content}, :#{#language.toLowerCase()})",
nativeQuery = true)
void saveWithNamedSpELArgumentsAndLowerCaseLanguage(@Param("id") long id, @Param("title") String title,
@Param("content") String content, @Param("language") String language);
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (:#{#article.id}, :#{#article.title}, :#{#article.content}, :#{#article.language})",
nativeQuery = true)
void saveWithSingleObjectSpELArgument(@Param("article") Article article);
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (:#{#wrapper.article.id}, :#{#wrapper.article.title}, :#{#wrapper.article.content}, :#{#wrapper.article.language})",
nativeQuery = true)
void saveWithSingleWrappedObjectSpELArgument(@Param("wrapper") ArticleWrapper articleWrapper);
@Query(value = "SELECT * FROM articles WHERE language = :#{locale.language}",
nativeQuery = true)
List<Article> findAllArticlesUsingLocaleWithNativeQuery();
}

View File

@ -0,0 +1,17 @@
package com.baeldung.spring.data.jpa.spel.repository;
import com.baeldung.spring.data.jpa.spel.entity.Article;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.NoRepositoryBean;
@NoRepositoryBean
public interface BaseNewsApplicationRepository<T, I> extends JpaRepository<T, I> {
@Query(value = "select e from #{#entityName} e")
List<Article> findAllEntitiesUsingEntityPlaceholder();
@Query(value = "SELECT * FROM #{#entityName}", nativeQuery = true)
List<Article> findAllEntitiesUsingEntityPlaceholderWithNativeQuery();
}

View File

@ -0,0 +1,47 @@
package com.baeldung.spring.data.jpa.spel.controller;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import com.baeldung.spring.data.jpa.spel.NewsApplication;
import com.baeldung.spring.data.jpa.spel.entity.Article;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.web.reactive.server.WebTestClient;
@SpringBootTest(classes = NewsApplication.class,
webEnvironment = WebEnvironment.RANDOM_PORT,
properties = {
"spring.jpa.show-sql=true",
"spring.jpa.generate-ddl=true",
"spring.jpa.defer-datasource-initialization=true",
"spring.sql.init.data-locations=classpath:articles-dml.sql"
})
class ArticleControllerIntegrationTest {
@Autowired
private ArticleController articleController;
@Autowired
private WebTestClient webTestClient;
@Test
void whenApplicationStartBeansArePresent() {
assertNotNull(articleController);
assertNotNull(webTestClient);
}
@ParameterizedTest
@CsvSource({"eng,2", "fr,2", "esp,2", "deu, 2", "jp,0"})
void whenAskForNewsGetAllNewsInSpecificLanguageBasedOnLocale(String language, int expectedResultSize) {
webTestClient.get().uri("/articles?locale=" + language)
.exchange()
.expectStatus().isOk()
.expectBodyList(Article.class)
.hasSize(expectedResultSize);
}
}

View File

@ -0,0 +1,141 @@
package com.baeldung.spring.data.jpa.spel.repository;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import com.baeldung.spring.data.jpa.spel.NewsApplication;
import com.baeldung.spring.data.jpa.spel.entity.Article;
import com.baeldung.spring.data.jpa.spel.entity.ArticleWrapper;
import com.baeldung.spring.data.jpa.spel.extension.LocaleContextHolderExtension;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = NewsApplication.class,
properties = {
"spring.jpa.show-sql=true",
"spring.jpa.generate-ddl=true",
})
class ArticleRepositoryIntegrationTest {
@Autowired
private ArticleRepository articleRepository;
private static final String ENGLISH = "eng";
private static final Article SPORTS_ARTICLE
= new Article(1L, "Sports Update",
"The local team won their game last night...", ENGLISH);
@Autowired
private LocaleContextHolderExtension localeContextHolderExtension;
@Test
void whenContextStartRepositoryIsPresent() {
assertNotNull(articleRepository, "Repository should be present");
}
@Test
void whenContextStartContextHolderIsPresent() {
assertNotNull(localeContextHolderExtension, "Context holder should be present");
assertNotNull(localeContextHolderExtension.getRootObject());
}
@AfterEach
void tearDown() {
articleRepository.deleteAll();
}
@Test
void givenArticleWhenCreateWithPositionalArgumentsPlaceholdersShouldBePersisted() {
articleRepository.saveWithPositionalArguments(1L, SPORTS_ARTICLE.getTitle(),
SPORTS_ARTICLE.getContent(),
SPORTS_ARTICLE.getLanguage());
final List<Article> articles = articleRepository.findAll();
assertEquals(1, articles.size());
}
@Test
void givenArticleWhenCreateWithPositionalSpELArgumentsPlaceholdersShouldBePersisted() {
articleRepository.saveWithPositionalSpELArguments(1L, SPORTS_ARTICLE.getTitle(),
SPORTS_ARTICLE.getContent(),
SPORTS_ARTICLE.getLanguage());
final List<Article> articles = articleRepository.findAll();
assertEquals(1, articles.size());
}
@Test
void givenArticleWhenCreateWithPositionalSpELArgumentsPlaceholdersWithEmptyCheckShouldStoreDefaultValue() {
articleRepository.saveWithPositionalSpELArgumentsWithEmptyCheck(1L,
SPORTS_ARTICLE.getTitle(), null, SPORTS_ARTICLE.getLanguage());
final List<Article> articles = articleRepository.findAll();
assertEquals(1, articles.size());
assertEquals("Empty Article", articles.get(0).getContent());
}
@Test
void givenArticleWhenCreateWithPositionalSpELArgumentsPlaceholdersWithEmptyCheckShouldStoreTheOriginalContent() {
articleRepository.saveWithPositionalSpELArgumentsWithEmptyCheck(1L,
SPORTS_ARTICLE.getTitle(), SPORTS_ARTICLE.getContent(), SPORTS_ARTICLE.getLanguage());
final List<Article> articles = articleRepository.findAll();
assertEquals(1, articles.size());
assertEquals(SPORTS_ARTICLE, articles.get(0));
}
@Test
void givenArticleWhenCreateWithNamedArgumentsPlaceholdersShouldBePersisted() {
articleRepository.saveWithNamedArguments(1L, SPORTS_ARTICLE.getTitle(),
SPORTS_ARTICLE.getContent(), SPORTS_ARTICLE.getLanguage());
final List<Article> articles = articleRepository.findAll();
assertEquals(1, articles.size());
}
@Test
void givenArticleWhenCreateWithNamedSpELArgumentsPlaceholdersShouldBePersisted() {
articleRepository.saveWithNamedSpELArguments(1L, SPORTS_ARTICLE.getTitle(),
SPORTS_ARTICLE.getContent(), SPORTS_ARTICLE.getLanguage());
final List<Article> articles = articleRepository.findAll();
assertEquals(1, articles.size());
}
@Test
void givenArticleWhenCreateWithNamedSpELArgumentsAndLowerCaseLanguagePlaceholdersConvertLanguageToLowerCase() {
final String language = "ENG";
articleRepository.saveWithNamedSpELArgumentsAndLowerCaseLanguage(1L,
SPORTS_ARTICLE.getTitle(), SPORTS_ARTICLE.getContent(),
language);
final List<Article> articles = articleRepository.findAll();
assertEquals(1, articles.size());
assertEquals(language.toLowerCase(), articles.get(0).getLanguage());
}
@Test
void givenArticleWhenCreateWithSingleObjectArgumentPlaceholdersShouldBePersisted() {
articleRepository.saveWithSingleObjectSpELArgument(SPORTS_ARTICLE);
final List<Article> articles = articleRepository.findAll();
assertEquals(1, articles.size());
}
@Test
void givenArticleWhenCreateWithSingleWrappedObjectArgumentPlaceholdersShouldBePersisted() {
articleRepository.saveWithSingleWrappedObjectSpELArgument(new ArticleWrapper(SPORTS_ARTICLE));
final List<Article> articles = articleRepository.findAll();
assertEquals(1, articles.size());
}
@Test
void givenInheritedQueryWhenSearchForArticlesWillReturnThem() {
articleRepository.save(SPORTS_ARTICLE);
final List<Article> articles = articleRepository.findAllEntitiesUsingEntityPlaceholder();
assertEquals(1, articles.size());
}
@Test
void givenInheritedNativeQueryWhenSearchForArticlesWillReturnThem() {
articleRepository.save(SPORTS_ARTICLE);
final List<Article> articles = articleRepository.findAllEntitiesUsingEntityPlaceholderWithNativeQuery();
assertEquals(1, articles.size());
}
}

View File

@ -0,0 +1,15 @@
-- English
INSERT INTO articles (id, title, content, language) VALUES (11, 'Sports Update', 'The local team won their game last night...', 'eng');
INSERT INTO articles (id, title, content, language) VALUES (17, 'Health News', 'New advancements in medical research were revealed...', 'eng');
-- French
INSERT INTO articles (id, title, content, language) VALUES (18, 'Nouvelles de la santé', 'De nouvelles avancées dans la recherche médicale ont été révélées...', 'fr');
INSERT INTO articles (id, title, content, language) VALUES (12, 'Actualité sportive', 'L''équipe locale a gagné son match hier soir...', 'fr');
-- Spanish
INSERT INTO articles (id, title, content, language) VALUES (13, 'Actualización Deportiva', 'El equipo local ganó su partido anoche...', 'esp');
INSERT INTO articles (id, title, content, language) VALUES (14, 'Innovación Tecnológica', 'Se ha anunciado un avance en la investigación de IA...', 'esp');
-- German
INSERT INTO articles (id, title, content, language) VALUES (15, 'Sportaktualisierung', 'Die lokale Mannschaft hat ihr Spiel gestern Abend gewonnen...', 'deu');
INSERT INTO articles (id, title, content, language) VALUES (16, 'Technologie Durchbruch', 'Ein Durchbruch in der KI-Forschung wurde bekannt gegeben...', 'deu');