Skip to content

Commit 11f3bc3

Browse files
Merge branch '34-query-cloning' into 'dev'
Support cloning queries Closes #34 See merge request objectbox/objectbox-java!41
2 parents 3fdf345 + f6e435e commit 11f3bc3

File tree

3 files changed

+171
-0
lines changed

3 files changed

+171
-0
lines changed

objectbox-java/src/main/java/io/objectbox/query/Query.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ public class Query<T> implements Closeable {
5555

5656
native void nativeDestroy(long handle);
5757

58+
/** Clones the native query, incl. conditions and parameters, and returns a handle to the clone. */
59+
native long nativeClone(long handle);
60+
5861
native Object nativeFindFirst(long handle, long cursorHandle);
5962

6063
native Object nativeFindUnique(long handle, long cursorHandle);
@@ -129,6 +132,20 @@ native void nativeSetParameter(long handle, int entityId, int propertyId, @Nulla
129132
this.comparator = comparator;
130133
}
131134

135+
/**
136+
* Creates a copy of the {@code originalQuery}, but pointing to a different native query using {@code handle}.
137+
*/
138+
// Note: not using recommended copy constructor (just passing this) as handle needs to change.
139+
private Query(Query<T> originalQuery, long handle) {
140+
this(
141+
originalQuery.box,
142+
handle,
143+
originalQuery.eagerRelations,
144+
originalQuery.filter,
145+
originalQuery.comparator
146+
);
147+
}
148+
132149
/**
133150
* Explicitly call {@link #close()} instead to avoid expensive finalization.
134151
*/
@@ -156,6 +173,22 @@ public synchronized void close() {
156173
}
157174
}
158175

176+
/**
177+
* Creates a copy of this for use in another thread.
178+
* <p>
179+
* Clones the native query, keeping any previously set parameters.
180+
* <p>
181+
* Closing the original query does not close the copy. {@link #close()} the copy once finished using it.
182+
* <p>
183+
* Note: a set {@link QueryBuilder#filter(QueryFilter) filter} or {@link QueryBuilder#sort(Comparator) sort}
184+
* order <b>must be thread safe</b>.
185+
*/
186+
// Note: not overriding clone() to avoid confusion with Java's cloning mechanism.
187+
public Query<T> copy() {
188+
long cloneHandle = nativeClone(handle);
189+
return new Query<>(this, cloneHandle);
190+
}
191+
159192
/** To be called inside a read TX */
160193
long cursorHandle() {
161194
return InternalAccess.getActiveTxCursorHandle(box);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.objectbox.query;
2+
3+
/**
4+
* A {@link ThreadLocal} that, given an original {@link Query} object,
5+
* returns a {@link Query#copy() copy}, for each thread.
6+
*/
7+
public class QueryThreadLocal<T> extends ThreadLocal<Query<T>> {
8+
9+
private final Query<T> original;
10+
11+
/**
12+
* See {@link QueryThreadLocal}.
13+
*/
14+
public QueryThreadLocal(Query<T> original) {
15+
this.original = original;
16+
}
17+
18+
@Override
19+
protected Query<T> initialValue() {
20+
return original.copy();
21+
}
22+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package io.objectbox.query;
2+
3+
import io.objectbox.TestEntity;
4+
import io.objectbox.TestEntity_;
5+
import org.junit.Test;
6+
7+
import java.util.Comparator;
8+
import java.util.List;
9+
import java.util.concurrent.CountDownLatch;
10+
import java.util.concurrent.TimeUnit;
11+
import java.util.concurrent.atomic.AtomicReference;
12+
13+
import static org.junit.Assert.*;
14+
15+
public class QueryCopyTest extends AbstractQueryTest {
16+
17+
@Test
18+
public void queryCopy_isClone() {
19+
putTestEntity("orange", 1);
20+
TestEntity banana = putTestEntity("banana", 2);
21+
putTestEntity("apple", 3);
22+
TestEntity bananaMilkShake = putTestEntity("banana milk shake", 4);
23+
putTestEntity("pineapple", 5);
24+
putTestEntity("papaya", 6);
25+
26+
// Only even nr: 2, 4, 6.
27+
QueryFilter<TestEntity> filter = entity -> entity.getSimpleInt() % 2 == 0;
28+
// Reverse insert order: 6, 4, 2.
29+
Comparator<TestEntity> comparator = Comparator.comparingLong(testEntity -> -testEntity.getId());
30+
31+
Query<TestEntity> queryOriginal = box.query(TestEntity_.simpleString.contains("").alias("fruit"))
32+
.filter(filter)
33+
.sort(comparator)
34+
.build();
35+
// Only bananas: 4, 2.
36+
queryOriginal.setParameter("fruit", banana.getSimpleString());
37+
38+
Query<TestEntity> queryCopy = queryOriginal.copy();
39+
40+
// Object instances and native query handle should differ.
41+
assertNotEquals(queryOriginal, queryCopy);
42+
assertNotEquals(queryOriginal.handle, queryCopy.handle);
43+
44+
// Verify results are identical.
45+
List<TestEntity> resultsOriginal = queryOriginal.find();
46+
queryOriginal.close();
47+
List<TestEntity> resultsCopy = queryCopy.find();
48+
queryCopy.close();
49+
assertEquals(2, resultsOriginal.size());
50+
assertEquals(2, resultsCopy.size());
51+
assertTestEntityEquals(bananaMilkShake, resultsOriginal.get(0));
52+
assertTestEntityEquals(bananaMilkShake, resultsCopy.get(0));
53+
assertTestEntityEquals(banana, resultsOriginal.get(1));
54+
assertTestEntityEquals(banana, resultsCopy.get(1));
55+
}
56+
57+
@Test
58+
public void queryCopy_setParameter_noEffectOriginal() {
59+
TestEntity orange = putTestEntity("orange", 1);
60+
TestEntity banana = putTestEntity("banana", 2);
61+
62+
Query<TestEntity> queryOriginal = box
63+
.query(TestEntity_.simpleString.equal(orange.getSimpleString()).alias("fruit"))
64+
.build();
65+
66+
// Set parameter on clone that changes result.
67+
Query<TestEntity> queryCopy = queryOriginal.copy()
68+
.setParameter("fruit", banana.getSimpleString());
69+
70+
List<TestEntity> resultsOriginal = queryOriginal.find();
71+
queryOriginal.close();
72+
assertEquals(1, resultsOriginal.size());
73+
assertTestEntityEquals(orange, resultsOriginal.get(0));
74+
75+
List<TestEntity> resultsCopy = queryCopy.find();
76+
queryCopy.close();
77+
assertEquals(1, resultsCopy.size());
78+
assertTestEntityEquals(banana, resultsCopy.get(0));
79+
}
80+
81+
private void assertTestEntityEquals(TestEntity expected, TestEntity actual) {
82+
assertEquals(expected.getId(), actual.getId());
83+
assertEquals(expected.getSimpleString(), actual.getSimpleString());
84+
}
85+
86+
@Test
87+
public void queryThreadLocal() throws InterruptedException {
88+
Query<TestEntity> queryOriginal = box.query().build();
89+
QueryThreadLocal<TestEntity> threadLocal = new QueryThreadLocal<>(queryOriginal);
90+
91+
AtomicReference<Query<TestEntity>> queryThreadAtomic = new AtomicReference<>();
92+
CountDownLatch latch = new CountDownLatch(1);
93+
new Thread(() -> {
94+
queryThreadAtomic.set(threadLocal.get());
95+
latch.countDown();
96+
}).start();
97+
98+
assertTrue(latch.await(1, TimeUnit.SECONDS));
99+
100+
Query<TestEntity> queryThread = queryThreadAtomic.get();
101+
Query<TestEntity> queryMain = threadLocal.get();
102+
103+
// Assert that initialValue returns something.
104+
assertNotNull(queryThread);
105+
assertNotNull(queryMain);
106+
107+
// Assert that initialValue returns clones.
108+
assertNotEquals(queryThread.handle, queryOriginal.handle);
109+
assertNotEquals(queryMain.handle, queryOriginal.handle);
110+
assertNotEquals(queryThread.handle, queryMain.handle);
111+
112+
queryOriginal.close();
113+
queryMain.close();
114+
queryThread.close();
115+
}
116+
}

0 commit comments

Comments
 (0)