- Useful Crud and Paging/Sorting methods (just like spring-data-jpa)
- Custom query methods (findBy*And*OrderBy*, @Query("FROM Abc")), native queries are also supported
- Support
@Transactional
(propagation, readOnly, rollbackFor, timeout, noRollbackFor, ...) - Support
@Modifying
,@Param
- Support
@Lock
,@EntityGraph
- Support
@NamedQuery
,@NamedEntityGraph
- Support SpEL
- Support Pagination
- Support Auditing
- Auto-config
- Of course, it is truly non-blocking and compatible with Webflux
1. Dependency and config:
<dependency>
<groupId>io.github.anaconda875</groupId>
<artifactId>reactive-hibernate-spring-boot-starter</artifactId>
<version>1.1.0</version>
</dependency>
Sometimes you might need to add (in case of dependencies conflict):
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.4.4.Final</version>
<scope>compile</scope>
</dependency>
Add a suitable driver (for example, MySQL):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-mysql-client</artifactId>
<version>${your.version}</version>
</dependency>
Then add these (example) configs:
spring.jpa.properties.jakarta.persistence.jdbc.url=jdbc:mysql://localhost:3306/blogdb
spring.jpa.properties.jakarta.persistence.jdbc.user=mysql
spring.jpa.properties.jakarta.persistence.jdbc.password=mysql
spring.jpa.properties.hibernate.connection.pool_size=10
spring.jpa.properties.hibernate.enhancer.enableDirtyTracking=false
spring.jpa.properties.hibernate.enhancer.enableLazyInitialization=false
spring.jpa.properties.hibernate.enhancer.enableAssociationManagement=false
2. Useful Crud and Paging/Sorting methods: see ReactiveCrudRepository and ReactivePagingAndSortingRepository
3. Custom query methods (with Pageable
, @Lock
, @EntityGraph
, @Param
, @Transactional
, @Modifying
):
@Lock(LockModeType.PESSIMISTIC_READ)
@EntityGraph(attributePaths = {"content"})
Flux<Post> findByContentOrderByCreatedAtDesc(String content);
@Query("SELECT p FROM Post p WHERE p.content = ?1")
Mono<Page<Post>> findByContentCustomPage(String content, Pageable pageable);
@Query(
nativeQuery = true,
value =
"SELECT id, title, content, created_at, created_by, last_modified_at, last_modified_by " +
"FROM posts WHERE id = ?1")
Mono<Post> nativeQ(UUID id);
@Query(
nativeQuery = true,
value =
"SELECT id, title, content, created_at, created_by, last_modified_at, last_modified_by "
+ "FROM posts WHERE content = :content")
Flux<Post> nativeQ2(@Param("content") String content);
@Modifying
@Query(nativeQuery = true, value = "DELETE from posts WHERE content = :content")
@Transactional
Mono<?> deleteNative2(@Param("content") String content);
@Modifying
@Query("DELETE FROM Post p WHERE p.title = :title")
Mono<?> deleteCustom(@Param("title") String title);
4. Support @NamedQuery
, @NamedEntityGraph
, Auditing:
@Data
@Entity
@NamedQueries(
value = {
@NamedQuery(
name = "Post.testNamed",
query = "SELECT p FROM Post p WHERE p.content = :content")
})
@NamedEntityGraphs({
@NamedEntityGraph(
name = "Post.testNamed",
attributeNodes = {@NamedAttributeNode("title")})
})
public class Post {
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid2")
UUID id;
String title;
String content;
//<Auditing>
@Column(name = "created_at")
@CreatedDate
LocalDateTime createdAt;
@Column(name = "last_modified_at")
@LastModifiedDate
LocalDateTime lastModifiedAt;
@Column(name = "created_by")
@CreatedBy
String createdBy;
@Column(name = "last_modified_by")
@LastModifiedBy
String lastModifiedBy;
//</Auditing>
}
//In repository
@Lock(LockModeType.READ)
@EntityGraph
Mono<Page<Post>> testNamed(String content, Pageable pageable);
//Config for Pagination, Auditing
@Configuration
@EnableReactiveJpaAuditing(auditorAwareRef = "reactiveAuditorAware")
public class Config {
@Bean
WebFluxConfigurer webFluxConfigurer() {
return new WebFluxConfigurer() {
@Override
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
configurer.addCustomResolver(new ReactivePageableHandlerMethodArgumentResolver());
}
};
}
@Bean
ReactiveAuditorAware<String> reactiveAuditorAware() {
return () ->
ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(Authentication::getPrincipal)
.map(UserDetails.class::cast)
.map(UserDetails::getUsername);
}
@Bean
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(authorize -> authorize.anyExchange().authenticated())
.httpBasic(Customizer.withDefaults())
.build();
}
@Bean
ReactiveUserDetailsService userDetailsService() {
var isabelle = User.withUsername("admin").password("admin").authorities("admin").build();
var bjorn =
User.withUsername("anonymous").password("anonymous").authorities("anonymous").build();
return new MapReactiveUserDetailsService(isabelle, bjorn);
}
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
ReactiveEvaluationContextExtension securityExtension() {
return new ReactiveEvaluationContextExtension() {
@Override
public String getExtensionId() {
return "webflux-security";
}
@Override
public Mono<? extends EvaluationContextExtension> getExtension() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(SecurityEvaluationContextExtension::new);
}
};
}
}
5. SpEL:
@Query(
nativeQuery = true,
value =
"SELECT id, title, content, created_at, created_by, last_modified_at, last_modified_by FROM posts " +
"WHERE created_by = :#{authentication.name} AND title = :title " +
"AND last_modified_by = :#{authentication.name}")
Mono<Post> testSpelNative2(@Param("title") String title);
@Query(
"SELECT p from #{#entityName} p WHERE p.lastModifiedBy = :#{authentication.name} AND p.title = ?1 AND p.createdBy = :#{authentication.name}")
@EntityGraph(attributePaths = {"createdBy"})
Mono<Post> testSpel3(String title);
6. TO BE CONTINUED...
1. Auto-config
- An Auto-config was implemented for registering Hibernate Reactive related (PersistentUnit, EntityManagerFactory, SessionFactory, etc)
- An Auto-config was implemented for registering Repositories (Spring Data related)
2. Repository internal working
- This project is 40% based on Spring Data Common
- Spring use Dynamic Proxy and AOP to create Repositories
- All custom methods (findByTitle,
@Query
) will be registered as metadata (see ReactiveJpaQueryLookupStrategy) - A chain of Advices
was added to handle method invocations (findBySlug, save, @Query, etc)
-
Case 1: Crud methods (findById, findAll, save, delete, etc): The interceptor will invoke SimpleReactiveJpaRepository
-
Case 2: Query methods (findByTagOrSlugOrderByDate, @Query, NamedQuery)
The interceptor will invoke RepositoryQuery
-
Case 3:
@Lock
,@EntityGraph
- see AbstractReactiveJpaQuery -
Case 4: SpEL - see SpELParameterValueEvaluator
-
Case 5: Native query - see NativeReactiveJpaQuery
-
Case 6: Named query - see NamedQuery
-
3. Transactional
- Extending an abstract class provided by Spring. It provides useful methods like open a transaction, resume a transaction, suspend, commit or rollback it.
- Spring registered an Interceptor to handle
@Transactional
by invoking our implementation.
4. Auditing
- A custom EntityCallback was created to invoke Auditing functions.
5. How does the library share common objects (Session, CrudMetadata, etc)?
Unlike Spring Web which is using ThreadLocal
to share common objects, the library
use Context
APIs which is provided by reactor
as ThreadLocal
may not work in
reactive app.
This is an example of how to use it (with Postgres): https://github.com/anaconda875/spring-hibernate-reactive-mutiny-example
If you guys find it useful for our business, feel free to use and report bugs to me