diff --git a/src/main/asciidoc/repository.adoc b/src/main/asciidoc/repository.adoc index 3b5246e4..b1cf4e8b 100644 --- a/src/main/asciidoc/repository.adoc +++ b/src/main/asciidoc/repository.adoc @@ -378,15 +378,20 @@ One aspect that is often needed and doesn't have a direct equivalent in the Spri `query consistency`. In both view-based queries and N1QL, you have this concept that the secondary index can return stale data, because the latest version hasn't been indexed yet. This gives the best performance at the expense of consistency. +Note that weaker consistencies can lead to data being returned that doesn't match the criteria of a derived query. + If one wants to have stronger consistency, there are two possibilities described in the next sections. ==== Configure it on a global level -The global consistency used by generated queries (views and N1QL) is defined at the template level, -using `Consistency` enumeration (like `Consistency.READ_YOUR_OWN_WRITE`): +A global consistency can be defined using the `Consistency` enumeration (eg. `Consistency.READ_YOUR_OWN_WRITE`): - in xml, this is done via the `consistency` attribute on ``. - in javaConfig, this is done by overriding the `getDefaultConsistency()` method. +By default it is `Consistency.UPDATE_AFTER` (which means speed is prioritized over consistency, but the view index will be updated after each request). + +IMPORTANT: This is **only used in repositories**, either for index-backed methods automatically provided by the repository interface (`findAll()`, `findAll(keys)`, `count()`, `deleteAll()`...) or methods you define in your specific interface using query derivation. + ==== Provide an implementation Provide the implementation and directly use `queryView` and `queryN1QL` methods on the template with a specific consistency (see <>). diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java index c257c64e..23837bad 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java @@ -134,6 +134,7 @@ public class SimpleCouchbaseRepository implements Co final ResolvedView resolvedView = determineView(); ViewQuery query = ViewQuery.from(resolvedView.getDesignDocument(), resolvedView.getViewName()); query.reduce(false); + query.stale(getCouchbaseOperations().getDefaultConsistency().viewConsistency()); return couchbaseOperations.findByView(query, entityInformation.getJavaType()); } @@ -142,6 +143,7 @@ public class SimpleCouchbaseRepository implements Co final ResolvedView resolvedView = determineView(); ViewQuery query = ViewQuery.from(resolvedView.getDesignDocument(), resolvedView.getViewName()); query.reduce(false); + query.stale(getCouchbaseOperations().getDefaultConsistency().viewConsistency()); JsonArray keys = JsonArray.create(); for (ID id : ids) { keys.add(id); @@ -156,6 +158,7 @@ public class SimpleCouchbaseRepository implements Co final ResolvedView resolvedView = determineView(); ViewQuery query = ViewQuery.from(resolvedView.getDesignDocument(), resolvedView.getViewName()); query.reduce(true); + query.stale(getCouchbaseOperations().getDefaultConsistency().viewConsistency()); ViewResult response = couchbaseOperations.queryView(query); @@ -172,6 +175,8 @@ public class SimpleCouchbaseRepository implements Co final ResolvedView resolvedView = determineView(); ViewQuery query = ViewQuery.from(resolvedView.getDesignDocument(), resolvedView.getViewName()); query.reduce(false); + query.stale(getCouchbaseOperations().getDefaultConsistency().viewConsistency()); + ViewResult response = couchbaseOperations.queryView(query); for (ViewRow row : response) { diff --git a/src/test/java/org/springframework/data/couchbase/repository/RepositoryIndexUsageTest.java b/src/test/java/org/springframework/data/couchbase/repository/RepositoryIndexUsageTest.java new file mode 100644 index 00000000..97fbde66 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/RepositoryIndexUsageTest.java @@ -0,0 +1,167 @@ +package org.springframework.data.couchbase.repository; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.document.json.JsonObject; +import com.couchbase.client.java.query.N1qlQuery; +import com.couchbase.client.java.view.ViewQuery; +import com.couchbase.client.java.view.ViewResult; +import com.couchbase.client.java.view.ViewRow; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.query.Consistency; +import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; +import org.springframework.data.couchbase.repository.support.N1qlCouchbaseRepository; +import org.springframework.data.couchbase.repository.support.ViewMetadataProvider; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + +public class RepositoryIndexUsageTest { + + private static final org.springframework.data.couchbase.core.query.Consistency CONSISTENCY = Consistency.STRONGLY_CONSISTENT; + + private CouchbaseOperations couchbaseOperations; + private N1qlCouchbaseRepository repository; + + @Before + public void initMocks() { + ViewRow mockCountRow1 = mock(ViewRow.class); + when(mockCountRow1.value()).thenReturn("100"); + ViewRow mockCountRow2 = mock(ViewRow.class); + when(mockCountRow2.value()).thenReturn("200"); + List allCountRows = Arrays.asList(mockCountRow1, mockCountRow2); + + ViewResult mockCountResult = mock(ViewResult.class); + when(mockCountResult.iterator()).thenReturn(allCountRows.iterator()); + + Bucket mockBucket = mock(Bucket.class); + when(mockBucket.name()).thenReturn("mockBucket"); + + CouchbaseConverter mockConverter = mock(CouchbaseConverter.class); + when(mockConverter.getTypeKey()).thenReturn("mockType"); + + couchbaseOperations = mock(CouchbaseOperations.class); + when(couchbaseOperations.getDefaultConsistency()).thenReturn(CONSISTENCY); + when(couchbaseOperations.getCouchbaseBucket()).thenReturn(mockBucket); + when(couchbaseOperations.getConverter()).thenReturn(mockConverter); + when(couchbaseOperations.findByView(any(ViewQuery.class), any(Class.class))).thenReturn(allCountRows); + when(couchbaseOperations.findByN1QL(any(N1qlQuery.class), any(Class.class))).thenReturn(Collections.emptyList()); + when(couchbaseOperations.queryView(any(ViewQuery.class))).thenReturn(mockCountResult); + when(couchbaseOperations.queryN1QL(any(N1qlQuery.class))).thenReturn(null); + + CouchbaseEntityInformation metadata = mock(CouchbaseEntityInformation.class); + when(metadata.getJavaType()).thenReturn(String.class); + + repository = new N1qlCouchbaseRepository(metadata, couchbaseOperations); + repository.setViewMetadataProvider(mock(ViewMetadataProvider.class)); + } + + @Test + public void testFindAllUsesViewWithConfiguredConsistency() { + String expectedQueryParams = "reduce=false&stale=false"; + repository.findAll(); + + verify(couchbaseOperations, never()).queryView(any(ViewQuery.class)); + verify(couchbaseOperations, never()).findByN1QL(any(N1qlQuery.class), any(Class.class)); + verify(couchbaseOperations, never()).queryN1QL(any(N1qlQuery.class)); + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(ViewQuery.class); + verify(couchbaseOperations).findByView(queryCaptor.capture(), any(Class.class)); + String sQuery = queryCaptor.getValue().toString(); + assertEquals(expectedQueryParams, sQuery); + } + + @Test + public void testFindAllKeysUsesViewWithConfiguredConsistency() { + String expectedQueryParams = "reduce=false&stale=false"; + repository.findAll(Collections.singleton("someKey")); + + verify(couchbaseOperations, never()).queryView(any(ViewQuery.class)); + verify(couchbaseOperations, never()).findByN1QL(any(N1qlQuery.class), any(Class.class)); + verify(couchbaseOperations, never()).queryN1QL(any(N1qlQuery.class)); + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(ViewQuery.class); + verify(couchbaseOperations).findByView(queryCaptor.capture(), any(Class.class)); + String sQuery = queryCaptor.getValue().toString(); + assertEquals(expectedQueryParams, sQuery); + } + + @Test + public void testCountUsesViewWithConfiguredConsistencyAndReduces() { + String expectedQueryParams = "reduce=true&stale=false"; + repository.count(); + + verify(couchbaseOperations, never()).findByView(any(ViewQuery.class), any(Class.class)); + verify(couchbaseOperations, never()).findByN1QL(any(N1qlQuery.class), any(Class.class)); + verify(couchbaseOperations, never()).queryN1QL(any(N1qlQuery.class)); + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(ViewQuery.class); + verify(couchbaseOperations).queryView(queryCaptor.capture()); + String sQuery = queryCaptor.getValue().toString(); + assertEquals(expectedQueryParams, sQuery); + } + + @Test + public void testCountParsesAndAddsLongValuesFromRows() { + long count = repository.count(); + assertEquals(300L, count); + } + + @Test + public void testDeleteAllUsesViewWithConfiguredConsistency() { + String expectedQueryParams = "reduce=false&stale=false"; + repository.deleteAll(); + + verify(couchbaseOperations, never()).findByView(any(ViewQuery.class), any(Class.class)); + verify(couchbaseOperations, never()).findByN1QL(any(N1qlQuery.class), any(Class.class)); + verify(couchbaseOperations, never()).queryN1QL(any(N1qlQuery.class)); + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(ViewQuery.class); + verify(couchbaseOperations).queryView(queryCaptor.capture()); + String sQuery = queryCaptor.getValue().toString(); + assertEquals(expectedQueryParams, sQuery); + } + + @Test + public void testFindAllSortedUsesN1qlWithConfiguredConsistencyAndOrderBy() { + String expectedOrderClause = "ORDER BY `length` ASC"; + Sort sort = new Sort(Sort.Direction.ASC, "length"); + repository.findAll(sort); + + verify(couchbaseOperations, never()).findByView(any(ViewQuery.class), any(Class.class)); + verify(couchbaseOperations, never()).queryView(any(ViewQuery.class)); + verify(couchbaseOperations, never()).queryN1QL(any(N1qlQuery.class)); + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(N1qlQuery.class); + verify(couchbaseOperations).findByN1QL(queryCaptor.capture(), any(Class.class)); + + JsonObject query = queryCaptor.getValue().n1ql(); + assertEquals(CONSISTENCY.n1qlConsistency().n1ql(), query.getString("scan_consistency")); + String statement = query.getString("statement"); + assertTrue("Expected " + expectedOrderClause + " in " + statement, statement.contains(expectedOrderClause)); + } + + @Test + public void testFindAllPagedUsesUsesN1qlConfiguredConsistencyAndLimitOffset() { + String expectedLimitClause = "LIMIT 10 OFFSET 0"; + repository.findAll(new PageRequest(0, 10)); + + verify(couchbaseOperations, never()).findByView(any(ViewQuery.class), any(Class.class)); + verify(couchbaseOperations, never()).queryView(any(ViewQuery.class)); + verify(couchbaseOperations, never()).queryN1QL(any(N1qlQuery.class)); + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(N1qlQuery.class); + verify(couchbaseOperations).findByN1QL(queryCaptor.capture(), any(Class.class)); + + JsonObject query = queryCaptor.getValue().n1ql(); + assertEquals(CONSISTENCY.n1qlConsistency().n1ql(), query.getString("scan_consistency")); + String statement = query.getString("statement"); + assertTrue("Expected " + expectedLimitClause + " in " + statement, statement.contains(expectedLimitClause)); + } +} \ No newline at end of file