Skip to main content

Validating Native Query and Entity Graph

 Validating Native Query and Entity Graph


Using native queries in Spring JPA/Hibernate can be less enjoyable, but they are occasionally indispensable, especially for enhancing performance. Similarly, EntryGraphs become necessary in specific situations to mitigate N+1 scenarios or define data boundaries retrieved from the database.
Since it's challenging to validate these scenarios without executing each query, inadequate testing can lead to significant issues. The same holds true when refactoring numerous database tables and columns, potentially missing updates in native queries, which may slip through testing and manifest in production.
You may encounter such scenarios as well. Therefore, we've invested effort into developing a solution to validate native queries against a schema. You might be interested in such thing or suggest some improvement over it.

Here's what we've accomplished:
Developed a component that conducts validation by executing each native query against the schema in a fairly generic manner.
To initiate this process, we've created the following class for gathering all native queries:


import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;

class RepositoryScanner extends ClassPathScanningCandidateComponentProvider {

    public RepositoryScanner() {
        super(false);
        addIncludeFilter(new AnnotationTypeFilter(Repository.class));
    }

    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return true;
    }
}
          
Then using the same class:
  
  private void scanRepositories(String basePackage) throws ClassNotFoundException {
    var candidateComponents = provider.findCandidateComponents(basePackage);

    log.warn(createWarningMessage(format("DB Validation Mode (%s)", basePackage),
        format("Native Queries and Entity Graphs (%s repositories)", candidateComponents.size())));

    for (var repositoryClass : candidateComponents) {
        Class jpaRepository = Class.forName(repositoryClass.getBeanClassName());

        for (var method : jpaRepository.getDeclaredMethods()) {
            var queryAnnotation = method.getAnnotation(Query.class);
            var entityGraph = method.getAnnotation(EntityGraph.class);

            if ((queryAnnotation != null && queryAnnotation.nativeQuery()) || entityGraph != null) {
                log.info("SQL: {}.{} - {}",  jpaRepository.getSimpleName(), method.getName(),
                        queryAnnotation != null ? queryAnnotation.value() : entityGraph.type() + ": " + Arrays.toString(entityGraph.attributePaths()));
            }
        }
    }
}
  
Currently, the invocation and validation aspects are in place (acknowledging that there's room for improvement, but the current setup suffices for our needs).
private Object invokeMethod(Class clazz, Method method) {
    Object[] parameters = new Object[method.getParameterCount()];

    try {
        for (int index = 0; index < parameters.length; index++) {
            Parameter param = method.getParameters()[index];
            parameters[index] = getRandomValueForType(param.getType());
        }

        Object instance = applicationContext.getBean(clazz);
        return transactionTemplate.execute(action -> invoke(instance, method, parameters));
    }
    catch (UndeclaredThrowableException e) {
        if (!(e.getCause() instanceof InvocationTargetException
            && ((InvocationTargetException) e.getCause()).getTargetException().getCause() instanceof ConstraintViolationException)) {
            throw new RuntimeException(format("With Parameters: %s", Arrays.toString(parameters)), e);
        }
    }
    catch (Exception e) {
        throw new RuntimeException(format("With Parameters: %s", Arrays.toString(parameters)), e);
    }

    return null;
}

@SneakyThrows
private Object invoke(Object instance, Method method, Object[] parameters) {
    return method.invoke(instance, parameters);
}

private Object getRandomValueForType(Class type) {
    if (type == Pageable.class) return Pageable.ofSize(1);
    if (type.isEnum()) return type.getEnumConstants()[0];
    if (type == Specification.class) return new UserSpecification(Long.MAX_VALUE, null);
    if (type == boolean.class || type == Boolean.class) return false;
    if (type == LocalDateTime.class) return LocalDateTime.now().plusYears(10);
    if (type == long.class || type == Long.class) return Long.MAX_VALUE;
    if (type == int.class || type == Integer.class) return Integer.MAX_VALUE;
    if (type == String.class) return "DatabaseSchemaValidation";
    if (type == double.class || type == Double.class) return Double.MAX_VALUE;
    if (Set.class.isAssignableFrom(type)) return new HashSet<>();
    if (Collection.class.isAssignableFrom(type) || List.class.isAssignableFrom(type)) return new ArrayList<>();

    log.warn("Type not mapped: {}", type.getName());
    return null;
}

Afterward, we can determine when to execute this validator, or perhaps opt to run it during startup or integration test, such as...
import static java.lang.String.format;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.UndeclaredThrowableException;
import java.time.LocalDateTime;
import java.util.*;

import javax.annotation.PostConstruct;

import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Profile;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

@ConditionalOnProperty(value = "database.schema.validation.enabled", havingValue = "true")
@Component
@Slf4j
@RequiredArgsConstructor
@Transactional
public class DatabaseSchemaValidation {
	private final RepositoryScanner provider = new RepositoryScanner();
	private final ApplicationContext applicationContext;
	private final TransactionTemplate transactionTemplate;

	@PostConstruct
	void validate() throws ClassNotFoundException {
		scanRepositories("com.example.repository");
	}

	private void scanRepositories(String basePackage) throws ClassNotFoundException {
		var candidateComponents = provider.findCandidateComponents(basePackage);

		log.warn("{} - {}", format("DB Validation Mode (%s)", basePackage), format("Native Queries and Entity Graphs (%s repositories)", candidateComponents.size())));

		for (var repositoryClass : candidateComponents) {
			Class jpaRepository = Class.forName(repositoryClass.getBeanClassName());

			for (var method : jpaRepository.getDeclaredMethods()) {
				var queryAnnotation = method.getAnnotation(Query.class);
				var entityGraph = method.getAnnotation(EntityGraph.class);

				if ((queryAnnotation != null && queryAnnotation.nativeQuery()) || entityGraph != null) {
					try {
						invokeMethod(jpaRepository, method);
					} catch (Exception e) {
						log.warn("Exception while calling: {}.{} - {} - {}", jpaRepository.getSimpleName(), method.getName(),
							queryAnnotation != null ? queryAnnotation.value() : entityGraph.type() + ": " + Arrays.toString(entityGraph.attributePaths()),
							e.getMessage());
						log.debug("{}", e.getMessage(), e);
						throw e;
					}
				}
			}
		}
	}

	private static class RepositoryScanner extends ClassPathScanningCandidateComponentProvider {

		public RepositoryScanner() {
			super(false);
			addIncludeFilter(new AnnotationTypeFilter(Repository.class));
		}

		@Override
		protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
			return true;
		}
	}

	public Object invokeMethod(Class clazz, Method method) {
		Object[] parameters = new Object[method.getParameterCount()];

		try {
			for (int index = 0; index < parameters.length; index++) {
				Parameter param = method.getParameters()[index];
				parameters[index] = getRandomValueForType(param.getType());
			}

			Object instance = applicationContext.getBean(clazz);
			return transactionTemplate.execute(action -> invoke(instance, method, parameters));
		}
		catch (UndeclaredThrowableException e) {
			if (!(e.getCause() instanceof InvocationTargetException
				&& ((InvocationTargetException) e.getCause()).getTargetException().getCause() instanceof ConstraintViolationException)) {
				throw new RuntimeException(format("With Parameters: %s", Arrays.toString(parameters)), e);
			}
		}
		catch (Exception e) {
			throw new RuntimeException(format("With Parameters: %s", Arrays.toString(parameters)), e);
		}

		return null;
	}

	@SneakyThrows
	private Object invoke(Object instance, Method method, Object[] parameters) {
		return method.invoke(instance, parameters);
	}

	private Object getRandomValueForType(Class type) {
		if (type == Pageable.class) return Pageable.ofSize(1);
		if (type.isEnum()) return type.getEnumConstants()[0];
		if (type == boolean.class || type == Boolean.class) return false;
		if (type == LocalDateTime.class) return LocalDateTime.now().plusYears(10);
		if (type == long.class || type == Long.class) return Long.MAX_VALUE;
		if (type == int.class || type == Integer.class) return Integer.MAX_VALUE;
		if (type == String.class) return "DatabaseSchemaValidation";
		if (type == double.class || type == Double.class) return Double.MAX_VALUE;
		if (Set.class.isAssignableFrom(type)) return new HashSet<>();
		if (Collection.class.isAssignableFrom(type) || List.class.isAssignableFrom(type)) return new ArrayList<>();

		log.warn("Type not mapped: {}", type.getName());
		return null;
	}
}

Comments

Popular posts from this blog

Apache Storm vs Apache Flink

 Apache Storm and Apache Flink are both distributed stream processing frameworks, but they have some key differences in terms of architecture, programming models, and features. Here's a comparison between Apache Storm and Apache Flink: 1. **Programming Model:**    - **Apache Storm:** Storm provides a low-level, event-driven programming model using spouts and bolts. Spouts are sources of data, and bolts are the processing units that apply transformations or analyses to the data. It is designed for building complex, directed acyclic graphs (DAGs) of processing stages.        - **Apache Flink:** Flink offers a more high-level and expressive API for stream processing. Flink's API includes a functional programming style using operations like map, flatMap, filter, and windowing operations, making it easier to express complex data transformations. 2. **Event Time Processing:**    - **Apache Storm:** Initially, Storm had challenges in handling event ...

Shell Scripts

Shell scripts $? variable: In a shell script, we can check the return status immediately after running any command to determine if command is successful or not. like echo $? if return status is 0, it indicates success,  and if the return status is non-zero, typically 1, means failure. /dev/null /dev/null is a special device file in Unix-like operating systems (including Linux) that discards all data written to it. It essentially acts as a black hole for data. When data is written to /dev/null, it simply disappears and does not consume any storage space. Here are some common use cases for /dev/null: Discarding Output: As mentioned earlier, redirecting output to /dev/null is a common way to discard unwanted output, such as diagnostic messages or verbose output, especially when running scripts or commands in the background where you don't need to see the output. command >/dev/null  # Redirects stdout to /dev/null command 2>/dev/null # Redirects stderr to /dev/null command ...

Recover lost files on Windows, free and effective

 Windows File Recovery If necessary, download and launch the app from Microsoft Store. Press the Windows key, enter Windows File Recovery in the search box, and then select Windows File Recovery. When you are prompted to allow the app to make changes to your device, select Yes. In the Command Prompt window, enter the command in the following format:  winfr source-drive: destination-drive: [/mode] [/switches] There are 2 basic modes you can use to recover files: Regular and Extensive.  Regular mode examples Recover your Documents folder from your C: drive to the recovery folder on an E: drive. Don’t forget the backslash (\) at the end of the folder.   winfr C: E: /regular /n \Users\<username>\Documents\  Recover PDF and Word files from your C: drive to the recovery folder on an E: drive.  winfr C: E: /regular /n *.pdf /n *.docx  Extensive mode examples   winfr E: C: /extensive /n *invoice*  Recover jpeg and png photos from your...