Skip to content

Commit

Permalink
Improve AbstractService and AbstractDatabase: Enhance documentation, …
Browse files Browse the repository at this point in the history
…caching, and initialization

Refactor AbstractService and AbstractDatabase to include comprehensive documentation explaining their roles, integrated caching mechanisms, and improved initialization processes. Updates ensure clear understanding and optimized performance for service and database operations within the application. Additionally, refined commit hooks to enforce consistent commit message standards.
  • Loading branch information
vnobo committed Sep 5, 2024
1 parent 2768c0d commit 30b584d
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,78 @@
import java.util.Map;

/**
* @author <a href="https://github.com/vnobo">Alex bob</a>
* AbstractDatabase serves as a foundation for implementing database operations with caching capabilities.
* It extends the AbstractService to inherit common service layer functionalities and integrates
* R2DBC for reactive database access.
* This class provides generic methods to execute queries and counts with caching support.
* <p>
* Key Features:
* - Utilizes R2dbcEntityTemplate for entity-based operations.
* - Leverages DatabaseClient for SQL execution.
* - Implements caching mechanisms to enhance query performance.
* - Supports reactive programming model using Flux and Mono.
* <p>
* Usage Note:
* Subclasses should implement specific database interaction logic while leveraging
* the provided caching wrappers around query and count operations.
* <p>
* Dependencies:
* Requires an R2dbcEntityTemplate, DatabaseClient, and Cache instance to be functional,
* typically wired through dependency injection.
*/
public abstract class AbstractDatabase extends AbstractService {

/**
* The R2dbcEntityTemplate instance used for executing reactive database operations.
* This template facilitates interaction with the database, including query execution,
* entity conversion, and transaction management specifically tailored for R2DBC (Reactive Relational Database Connectivity).
*/
protected R2dbcEntityTemplate entityTemplate;

/**
* Represents a client for interacting with the database, facilitating operations such as
* querying, updating, and managing data within the database. This field is initialized
* and configured typically during the setup or initialization phase of the hosting class,
* enabling seamless database access throughout the application.
*/
protected DatabaseClient databaseClient;

/**
* The R2DBC Converter instance used for converting between R2DBC Data types and domain-specific objects.
* This converter plays a crucial role in mapping query results to entity classes and vice versa,
* facilitating seamless interaction with the R2DBC database through the R2dbcEntityTemplate.
*/
protected R2dbcConverter r2dbcConverter;

/**
* Executes a database query with caching functionality.
* It takes a cache key, a query object, and an entity class type to perform the operation.
* The results are cached for future queries with the same key.
*
* @param <T> The type of entities expected as query results.
* @param key The unique identifier used as a cache key. Determines the cache entry for storing/retrieving results.
* @param query The query object defining the SQL query and its potential parameters.
* @param entityClass The class of the entity that each row in the result set will be mapped to.
* @return A {@link Flux} emitting the query results, potentially from cache if previously stored.
*/
protected <T> Flux<T> queryWithCache(Object key, Query query, Class<T> entityClass) {
Flux<T> source = this.entityTemplate.select(query, entityClass);
return queryWithCache(key, source).cache();
}

/**
* Executes a SQL query with caching capability. It binds provided parameters to the SQL query,
* maps the result set to entities of the specified class, applies user auditor serialization,
* and utilizes a cache to store query results for subsequent identical queries.
*
* @param <T> The type of entities the SQL query results will be mapped to.
* @param key The cache key used to identify the cached data. This should uniquely represent
* the query and its parameters.
* @param sql The SQL query string to be executed.
* @param bindParams A map containing named parameter bindings for the SQL query.
* @param entityClass The class of the entity that each row in the result set will be converted into.
* @return A {@link Flux} emitting the entities resulting from the query, potentially from the cache.
*/
protected <T> Flux<T> queryWithCache(Object key, String sql,
Map<String, Object> bindParams, Class<T> entityClass) {
var executeSpec = this.databaseClient.sql(() -> sql);
Expand All @@ -42,6 +99,17 @@ protected <T> Flux<T> queryWithCache(Object key, String sql,
return queryWithCache(key, source);
}

/**
* Executes a Flux-based database query with caching functionality.
* It enhances the source Flux by caching its emissions under a specified key.
* If the cache contains data for the key, it returns the cached data immediately.
* Otherwise, it executes the source Flux, caches the results, and then emits them.
*
* @param <T> The type of elements emitted by the Flux.
* @param key The unique identifier used as the cache key.
* @param sourceFlux The original Flux to be executed for querying data.
* @return A Flux that emits the cached data if available, otherwise executes the source Flux and caches the result.
*/
protected <T> Flux<T> queryWithCache(Object key, Flux<T> sourceFlux) {
String cacheKey = key + ":data";
Collection<T> cacheData = this.cache.get(cacheKey, ArrayList::new);
Expand All @@ -52,30 +120,81 @@ protected <T> Flux<T> queryWithCache(Object key, Flux<T> sourceFlux) {
.switchIfEmpty(Flux.defer(() -> source));
}

/**
* Counts entities with caching support based on the provided key, query, and entity class.
* This method enhances entity counting by storing the count result in a cache,
* allowing subsequent calls with the same key to retrieve the count directly from the cache
* rather than executing the query again.
*
* @param <T> The type of entities for which the count is to be performed.
* @param key A unique identifier used as a cache key. Determines the cached count's retrieval.
* @param query The query object defining the criteria for counting entities.
* @param entityClass The class of the entities being counted.
* @return A {@link Mono} emitting the count of entities as a {@link Long}, potentially from cache.
*/
protected <T> Mono<Long> countWithCache(Object key, Query query, Class<T> entityClass) {
Mono<Long> source = this.entityTemplate.count(query, entityClass);
return countWithCache(key, source).cache();
}

/**
* Executes a SQL count query with caching capabilities. It prepares the SQL query with provided bind parameters,
* creates a Mono source to fetch the count, and then delegates to another method to handle caching logic.
*
* @param key The unique cache key associated with the count query. Used for caching and retrieving results.
* @param sql The SQL count query string to be executed.
* @param bindParams A map containing named parameter placeholders and their respective values for the SQL query.
* @return A Mono emitting the count result, potentially fetched from cache or computed from the database.
*/
protected Mono<Long> countWithCache(Object key, String sql, Map<String, Object> bindParams) {
var executeSpec = this.databaseClient.sql(() -> sql);
executeSpec = executeSpec.bindValues(bindParams);
Mono<Long> source = executeSpec.mapValue(Long.class).first();
return countWithCache(key, source);
}

/**
* Counts entities using a cached value if present, otherwise executes the provided Mono source
* and caches the result for future use. This method enhances performance by retrieving counts
* from a cache, thereby reducing the need for repetitive count queries to the underlying database.
*
* @param key The unique identifier used as the cache key. This key is instrumental in
* both retrieving and storing count values within the cache.
* @param sourceMono A Mono publisher that, when subscribed to, executes a count operation.
* If no cached value is found for the specified key, this Mono will be
* subscribed to retrieve the count, which is subsequently cached.
* @return A Mono emitting the count of entities. If a cached value is present, it is emitted
* immediately. Otherwise, the Mono returned by `sourceMono` is executed, its emission is
* cached, and then emitted.
*/
protected Mono<Long> countWithCache(Object key, Mono<Long> sourceMono) {
String cacheKey = key + ":count";
Long cacheCount = this.cache.get(cacheKey, () -> null);
Mono<Long> source = sourceMono.doOnNext(count -> BeanUtils.cachePut(this.cache, cacheKey, count));
return Mono.justOrEmpty(cacheCount).switchIfEmpty(Mono.defer(() -> source));
}

/**
* Sets the R2dbcEntityTemplate instance to be used by this class for database operations.
*
* @param entityTemplate The R2dbcEntityTemplate instance to inject.
*/
@Autowired
public void setEntityTemplate(R2dbcEntityTemplate entityTemplate) {
this.entityTemplate = entityTemplate;
}

/**
* Invoked by the containing {@code BeanFactory} after it has set all bean properties
* and satisfied all dependencies for this bean. This method allows the bean instance
* to perform initialization only possible when all bean properties have been set
* and to throw an exception in the event of misconfiguration.
*
* <p>In this implementation, the method sets up the {@code databaseClient} and
* {@code r2dbcConverter} by extracting them from the injected {@code entityTemplate}.
* It calls the superclass's {@code afterPropertiesSet} first to ensure any
* necessary setup in the parent class is also executed.
*/
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,85 @@
import java.util.Optional;

/**
* @author <a href="https://github.com/vnobo">Alex bob</a>
* AbstractService serves as a foundation for implementing service layers with common functionalities,
* such as caching and initialization routines. It integrates with Spring's cache management and
* initialization callbacks, providing a structured approach to service layer development.
*
* <p>This abstract class requires implementations to define business logic while benefiting from
* built-in support for an {@link ObjectMapper} for JSON serialization/deserialization and a
* {@link CacheManager} for managing caching operations. It enforces the setup of a dedicated cache
* per extending service class, enhancing performance through result caching.
* <p>Notable features include:
* <ul>
* <li>Automatic cache initialization upon bean creation.</li>
* <li>Methods to execute database queries with integrated caching capabilities.</li>
* <li>Support for dependency injection, including an {@link ObjectMapper} and a {@link CacheManager}.</li>
* </ul>
* Extending classes should override or implement specific business logic methods as needed,
* leveraging the provided utilities and infrastructure setup.
*
* @see InitializingBean Interface to be implemented by beans that need to react once all their properties
* have been set by a BeanFactory.
*/
@Log4j2
public abstract class AbstractService implements InitializingBean {

/**
* Cache instance used to store and retrieve data within the service.
* Initialized through the {@link #initializingCache(String)} method with a cache name defaulting to the class name concatenated with ".cache".
* This cache is cleared upon initialization and is essential for providing temporary storage that accelerates data access.
*/
protected Cache cache;

/**
* Manages the caching infrastructure for the service.
* This field holds an instance of a CacheManager which is responsible for creating,
* retrieving, and managing caches used throughout the application to improve performance by storing frequently accessed data.
* It is initialized via dependency injection and utilized in the {@link #initializingCache(String)} method to obtain or create specific caches.
*/
protected CacheManager cacheManager;

/**
* Provides an instance of Jackson's {@link ObjectMapper}, which is a powerful JSON processor
* capable of converting Java objects into their JSON representation and vice versa.
* This field is automatically wired by Spring's dependency injection, ensuring that it is properly configured
* and ready to handle complex object mappings and custom serialization/deserialization scenarios within the service layer.
*
* <p>Usage of this {@link ObjectMapper} should adhere to best practices regarding JSON processing within the application,
* including proper handling of date formats, polymorphism, and any custom serializers/deserializers defined
* for application-specific data structures.
*/
protected ObjectMapper objectMapper;

/**
* Sets the ObjectMapper instance to be used by this component.
*
* @param objectMapper The ObjectMapper object which provides functionality for JSON serialization and deserialization.
*/
@Autowired
public void setObjectMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

/**
* Sets the CacheManager instance to be used by this component.
*
* @param cacheManager The CacheManager instance to manage caches.
*/
@Autowired
public void setCacheManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}

/**
* Initializes the cache with the specified name.
* If the cache manager is set, it attempts to retrieve the cache from the manager;
* otherwise, it creates a new ConcurrentMapCache with the given name.
* After initialization, the cache is cleared and a debug log message is generated indicating
* the cache's class name and name.
*
* @param cacheName The name of the cache to initialize.
*/
protected void initializingCache(String cacheName) {
this.cache = Optional.ofNullable(this.cacheManager).map(manager -> manager.getCache(cacheName))
.orElse(new ConcurrentMapCache(cacheName));
Expand All @@ -40,6 +98,15 @@ protected void initializingCache(String cacheName) {
this.cache.getNativeCache().getClass().getSimpleName(), this.cache.getName());
}

/**
* Invoked by the containing {@code BeanFactory} after it has set all bean properties
* and satisfied all dependencies for this bean. This method allows the bean instance
* to perform initialization only possible when all bean properties have been set
* and to throw an exception in the event of misconfiguration.
*
* <p>This implementation initializes the cache associated with this component,
* using the bean's class name concatenated with ".cache" as the cache identifier.
*/
@Override
public void afterPropertiesSet() {
initializingCache(this.getClass().getName().concat(".cache"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,32 @@
import java.util.Collection;

/**
* @author <a href="https://github.com/vnobo">Alex bob</a>
* Represents the base entity contract for entities that require common functionality
* such as having a unique code, being serializable, and persistable with a generic type identifier.
* Implementing classes should provide concrete behavior for these base operations.
*/
public interface BaseEntity<T> extends Serializable, Persistable<T> {

/**
* 设置代码值。
* <p>
* 此方法提供了一个接口来为相关对象设置一个代码值。具体的实现可能会根据实际需求来决定如何处理这个代码值。
* 由于这是一个默认方法,它为接口的实现提供了一种灵活的方式来处理代码值,而不需要强制实现这个方法。
* Sets the unique code for an entity.
* This method is intended to assign or update the code attribute of an entity,
* which serves as a unique identifier in accordance with the BaseEntity interface.
* The implementation should handle the logic of how the code is generated or validated,
* based on specific business rules, which may involve prefixing, formatting, or checking for uniqueness.
*
* @param code 要设置的代码值。这个参数允许调用者指定一个代码值,该值可以是任何字符串,具体的含义和使用方式取决于实现。
* @param code The code to be set for the entity. It could be a plain string or follow a predefined format.
*/
default void setCode(String code) {
//todo 方法体为空,具体的实现可能需要根据实际需求来决定如何处理code参数。
//todo
}

/**
* 判断当前对象是否为新对象,即是否具有ID。
* 如果对象尚未分配ID,则将其视为新对象,并生成一个新的ID。
* 此方法用于标识对象是否已存在于持久化存储中,如果没有ID,则认为是新对象需要进行持久化操作。
* Determines whether the entity instance is considered new.
* This is typically used to decide if the entity needs to be inserted or updated when persisted.
* The method checks if the identifier ({@code getId()}) is empty to assess newness.
* If the entity is determined to be new, it generates and assigns a new unique code ({@code setCode(ContextUtils.nextId());}).
*
* @return 如果对象是新对象(没有ID),则返回true;否则返回false。
* @return {@code true} if the entity is considered new (i.e., its identifier is empty), otherwise {@code false}.
*/
@Override
@JsonIgnore
Expand All @@ -47,11 +51,13 @@ default boolean isNew() {
}

/**
* 创建一个Criteria对象,用于构建查询条件。
* 此方法允许指定一组应被忽略的属性键,这些键不会被包含在查询条件中。
* Constructs a {@link Criteria} instance based on the current entity,
* allowing for the specification of properties to be excluded from criteria creation.
*
* @param skipKeys 一个字符串集合,包含应被忽略的属性键。
* @return 返回一个Criteria对象,用于进一步构建查询条件。
* @param skipKeys A {@link Collection} of {@link String} property keys to be skipped when building the criteria.
* These properties will not be included in the generated criteria.
* @return A {@link Criteria} object tailored according to the current entity,
* excluding the properties specified in the {@code skipKeys} collection.
*/
default Criteria criteria(Collection<String> skipKeys) {
// 调用CriteriaUtils的静态方法build来创建Criteria对象,并传入当前对象和要忽略的属性键集合。
Expand Down

0 comments on commit 30b584d

Please sign in to comment.