From 04b2705bf9c4162f623dd48b6d04330dbe8b9fd4 Mon Sep 17 00:00:00 2001 From: Maciej Walkowiak Date: Thu, 11 Oct 2018 22:50:39 +0200 Subject: [PATCH 1/2] DATAJDBC-263 - publish events for @Query annotated methods --- .../support/JdbcQueryLookupStrategy.java | 8 ++- .../support/JdbcRepositoryFactory.java | 2 +- .../support/JdbcRepositoryQuery.java | 46 ++++++++++++++-- .../JdbcQueryLookupStrategyUnitTests.java | 4 +- .../support/JdbcRepositoryQueryUnitTests.java | 52 ++++++++++++++++++- 5 files changed, 102 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java index 0d23a40cb0..8e96da8959 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java @@ -17,6 +17,7 @@ import java.lang.reflect.Method; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.EntityRowMapper; import org.springframework.data.jdbc.repository.RowMapperMap; @@ -43,6 +44,7 @@ */ class JdbcQueryLookupStrategy implements QueryLookupStrategy { + private final ApplicationEventPublisher publisher; private final RelationalMappingContext context; private final RelationalConverter converter; private final DataAccessStrategy accessStrategy; @@ -58,14 +60,16 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { * @param accessStrategy must not be {@literal null}. * @param rowMapperMap must not be {@literal null}. */ - JdbcQueryLookupStrategy(RelationalMappingContext context, RelationalConverter converter, + JdbcQueryLookupStrategy(ApplicationEventPublisher publisher, RelationalMappingContext context, RelationalConverter converter, DataAccessStrategy accessStrategy, RowMapperMap rowMapperMap, NamedParameterJdbcOperations operations) { + Assert.notNull(publisher, "Publisher must not be null!"); Assert.notNull(context, "RelationalMappingContext must not be null!"); Assert.notNull(converter, "RelationalConverter must not be null!"); Assert.notNull(accessStrategy, "DataAccessStrategy must not be null!"); Assert.notNull(rowMapperMap, "RowMapperMap must not be null!"); + this.publisher = publisher; this.context = context; this.converter = converter; this.accessStrategy = accessStrategy; @@ -85,7 +89,7 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository RowMapper rowMapper = queryMethod.isModifyingQuery() ? null : createRowMapper(queryMethod); - return new JdbcRepositoryQuery(queryMethod, operations, rowMapper); + return new JdbcRepositoryQuery(publisher, context, queryMethod, operations, rowMapper); } private RowMapper createRowMapper(JdbcQueryMethod queryMethod) { diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java index e4eb0feded..30c1145423 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java @@ -133,6 +133,6 @@ protected Optional getQueryLookupStrategy(@Nullable QueryLo throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key)); } - return Optional.of(new JdbcQueryLookupStrategy(context, converter, accessStrategy, rowMapperMap, operations)); + return Optional.of(new JdbcQueryLookupStrategy(publisher, context, converter, accessStrategy, rowMapperMap, operations)); } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java index 3ba094c256..ddd66b4134 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java @@ -16,8 +16,12 @@ package org.springframework.data.jdbc.repository.support; import org.springframework.beans.BeanUtils; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.event.AfterLoadEvent; +import org.springframework.data.relational.core.mapping.event.Identifier; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; @@ -26,6 +30,8 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import java.util.List; + /** * A query to be executed based on a repository method, it's annotated SQL query and the arguments provided to the * method. @@ -33,11 +39,14 @@ * @author Jens Schauder * @author Kazuki Shimizu * @author Oliver Gierke + * @author Maciej Walkowiak */ class JdbcRepositoryQuery implements RepositoryQuery { private static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters. Use @Param for query method parameters, or when on Java 8+ use the javac flag -parameters."; + private final ApplicationEventPublisher publisher; + private final RelationalMappingContext context; private final JdbcQueryMethod queryMethod; private final NamedParameterJdbcOperations operations; private final RowMapper rowMapper; @@ -45,14 +54,16 @@ class JdbcRepositoryQuery implements RepositoryQuery { /** * Creates a new {@link JdbcRepositoryQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} and * {@link RowMapper}. - * + * * @param queryMethod must not be {@literal null}. * @param operations must not be {@literal null}. * @param defaultRowMapper can be {@literal null} (only in case of a modifying query). */ - JdbcRepositoryQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, + JdbcRepositoryQuery(ApplicationEventPublisher publisher, RelationalMappingContext context, JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, @Nullable RowMapper defaultRowMapper) { + Assert.notNull(publisher, "Publisher must not be null!"); + Assert.notNull(context, "Context must not be null!"); Assert.notNull(queryMethod, "Query method must not be null!"); Assert.notNull(operations, "NamedParameterJdbcOperations must not be null!"); @@ -60,6 +71,8 @@ class JdbcRepositoryQuery implements RepositoryQuery { Assert.notNull(defaultRowMapper, "RowMapper must not be null!"); } + this.publisher = publisher; + this.context = context; this.queryMethod = queryMethod; this.operations = operations; this.rowMapper = createRowMapper(queryMethod, defaultRowMapper); @@ -84,11 +97,15 @@ public Object execute(Object[] objects) { } if (queryMethod.isCollectionQuery() || queryMethod.isStreamQuery()) { - return operations.query(query, parameters, rowMapper); + List result = operations.query(query, parameters, rowMapper); + publishAfterLoad(result); + return result; } try { - return operations.queryForObject(query, parameters, rowMapper); + Object result = operations.queryForObject(query, parameters, rowMapper); + publishAfterLoad(result); + return result; } catch (EmptyResultDataAccessException e) { return null; } @@ -136,4 +153,25 @@ private static RowMapper createRowMapper(JdbcQueryMethod queryMethod, @Nullab ? defaultRowMapper // : (RowMapper) BeanUtils.instantiateClass(rowMapperClass); } + + private void publishAfterLoad(Iterable all) { + + for (T e : all) { + publishAfterLoad(e); + } + } + + private void publishAfterLoad(@Nullable T entity) { + + if (entity != null && context.hasPersistentEntityFor(entity.getClass())) { + RelationalPersistentEntity e = context.getRequiredPersistentEntity(entity.getClass()); + Object identifier = e.getIdentifierAccessor(entity) + .getIdentifier(); + + if (identifier != null) { + publisher.publishEvent(new AfterLoadEvent(Identifier.of(identifier), entity)); + } + } + + } } diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java index ea8db06ce2..a5b61df2ac 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java @@ -25,6 +25,7 @@ import org.junit.Before; import org.junit.Test; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.repository.RowMapperMap; import org.springframework.data.jdbc.repository.config.ConfigurableRowMapperMap; @@ -49,6 +50,7 @@ */ public class JdbcQueryLookupStrategyUnitTests { + ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class); RelationalMappingContext mappingContext = mock(RelationalMappingContext.class, RETURNS_DEEP_STUBS); RelationalConverter converter = mock(BasicRelationalConverter.class); DataAccessStrategy accessStrategy = mock(DataAccessStrategy.class); @@ -82,7 +84,7 @@ public void typeBasedRowMapperGetsUsedForQuery() { private RepositoryQuery getRepositoryQuery(String name, RowMapperMap rowMapperMap) { - JdbcQueryLookupStrategy queryLookupStrategy = new JdbcQueryLookupStrategy(mappingContext, converter, accessStrategy, + JdbcQueryLookupStrategy queryLookupStrategy = new JdbcQueryLookupStrategy(publisher, mappingContext, converter, accessStrategy, rowMapperMap, operations); return queryLookupStrategy.resolveQuery(getMethod(name), metadata, projectionFactory, namedQueries); diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQueryUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQueryUnitTests.java index 7c68578ba9..85bb067913 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQueryUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQueryUnitTests.java @@ -19,10 +19,14 @@ import static org.mockito.Mockito.*; import java.sql.ResultSet; +import java.util.Arrays; import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Test; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.event.AfterLoadEvent; import org.springframework.data.repository.query.DefaultParameters; import org.springframework.data.repository.query.Parameters; import org.springframework.jdbc.core.RowMapper; @@ -42,6 +46,8 @@ public class JdbcRepositoryQueryUnitTests { RowMapper defaultRowMapper; JdbcRepositoryQuery query; NamedParameterJdbcOperations operations; + ApplicationEventPublisher publisher; + RelationalMappingContext context; @Before public void setup() throws NoSuchMethodException { @@ -54,8 +60,10 @@ public void setup() throws NoSuchMethodException { this.defaultRowMapper = mock(RowMapper.class); this.operations = mock(NamedParameterJdbcOperations.class); + this.publisher = mock(ApplicationEventPublisher.class); + this.context = mock(RelationalMappingContext.class, RETURNS_DEEP_STUBS); - this.query = new JdbcRepositoryQuery(queryMethod, operations, defaultRowMapper); + this.query = new JdbcRepositoryQuery(publisher, context, queryMethod, operations, defaultRowMapper); } @Test // DATAJDBC-165 @@ -94,12 +102,40 @@ public void customRowMapperIsUsedWhenSpecified() { doReturn("some sql statement").when(queryMethod).getAnnotatedQuery(); doReturn(CustomRowMapper.class).when(queryMethod).getRowMapperClass(); - new JdbcRepositoryQuery(queryMethod, operations, defaultRowMapper).execute(new Object[] {}); + new JdbcRepositoryQuery(publisher, context, queryMethod, operations, defaultRowMapper).execute(new Object[] {}); verify(operations) // .queryForObject(anyString(), any(SqlParameterSource.class), isA(CustomRowMapper.class)); } + @Test + public void publishesSingleEventWhenQueryReturnsSingleElement() { + + doReturn("some sql statement").when(queryMethod).getAnnotatedQuery(); + doReturn(false).when(queryMethod).isCollectionQuery(); + doReturn(new DummyEntity(1L)).when(operations).queryForObject(anyString(), any(SqlParameterSource.class), any(RowMapper.class)); + doReturn(true).when(context).hasPersistentEntityFor(DummyEntity.class); + when(context.getRequiredPersistentEntity(DummyEntity.class).getIdentifierAccessor(any()).getRequiredIdentifier()).thenReturn("some identifier"); + + new JdbcRepositoryQuery(publisher, context, queryMethod, operations, defaultRowMapper).execute(new Object[] {}); + + verify(publisher).publishEvent(any(AfterLoadEvent.class)); + } + + @Test + public void publishesAsManyEventsAsReturnedEntities() { + + doReturn("some sql statement").when(queryMethod).getAnnotatedQuery(); + doReturn(true).when(queryMethod).isCollectionQuery(); + doReturn(Arrays.asList(new DummyEntity(1L), new DummyEntity(1L))).when(operations).query(anyString(), any(SqlParameterSource.class), any(RowMapper.class)); + doReturn(true).when(context).hasPersistentEntityFor(DummyEntity.class); + when(context.getRequiredPersistentEntity(DummyEntity.class).getIdentifierAccessor(any()).getRequiredIdentifier()).thenReturn("some identifier"); + + new JdbcRepositoryQuery(publisher, context, queryMethod, operations, defaultRowMapper).execute(new Object[] {}); + + verify(publisher, times(2)).publishEvent(any(AfterLoadEvent.class)); + } + /** * The whole purpose of this method is to easily generate a {@link DefaultParameters} instance during test setup. */ @@ -113,4 +149,16 @@ public Object mapRow(ResultSet rs, int rowNum) { return null; } } + + private static class DummyEntity { + private Long id; + + public DummyEntity(Long id) { + this.id = id; + } + + Long getId() { + return id; + } + } } From 65b8099ee864df9940faa04529115700a499ae51 Mon Sep 17 00:00:00 2001 From: Maciej Walkowiak Date: Thu, 11 Oct 2018 23:16:16 +0200 Subject: [PATCH 2/2] DATAJDBC-263 - add missing Javadocs. --- .../data/jdbc/repository/support/JdbcQueryLookupStrategy.java | 1 + .../data/jdbc/repository/support/JdbcRepositoryQuery.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java index 8e96da8959..099740e310 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java @@ -55,6 +55,7 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { * Creates a new {@link JdbcQueryLookupStrategy} for the given {@link RelationalMappingContext}, * {@link DataAccessStrategy} and {@link RowMapperMap}. * + * @param publisher must not be {@literal null}. * @param context must not be {@literal null}. * @param converter must not be {@literal null}. * @param accessStrategy must not be {@literal null}. diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java index ddd66b4134..246447abbe 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java @@ -55,6 +55,8 @@ class JdbcRepositoryQuery implements RepositoryQuery { * Creates a new {@link JdbcRepositoryQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} and * {@link RowMapper}. * + * @param publisher must not be {@literal null}. + * @param context must not be {@literal null}. * @param queryMethod must not be {@literal null}. * @param operations must not be {@literal null}. * @param defaultRowMapper can be {@literal null} (only in case of a modifying query).