diff --git a/gradle.properties b/gradle.properties index fe013fce..9d65effe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ junitVersion=4.8.2 gemfireVersion=7.0.1 spring.range="[3.2.0, 4.0.0)" springVersion=3.2.2.RELEASE -springDataCommonsVersion=1.5.0.RELEASE +springDataCommonsVersion=1.6.0.BUILD-SNAPSHOT log4jVersion=1.2.16 hamcrestVersion=1.2.1 version=1.3.0.BUILD-SNAPSHOT diff --git a/src/main/java/org/springframework/data/gemfire/repository/Query.java b/src/main/java/org/springframework/data/gemfire/repository/Query.java index b6e6cfd2..0a1f9834 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/Query.java +++ b/src/main/java/org/springframework/data/gemfire/repository/Query.java @@ -31,4 +31,5 @@ import java.lang.annotation.Target; public @interface Query { String value() default ""; + } diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryCreator.java b/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryCreator.java index 0d28e039..51d2ba27 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryCreator.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryCreator.java @@ -47,7 +47,7 @@ class GemfireQueryCreator extends AbstractQueryCreator super(tree); - this.query = new QueryBuilder(entity); + this.query = new QueryBuilder(entity,tree); this.indexes = new IndexProvider(); } diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java b/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java index 0018bb5e..36a88d4a 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java @@ -23,6 +23,7 @@ import org.springframework.util.Assert; * Base class for GemFire specific {@link RepositoryQuery} implementations. * * @author Oliver Gierke + * @author David Turanski */ abstract class GemfireRepositoryQuery implements RepositoryQuery { @@ -47,4 +48,6 @@ abstract class GemfireRepositoryQuery implements RepositoryQuery { public QueryMethod getQueryMethod() { return this.queryMethod; } + + protected abstract boolean isCountQuery(); } diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java b/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java index 4e3edc06..08ec5a2c 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java @@ -65,7 +65,7 @@ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery { QueryString query = new GemfireQueryCreator(tree, method.getPersistentEntity()).createQuery(parameterAccessor .getSort()); - RepositoryQuery repositoryQuery = new StringBasedGemfireRepositoryQuery(query.toString(), method, template); + RepositoryQuery repositoryQuery = new StringBasedGemfireRepositoryQuery(query.toString(), method, template,isCountQuery()); return repositoryQuery.execute(prepareStringParameters(parameters)); } @@ -102,4 +102,12 @@ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery { return result; } + + /* (non-Javadoc) + * @see org.springframework.data.gemfire.repository.query.GemfireRepositoryQuery#isCountQuery() + */ + @Override + protected boolean isCountQuery() { + return this.tree.isCountProjection(); + } } diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/QueryBuilder.java b/src/main/java/org/springframework/data/gemfire/repository/query/QueryBuilder.java index 3d1978f9..8dd87b75 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/QueryBuilder.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/QueryBuilder.java @@ -16,11 +16,13 @@ package org.springframework.data.gemfire.repository.query; import org.springframework.data.gemfire.mapping.GemfirePersistentEntity; +import org.springframework.data.repository.query.parser.PartTree; import org.springframework.util.Assert; /** * * @author Oliver Gierke + * @author David Turanski */ class QueryBuilder { @@ -33,8 +35,8 @@ class QueryBuilder { this.query = source; } - public QueryBuilder(GemfirePersistentEntity entity) { - this(String.format("SELECT * FROM /%s %s", entity.getRegionName(), DEFAULT_ALIAS)); + public QueryBuilder(GemfirePersistentEntity entity, PartTree tree) { + this(String.format(tree.isCountProjection()? "SELECT count(*) FROM /%s %s":"SELECT * FROM /%s %s", entity.getRegionName(), DEFAULT_ALIAS)); } public QueryString create(Predicate predicate) { diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java b/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java index 45807d16..5003cc05 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java @@ -30,6 +30,7 @@ import com.gemstone.gemfire.cache.Region; * Value object to work with OQL query strings. * * @author Oliver Gierke + * @author David Turanski */ class QueryString { @@ -56,7 +57,18 @@ class QueryString { * @param domainClass must not be {@literal null}. */ public QueryString(Class domainClass) { - this(String.format("SELECT * FROM /%s", domainClass.getSimpleName())); + this(domainClass, false); + } + + /** + * Creates a {@literal SELECT} query for the given domain class. + * + * @param domainClass must not be {@literal null}. + * @param isCountQuery indicates if this is a count query + */ + public QueryString(Class domainClass, boolean isCountQuery) { + this(String + .format(isCountQuery ? "SELECT count(*) FROM /%s" : "SELECT * FROM /%s", domainClass.getSimpleName())); } /** @@ -66,7 +78,7 @@ class QueryString { * @param region must not be {@literal null}. * @return */ - public QueryString forRegion(Class domainClass, Region region) { + public QueryString forRegion(Class domainClass, Region region) { return new QueryString(query.replaceAll(REGION_PATTERN, region.getName())); } diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java b/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java index 6818818b..c65140d3 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java @@ -32,11 +32,13 @@ import com.gemstone.gemfire.cache.query.internal.ResultsBag; * {@link GemfireRepositoryQuery} using plain {@link String} based OQL queries. * * @author Oliver Gierke + * @author David Turanski */ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { private static final String INVALID_EXECUTION = "Paging and modifying queries are not supported!"; + private final boolean isCountProjection; private final QueryString query; private final GemfireQueryMethod method; private final GemfireTemplate template; @@ -49,7 +51,7 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { * @param template must not be {@literal null}. */ public StringBasedGemfireRepositoryQuery(GemfireQueryMethod method, GemfireTemplate template) { - this(method.getAnnotatedQuery(), method, template); + this(method.getAnnotatedQuery(), method, template, null); } /** @@ -61,7 +63,8 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { * @param method must not be {@literal null}. * @param template must not be {@literal null}. */ - public StringBasedGemfireRepositoryQuery(String query, GemfireQueryMethod method, GemfireTemplate template) { + public StringBasedGemfireRepositoryQuery(String query, GemfireQueryMethod method, GemfireTemplate template, + Boolean isCountProjection) { super(method); @@ -70,6 +73,11 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { this.query = new QueryString(StringUtils.hasText(query) ? query : method.getAnnotatedQuery()); this.method = method; this.template = template; + if (isCountProjection == null && StringUtils.hasText(method.getAnnotatedQuery())) { + this.isCountProjection = method.getAnnotatedQuery().toLowerCase().contains("count(*)"); + } else { + this.isCountProjection = isCountProjection; + } if (method.isPageQuery() || method.isModifyingQuery()) { throw new IllegalStateException(INVALID_EXECUTION); @@ -96,7 +104,6 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { if (method.isCollectionQuery()) { return result; } else if (method.isQueryForEntity()) { - if (result.isEmpty()) { return null; } else if (result.size() == 1) { @@ -104,9 +111,16 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { } else { throw new IncorrectResultSizeDataAccessException(1, result.size()); } + } else if (isCountProjection) { + int count = (Integer) result.iterator().next(); + if (long.class.isAssignableFrom(method.getReturnedObjectType())) { + return (long) count; + } else { + return count; + } } else { - throw new IllegalStateException(INVALID_EXECUTION); + throw new IllegalStateException("Unsupported query: " + query.toString()); } } @@ -130,4 +144,12 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source); } + + /* (non-Javadoc) + * @see org.springframework.data.gemfire.repository.query.GemfireRepositoryQuery#isCountQuery() + */ + @Override + protected boolean isCountQuery() { + return this.isCountProjection; + } } diff --git a/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactory.java b/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactory.java index 80577347..ce2c7a04 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactory.java +++ b/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactory.java @@ -44,6 +44,7 @@ import com.gemstone.gemfire.cache.Region; * for Gemfire. * * @author Oliver Gierke + * @author David Turanski */ public class GemfireRepositoryFactory extends RepositoryFactorySupport { @@ -164,7 +165,7 @@ public class GemfireRepositoryFactory extends RepositoryFactorySupport { String namedQueryName = queryMethod.getNamedQueryName(); if (namedQueries.hasQuery(namedQueryName)) { return new StringBasedGemfireRepositoryQuery(namedQueries.getQuery(namedQueryName), queryMethod, - template); + template,false); } return new PartTreeGemfireRepositoryQuery(queryMethod, template); diff --git a/src/test/java/org/springframework/data/gemfire/repository/sample/PersonRepository.java b/src/test/java/org/springframework/data/gemfire/repository/sample/PersonRepository.java index af406598..0471f0b8 100644 --- a/src/test/java/org/springframework/data/gemfire/repository/sample/PersonRepository.java +++ b/src/test/java/org/springframework/data/gemfire/repository/sample/PersonRepository.java @@ -17,15 +17,16 @@ package org.springframework.data.gemfire.repository.sample; import java.util.Collection; +import org.springframework.data.gemfire.repository.GemfireRepository; import org.springframework.data.gemfire.repository.Query; -import org.springframework.data.repository.CrudRepository; /** * Sample repository interface managing {@link Person}s. * * @author Oliver Gierke + * @author David Turanski */ -public interface PersonRepository extends CrudRepository { +public interface PersonRepository extends GemfireRepository { @Query("SELECT * FROM /Person p WHERE p.firstname = $1") Collection findByFirstnameAnnotated(String firstname); @@ -52,4 +53,11 @@ public interface PersonRepository extends CrudRepository { Collection findByLastnameEndingWith(String lastname); Collection findByFirstnameContaining(String firstname); + + long countByFirstname(String firstname); + + int countByLastname(String lastname); + + @Query("SELECT count(*) FROM /Person p WHERE p.firstname = $1") + int countFirstNameManual(String firstName); } diff --git a/src/test/java/org/springframework/data/gemfire/repository/support/PersonRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/repository/support/PersonRepositoryIntegrationTests.java new file mode 100644 index 00000000..09db3f1d --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/repository/support/PersonRepositoryIntegrationTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2013 the original author or 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 + * + * http://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 org.springframework.data.gemfire.repository.support; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.gemfire.repository.sample.Person; +import org.springframework.data.gemfire.repository.sample.PersonRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author David Turanski + * + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class PersonRepositoryIntegrationTests { + @Autowired + PersonRepository repository; + + @Test + public void testCountProjections() { + Person dave1 = new Person(1L,"Dave","Matthews"); + Person dave2 = new Person(2L,"Dave","Turanski"); + repository.save(dave1); + repository.save(dave2); + assertEquals(2L,repository.countByFirstname("Dave")); + assertEquals(1,repository.countByLastname("Matthews")); + assertEquals(2,repository.countFirstNameManual("Dave")); + } +} diff --git a/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryIntegrationTest.java b/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryIntegrationTest.java index a70c6872..98793f32 100644 --- a/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryIntegrationTest.java +++ b/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryIntegrationTest.java @@ -55,21 +55,20 @@ public class SimpleGemfireRepositoryIntegrationTest { @Autowired GemfireTemplate template; - - @Resource(name="simple") - Region simpleRegion; + + @Resource(name = "simple") + Region simpleRegion; SimpleGemfireRepository repository; - - + RegionClearListener regionClearListener; @SuppressWarnings("unchecked") @Before public void setUp() { + simpleRegion.clear(); regionClearListener = new RegionClearListener(); simpleRegion.getAttributesMutator().addCacheListener(regionClearListener); - EntityInformation information = new ReflectionEntityInformation(Person.class); repository = new SimpleGemfireRepository(template, information); } @@ -91,7 +90,7 @@ public class SimpleGemfireRepositoryIntegrationTest { assertThat(repository.findOne(person.id), is(nullValue())); assertThat(repository.findAll().size(), is(0)); } - + @Test public void testDeleteAllFiresClearEvent() { assertFalse(regionClearListener.eventFired); @@ -106,7 +105,8 @@ public class SimpleGemfireRepositoryIntegrationTest { template.put(1L, person); - SelectResults persons = template.find("SELECT * FROM /simple s WHERE s.firstname = $1", person.firstname); + SelectResults persons = template.find("SELECT * FROM /simple s WHERE s.firstname = $1", + person.firstname); assertThat(persons.size(), is(1)); assertThat(persons.iterator().next(), is(person)); @@ -127,10 +127,11 @@ public class SimpleGemfireRepositoryIntegrationTest { assertThat(result, hasItems(carter, leroi)); assertThat(result, not(hasItems(dave))); } - + @SuppressWarnings("rawtypes") public static class RegionClearListener extends CacheListenerAdapter { public boolean eventFired; + @Override public void afterRegionClear(RegionEvent ev) { eventFired = true; diff --git a/src/test/resources/org/springframework/data/gemfire/repository/support/PersonRepositoryIntegrationTests-context.xml b/src/test/resources/org/springframework/data/gemfire/repository/support/PersonRepositoryIntegrationTests-context.xml new file mode 100644 index 00000000..a34fbeb2 --- /dev/null +++ b/src/test/resources/org/springframework/data/gemfire/repository/support/PersonRepositoryIntegrationTests-context.xml @@ -0,0 +1,13 @@ + + + + + + +