io.micronaut.data.jdbc.operations.DefaultJdbcRepositoryOperations Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of micronaut-data-jdbc Show documentation
Show all versions of micronaut-data-jdbc Show documentation
Data Repository Support for Micronaut
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.data.jdbc.operations;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import io.micronaut.context.BeanContext;
import io.micronaut.context.annotation.EachBean;
import io.micronaut.context.annotation.Parameter;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.beans.BeanProperty;
import io.micronaut.core.beans.BeanWrapper;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.data.annotation.DateUpdated;
import io.micronaut.data.annotation.Query;
import io.micronaut.data.annotation.Relation;
import io.micronaut.data.annotation.Repository;
import io.micronaut.data.exceptions.DataAccessException;
import io.micronaut.data.intercept.annotation.DataMethod;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.jdbc.mapper.ColumnIndexResultSetReader;
import io.micronaut.data.jdbc.mapper.ColumnNameResultSetReader;
import io.micronaut.data.jdbc.mapper.JdbcQueryStatement;
import io.micronaut.data.jdbc.mapper.SqlResultConsumer;
import io.micronaut.data.jdbc.runtime.ConnectionCallback;
import io.micronaut.data.jdbc.runtime.PreparedStatementCallback;
import io.micronaut.data.model.*;
import io.micronaut.data.model.query.builder.QueryBuilder;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder;
import io.micronaut.data.model.runtime.*;
import io.micronaut.data.operations.async.AsyncCapableRepository;
import io.micronaut.data.operations.reactive.ReactiveCapableRepository;
import io.micronaut.data.operations.reactive.ReactiveRepositoryOperations;
import io.micronaut.data.repository.GenericRepository;
import io.micronaut.data.runtime.date.DateTimeProvider;
import io.micronaut.data.runtime.mapper.DTOMapper;
import io.micronaut.data.runtime.mapper.ResultConsumer;
import io.micronaut.data.runtime.mapper.ResultReader;
import io.micronaut.data.runtime.mapper.TypeMapper;
import io.micronaut.data.runtime.mapper.sql.SqlDTOMapper;
import io.micronaut.data.runtime.mapper.sql.SqlResultEntityTypeMapper;
import io.micronaut.data.runtime.mapper.sql.SqlTypeMapper;
import io.micronaut.data.runtime.operations.ExecutorAsyncOperations;
import io.micronaut.data.runtime.operations.ExecutorReactiveOperations;
import io.micronaut.http.codec.MediaTypeCodec;
import io.micronaut.inject.BeanDefinition;
import io.micronaut.inject.qualifiers.Qualifiers;
import io.micronaut.transaction.TransactionOperations;
import javax.annotation.PreDestroy;
import javax.inject.Named;
import javax.sql.DataSource;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.nio.charset.StandardCharsets;
import java.sql.*;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
/**
* Implementation of {@link JdbcRepositoryOperations}.
*
* @author graemerocher
* @since 1.0.0
*/
@EachBean(DataSource.class)
public class DefaultJdbcRepositoryOperations extends AbstractSqlRepositoryOperations implements
JdbcRepositoryOperations,
AsyncCapableRepository,
ReactiveCapableRepository,
AutoCloseable {
private static final Object IGNORED_PARAMETER = new Object();
private final TransactionOperations transactionOperations;
private final DataSource dataSource;
private ExecutorAsyncOperations asyncOperations;
private ExecutorService executorService;
/**
* Default constructor.
*
* @param dataSourceName The data source name
* @param dataSource The datasource
* @param transactionOperations The JDBC operations for the data source
* @param executorService The executor service
* @param beanContext The bean context
* @param codecs The codecs
* @param dateTimeProvider The dateTimeProvider
*/
protected DefaultJdbcRepositoryOperations(@Parameter String dataSourceName,
DataSource dataSource,
@Parameter TransactionOperations transactionOperations,
@Named("io") @Nullable ExecutorService executorService,
BeanContext beanContext,
List codecs,
@NonNull DateTimeProvider dateTimeProvider) {
super(
new ColumnNameResultSetReader(),
new ColumnIndexResultSetReader(),
new JdbcQueryStatement(),
codecs,
dateTimeProvider
);
ArgumentUtils.requireNonNull("dataSource", dataSource);
ArgumentUtils.requireNonNull("transactionOperations", transactionOperations);
this.dataSource = dataSource;
this.transactionOperations = transactionOperations;
this.executorService = executorService;
Collection> beanDefinitions = beanContext.getBeanDefinitions(GenericRepository.class, Qualifiers.byStereotype(Repository.class));
for (BeanDefinition beanDefinition : beanDefinitions) {
String targetDs = beanDefinition.stringValue(Repository.class).orElse("default");
if (targetDs.equalsIgnoreCase(dataSourceName)) {
Dialect dialect = beanDefinition.enumValue(JdbcRepository.class, "dialect", Dialect.class).orElseGet(() -> beanDefinition.enumValue(JdbcRepository.class, "dialectName", Dialect.class).orElse(Dialect.ANSI));
dialects.put(beanDefinition.getBeanType(), dialect);
QueryBuilder qb = queryBuilders.get(dialect);
if (qb == null) {
queryBuilders.put(dialect, new SqlQueryBuilder(dialect));
}
}
}
}
@NonNull
private ExecutorService newLocalThreadPool() {
this.executorService = Executors.newCachedThreadPool();
return executorService;
}
@NonNull
@Override
public ExecutorAsyncOperations async() {
ExecutorAsyncOperations asyncOperations = this.asyncOperations;
if (asyncOperations == null) {
synchronized (this) { // double check
asyncOperations = this.asyncOperations;
if (asyncOperations == null) {
asyncOperations = new ExecutorAsyncOperations(
this,
executorService != null ? executorService : newLocalThreadPool()
);
this.asyncOperations = asyncOperations;
}
}
}
return asyncOperations;
}
@NonNull
@Override
public ReactiveRepositoryOperations reactive() {
return new ExecutorReactiveOperations(async());
}
@Nullable
@Override
public R findOne(@NonNull PreparedQuery preparedQuery) {
return transactionOperations.executeRead(status -> {
Connection connection = status.getConnection();
try (PreparedStatement ps = prepareStatement(connection, preparedQuery, false, true)) {
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
Class rootEntity = preparedQuery.getRootEntity();
Class resultType = preparedQuery.getResultType();
if (resultType == rootEntity) {
@SuppressWarnings("unchecked")
RuntimePersistentEntity persistentEntity = getEntity((Class) rootEntity);
TypeMapper mapper = new SqlResultEntityTypeMapper<>(
persistentEntity,
columnNameResultSetReader,
preparedQuery.getJoinFetchPaths(),
jsonCodec
);
R result = mapper.map(rs, resultType);
if (preparedQuery.hasResultConsumer()) {
preparedQuery.getParameterInRole(SqlResultConsumer.ROLE, SqlResultConsumer.class)
.ifPresent(consumer -> consumer.accept(result, newMappingContext(rs)));
}
return result;
} else {
if (preparedQuery.isDtoProjection()) {
RuntimePersistentEntity persistentEntity = getEntity(preparedQuery.getRootEntity());
TypeMapper introspectedDataMapper = new DTOMapper<>(
persistentEntity,
columnNameResultSetReader,
jsonCodec
);
return introspectedDataMapper.map(rs, resultType);
} else {
Object v = columnIndexResultSetReader.readDynamic(rs, 1, preparedQuery.getResultDataType());
if (resultType.isInstance(v)) {
return (R) v;
} else {
return columnIndexResultSetReader.convertRequired(v, resultType);
}
}
}
}
}
} catch (SQLException e) {
throw new DataAccessException("Error executing SQL Query: " + e.getMessage(), e);
}
return null;
});
}
@NonNull
private ResultConsumer.Context newMappingContext(ResultSet rs) {
return new ResultConsumer.Context() {
@Override
public ResultSet getResultSet() {
return rs;
}
@Override
public ResultReader getResultReader() {
return columnNameResultSetReader;
}
@NonNull
@Override
public E readEntity(String prefix, Class type) throws DataAccessException {
RuntimePersistentEntity entity = getEntity(type);
TypeMapper mapper = new SqlResultEntityTypeMapper<>(
prefix,
entity,
columnNameResultSetReader,
jsonCodec
);
return mapper.map(rs, type);
}
@NonNull
@Override
public D readDTO(@NonNull String prefix, @NonNull Class rootEntity, @NonNull Class dtoType) throws DataAccessException {
RuntimePersistentEntity entity = getEntity(rootEntity);
TypeMapper introspectedDataMapper = new DTOMapper<>(
entity,
columnNameResultSetReader,
jsonCodec
);
return introspectedDataMapper.map(rs, dtoType);
}
};
}
@Override
public boolean exists(@NonNull PreparedQuery preparedQuery) {
//noinspection ConstantConditions
return transactionOperations.executeRead(status -> {
try {
Connection connection = status.getConnection();
PreparedStatement ps = prepareStatement(connection, preparedQuery, false, true);
ResultSet rs = ps.executeQuery();
return rs.next();
} catch (SQLException e) {
throw new DataAccessException("Error executing SQL query: " + e.getMessage(), e);
}
});
}
@NonNull
@Override
public Stream findStream(@NonNull PreparedQuery preparedQuery) {
//noinspection ConstantConditions
return transactionOperations.executeRead(status -> {
Connection connection = status.getConnection();
return findStream(preparedQuery, connection);
});
}
private Stream findStream(@NonNull PreparedQuery preparedQuery, Connection connection) {
Class rootEntity = preparedQuery.getRootEntity();
Class resultType = preparedQuery.getResultType();
PreparedStatement ps;
try {
ps = prepareStatement(connection, preparedQuery, false, false);
} catch (SQLException e) {
throw new DataAccessException("SQL Error preparing Query: " + e.getMessage(), e);
}
ResultSet rs;
try {
rs = ps.executeQuery();
} catch (SQLException e) {
try {
ps.close();
} catch (SQLException e2) {
// ignore
}
throw new DataAccessException("SQL Error executing Query: " + e.getMessage(), e);
}
boolean dtoProjection = preparedQuery.isDtoProjection();
boolean isRootResult = resultType == rootEntity;
Spliterator spliterator;
AtomicBoolean finished = new AtomicBoolean();
if (isRootResult || dtoProjection) {
SqlResultConsumer sqlMappingConsumer = preparedQuery.hasResultConsumer() ? preparedQuery.getParameterInRole(SqlResultConsumer.ROLE, SqlResultConsumer.class).orElse(null) : null;
SqlTypeMapper mapper;
if (dtoProjection) {
mapper = new SqlDTOMapper<>(
getEntity(rootEntity),
columnNameResultSetReader,
jsonCodec
);
} else {
mapper = new SqlResultEntityTypeMapper<>(
getEntity(resultType),
columnNameResultSetReader,
preparedQuery.getJoinFetchPaths(),
jsonCodec
);
}
spliterator = new Spliterators.AbstractSpliterator(Long.MAX_VALUE,
Spliterator.ORDERED | Spliterator.IMMUTABLE) {
@Override
public boolean tryAdvance(Consumer super R> action) {
if (finished.get()) {
return false;
}
boolean hasNext = mapper.hasNext(rs);
if (hasNext) {
R o = mapper.map(rs, resultType);
if (sqlMappingConsumer != null) {
sqlMappingConsumer.accept(rs, o);
}
action.accept(o);
} else {
closeResultSet(ps, rs, finished);
}
return hasNext;
}
};
} else {
spliterator = new Spliterators.AbstractSpliterator(Long.MAX_VALUE,
Spliterator.ORDERED | Spliterator.IMMUTABLE) {
@Override
public boolean tryAdvance(Consumer super R> action) {
if (finished.get()) {
return false;
}
try {
boolean hasNext = rs.next();
if (hasNext) {
Object v = columnIndexResultSetReader.readDynamic(rs, 1, preparedQuery.getResultDataType());
if (resultType.isInstance(v)) {
//noinspection unchecked
action.accept((R) v);
} else {
Object r = columnIndexResultSetReader.convertRequired(v, resultType);
action.accept((R) r);
}
} else {
closeResultSet(ps, rs, finished);
}
return hasNext;
} catch (SQLException e) {
throw new DataAccessException("Error retrieving next JDBC result: " + e.getMessage(), e);
}
}
};
}
return StreamSupport.stream(spliterator, false).onClose(() -> {
closeResultSet(ps, rs, finished);
});
}
private void closeResultSet(PreparedStatement ps, ResultSet rs, AtomicBoolean finished) {
if (finished.compareAndSet(false, true)) {
try {
rs.close();
ps.close();
} catch (SQLException e) {
throw new DataAccessException("Error closing JDBC result stream: " + e.getMessage(), e);
}
}
}
@NonNull
@Override
public Iterable findAll(@NonNull PreparedQuery preparedQuery) {
return transactionOperations.executeRead(status -> {
Connection connection = status.getConnection();
return findStream(preparedQuery, connection).collect(Collectors.toList());
});
}
@NonNull
@Override
public Optional executeUpdate(@NonNull PreparedQuery, Number> preparedQuery) {
//noinspection ConstantConditions
return transactionOperations.executeWrite(status -> {
try {
Connection connection = status.getConnection();
try (PreparedStatement ps = prepareStatement(connection, preparedQuery, true, false)) {
int result = ps.executeUpdate();
if (QUERY_LOG.isTraceEnabled()) {
QUERY_LOG.trace("Update operation updated {} records", result);
}
return Optional.of(result);
}
} catch (SQLException e) {
throw new DataAccessException("Error executing SQL UPDATE: " + e.getMessage(), e);
}
});
}
@Override
public Optional deleteAll(@NonNull BatchOperation operation) {
throw new UnsupportedOperationException("The deleteAll method via batch is unsupported. Execute the SQL update directly");
}
@NonNull
@Override
public T update(@NonNull UpdateOperation operation) {
final AnnotationMetadata annotationMetadata = operation.getAnnotationMetadata();
final String[] params = annotationMetadata.stringValues(DataMethod.class, DataMethod.META_MEMBER_PARAMETER_BINDING_PATHS);
final String query = annotationMetadata.stringValue(Query.class).orElse(null);
final T entity = operation.getEntity();
final Set persisted = new HashSet(10);
final Class> repositoryType = operation.getRepositoryType();
return updateOne(repositoryType, annotationMetadata, query, params, entity, persisted);
}
private T updateOne(Class> repositoryType, AnnotationMetadata annotationMetadata, String query, String[] params, T entity, Set persisted) {
Objects.requireNonNull(entity, "Passed entity cannot be null");
if (StringUtils.isNotEmpty(query) && ArrayUtils.isNotEmpty(params)) {
final RuntimePersistentEntity persistentEntity =
(RuntimePersistentEntity) getEntity(entity.getClass());
return transactionOperations.executeWrite(status -> {
try {
Connection connection = status.getConnection();
if (QUERY_LOG.isDebugEnabled()) {
QUERY_LOG.debug("Executing SQL UPDATE: {}", query);
}
try (PreparedStatement ps = connection.prepareStatement(query)) {
for (int i = 0; i < params.length; i++) {
String propertyName = params[i];
RuntimePersistentProperty pp =
persistentEntity.getPropertyByName(propertyName);
if (pp == null) {
int j = propertyName.indexOf('.');
if (j > -1) {
RuntimePersistentProperty embeddedProp = (RuntimePersistentProperty)
persistentEntity.getPropertyByPath(propertyName).orElse(null);
if (embeddedProp != null) {
// embedded case
pp = persistentEntity.getPropertyByName(propertyName.substring(0, j));
if (pp instanceof Association) {
Association assoc = (Association) pp;
if (assoc.getKind() == Relation.Kind.EMBEDDED) {
Object embeddedInstance = pp.getProperty().get(entity);
Object embeddedValue = embeddedInstance != null ? embeddedProp.getProperty().get(embeddedInstance) : null;
int index = i + 1;
preparedStatementWriter.setDynamic(
ps,
index,
embeddedProp.getDataType(),
embeddedValue
);
}
}
} else {
throw new IllegalStateException("Cannot perform update for non-existent property: " + persistentEntity.getSimpleName() + "." + propertyName);
}
} else {
throw new IllegalStateException("Cannot perform update for non-existent property: " + persistentEntity.getSimpleName() + "." + propertyName);
}
} else {
final Object newValue;
final BeanProperty beanProperty = pp.getProperty();
if (beanProperty.hasAnnotation(DateUpdated.class)) {
newValue = dateTimeProvider.getNow();
beanProperty.convertAndSet(entity, newValue);
} else {
newValue = beanProperty.get(entity);
}
final DataType dataType = pp.getDataType();
if (dataType == DataType.ENTITY && newValue != null && pp instanceof Association) {
final RuntimePersistentProperty