Skip to content

Commit ce50492

Browse files
committed
types: ResultSet API for TarantoolClient
This commit introduces a new API to handle TarantoolClient result. The concept is similar to the JDBC ResultSet in terms of a getting the data using rows ans columns. Instead of a guessing-style processing the result via List<?>, TarantoolResultSet offers set of typed methods to retrieve the data or get an error if the result cannot be represented as the designated type. Latter case requires to declare formal rules of a casting between the types. In scope of this commit it is supported 11 standard types and conversions between each other. These types are byte, short, int, long, float, double, boolean, BigInteger, BigDecimal, String, and byte[]. Closes: #211
1 parent 45e4b39 commit ce50492

14 files changed

+3137
-0
lines changed

README.md

+75
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ To get the Java connector for Tarantool 1.6.9, visit
1818
* [Spring NamedParameterJdbcTemplate usage example](#spring-namedparameterjdbctemplate-usage-example)
1919
* [JDBC](#JDBC)
2020
* [Cluster support](#cluster-support)
21+
* [Getting a result](#getting-a-result)
2122
* [Logging](#logging)
2223
* [Building](#building)
2324
* [Where to get help](#where-to-get-help)
@@ -418,6 +419,80 @@ against its integer IDs.
418419
3. The client guarantees an order of synchronous requests per thread. Other cases such
419420
as asynchronous or multi-threaded requests may be out of order before the execution.
420421

422+
## Getting a result
423+
424+
Traditionally, when a response is parsed by the internal MsgPack implementation the client
425+
will return it as a heterogeneous list of objects `List` that in most cases is inconvenient
426+
for users to use. It requires a type guessing as well as a writing more boilerplate code to work
427+
with typed data. Most of the methods which are provided by `TarantoolClientOps` (i.e. `select`)
428+
return raw de-serialized data via `List`.
429+
430+
Consider a small example how it is usually used:
431+
432+
```java
433+
// get an untyped array of tuples
434+
List<?> result = client.syncOps().execute(Requests.selectRequest("space", "pk"));
435+
for (int i = 0; i < result.size(); i++) {
436+
// get the first tuple (also untyped)
437+
List<?> row = result.get(i);
438+
// try to cast the first tuple as a couple of values
439+
int id = (int) row.get(0);
440+
String text = (String) row.get(1);
441+
processEntry(id, text);
442+
}
443+
```
444+
445+
There is an additional way to work with data using `TarantoolClient.executeRequest(TarantoolRequestConvertible)`
446+
method. This method returns a result wrapper over original data that allows to extract in a more
447+
typed manner rather than it is directly provided by MsgPack serialization. The `executeRequest`
448+
returns the `TarantoolResultSet` which provides a bunch of methods to get data. Inside the result
449+
set the data is represented as a list of rows (tuples) where each row has columns (fields).
450+
In general, it is possible that different rows have different size of their columns in scope of
451+
the same result.
452+
453+
```java
454+
TarantoolResultSet result = client.executeRequest(Requests.selectRequest("space", "pk"));
455+
while (result.next()) {
456+
long id = result.getLong(0);
457+
String text = result.getString(1);
458+
processEntry(id, text);
459+
}
460+
```
461+
462+
The `TarantoolResultSet` provides an implicit conversation between types if it's possible.
463+
464+
Numeric types internally can represent each other if a type range allows to do it. For example,
465+
byte 100 can be represented as a short, int and other types wider than byte. But 200 integer
466+
cannot be narrowed to a byte because of overflow (byte range is [-128..127]). If a floating
467+
point number is converted to a integer then the fraction part will be omitted. It is also
468+
possible to convert a valid string to a number.
469+
470+
Boolean type can be obtained from numeric types such as byte, short, int, long, BigInteger,
471+
float and double where 1 (1.0) means true and 0 (0.0) means false. Or it can be got from
472+
a string using well-known patterns such as "1", "t|true", "y|yes", "on" for true and
473+
"0", "f|false", "n|no", "off" for false respectively.
474+
475+
String type can be converted from a byte array and any numeric types. In case of `byte[]`
476+
all bytes will be interpreted as a UTF-8 sequence.
477+
478+
There is a special method called `getObject(int, Map)` where a user can provide its own
479+
mapping functions to be applied if a designated type matches a value one.
480+
481+
For instance, using the following map each strings will be transformed to an upper case and
482+
boolean values will be represented as strings "yes" or "no":
483+
484+
```java
485+
Map<Class<?>, Function<Object, Object>> mappers = new HashMap<>();
486+
mappers.put(
487+
String.class,
488+
v -> ((String) v).toUpperCase()
489+
);
490+
mappers.put(
491+
Boolean.class,
492+
v -> (boolean) v ? "yes" : "no"
493+
);
494+
```
495+
421496
## Spring NamedParameterJdbcTemplate usage example
422497

423498
The JDBC driver uses `TarantoolClient` implementation to provide a communication with server.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package org.tarantool;
2+
3+
import org.tarantool.conversion.ConverterRegistry;
4+
import org.tarantool.conversion.NotConvertibleValueException;
5+
6+
import java.math.BigInteger;
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
import java.util.Map;
10+
11+
/**
12+
* Simple implementation of {@link TarantoolResultSet}
13+
* that contains all tuples in local memory.
14+
*/
15+
class InMemoryResultSet implements TarantoolResultSet {
16+
17+
private final ConverterRegistry converterRegistry;
18+
private final List<Object> results;
19+
20+
private int currentIndex;
21+
private List<Object> currentTuple;
22+
23+
InMemoryResultSet(List<Object> rawResult, boolean asSingleResult, ConverterRegistry converterRegistry) {
24+
currentIndex = -1;
25+
this.converterRegistry = converterRegistry;
26+
27+
results = new ArrayList<>();
28+
ArrayList<Object> copiedResult = new ArrayList<>(rawResult);
29+
if (asSingleResult) {
30+
results.add(copiedResult);
31+
} else {
32+
results.addAll(copiedResult);
33+
}
34+
}
35+
36+
@Override
37+
public boolean next() {
38+
if ((currentIndex + 1) < results.size()) {
39+
currentTuple = getAsTuple(++currentIndex);
40+
return true;
41+
}
42+
return false;
43+
}
44+
45+
@Override
46+
public boolean previous() {
47+
if ((currentIndex - 1) >= 0) {
48+
currentTuple = getAsTuple(--currentIndex);
49+
return true;
50+
}
51+
return false;
52+
}
53+
54+
@Override
55+
public byte getByte(int columnIndex) {
56+
return getTypedValue(columnIndex, Byte.class, (byte) 0);
57+
}
58+
59+
@Override
60+
public short getShort(int columnIndex) {
61+
return getTypedValue(columnIndex, Short.class, (short) 0);
62+
}
63+
64+
@Override
65+
public int getInt(int columnIndex) {
66+
return getTypedValue(columnIndex, Integer.class, 0);
67+
}
68+
69+
@Override
70+
public long getLong(int columnIndex) {
71+
return getTypedValue(columnIndex, Long.class, 0L);
72+
}
73+
74+
@Override
75+
public float getFloat(int columnIndex) {
76+
return getTypedValue(columnIndex, Float.class, 0.0f);
77+
}
78+
79+
@Override
80+
public double getDouble(int columnIndex) {
81+
return getTypedValue(columnIndex, Double.class, 0.0d);
82+
}
83+
84+
@Override
85+
public boolean getBoolean(int columnIndex) {
86+
return getTypedValue(columnIndex, Boolean.class, false);
87+
}
88+
89+
@Override
90+
public byte[] getBytes(int columnIndex) {
91+
return getTypedValue(columnIndex, byte[].class, null);
92+
}
93+
94+
@Override
95+
public String getString(int columnIndex) {
96+
return getTypedValue(columnIndex, String.class, null);
97+
}
98+
99+
@Override
100+
public Object getObject(int columnIndex) {
101+
return requireInRange(columnIndex);
102+
}
103+
104+
@Override
105+
public BigInteger getBigInteger(int columnIndex) {
106+
return getTypedValue(columnIndex, BigInteger.class, null);
107+
}
108+
109+
@Override
110+
@SuppressWarnings("unchecked")
111+
public List<Object> getList(int columnIndex) {
112+
Object value = requireInRange(columnIndex);
113+
if (value == null) {
114+
return null;
115+
}
116+
if (value instanceof List<?>) {
117+
return (List<Object>) value;
118+
}
119+
throw new NotConvertibleValueException(value.getClass(), List.class);
120+
}
121+
122+
@Override
123+
@SuppressWarnings("unchecked")
124+
public Map<Object, Object> getMap(int columnIndex) {
125+
Object value = requireInRange(columnIndex);;
126+
if (value == null) {
127+
return null;
128+
}
129+
if (value instanceof Map<?, ?>) {
130+
return (Map<Object, Object>) value;
131+
}
132+
throw new NotConvertibleValueException(value.getClass(), Map.class);
133+
}
134+
135+
@Override
136+
public boolean isNull(int columnIndex) {
137+
Object value = requireInRange(columnIndex);
138+
return value == null;
139+
}
140+
141+
@Override
142+
public TarantoolTuple getTuple(int size) {
143+
requireInRow();
144+
int capacity = size == 0 ? currentTuple.size() : size;
145+
return new TarantoolTuple(currentTuple, capacity);
146+
}
147+
148+
@Override
149+
public int getRowSize() {
150+
return (currentTuple != null) ? currentTuple.size() : -1;
151+
}
152+
153+
@Override
154+
public boolean isEmpty() {
155+
return results.isEmpty();
156+
}
157+
158+
@Override
159+
public void close() {
160+
results.clear();
161+
currentTuple = null;
162+
currentIndex = -1;
163+
}
164+
165+
@SuppressWarnings("unchecked")
166+
private <R> R getTypedValue(int columnIndex, Class<R> type, R defaultValue) {
167+
Object value = requireInRange(columnIndex);
168+
if (value == null) {
169+
return defaultValue;
170+
}
171+
if (type.isInstance(value)) {
172+
return (R) value;
173+
}
174+
return converterRegistry.convert(value, type);
175+
}
176+
177+
@SuppressWarnings("unchecked")
178+
private List<Object> getAsTuple(int index) {
179+
Object row = results.get(index);
180+
return (List<Object>) row;
181+
}
182+
183+
private Object requireInRange(int index) {
184+
requireInRow();
185+
if (index < 1 || index > currentTuple.size()) {
186+
throw new IndexOutOfBoundsException("Index out of range: " + index);
187+
}
188+
return currentTuple.get(index - 1);
189+
}
190+
191+
private void requireInRow() {
192+
if (currentIndex == -1) {
193+
throw new IllegalArgumentException("Result set out of row position. Try call next() before.");
194+
}
195+
}
196+
197+
}

src/main/java/org/tarantool/TarantoolClient.java

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.tarantool;
22

3+
import org.tarantool.dsl.TarantoolRequestSpec;
34
import org.tarantool.schema.TarantoolSchemaMeta;
45

56
import java.util.List;
@@ -33,4 +34,5 @@ public interface TarantoolClient {
3334

3435
TarantoolSchemaMeta getSchemaMeta();
3536

37+
TarantoolResultSet executeRequest(TarantoolRequestSpec requestSpec);
3638
}

src/main/java/org/tarantool/TarantoolClientImpl.java

+16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package org.tarantool;
22

3+
import org.tarantool.conversion.ConverterRegistry;
4+
import org.tarantool.conversion.DefaultConverterRegistry;
5+
import org.tarantool.dsl.TarantoolRequestSpec;
36
import org.tarantool.logging.Logger;
47
import org.tarantool.logging.LoggerFactory;
58
import org.tarantool.protocol.ProtoConstants;
@@ -92,6 +95,7 @@ public class TarantoolClientImpl extends TarantoolBase<Future<?>> implements Tar
9295
protected Thread writer;
9396

9497
protected TarantoolSchemaMeta schemaMeta = new TarantoolMetaSpacesCache(this);
98+
protected ConverterRegistry converterRegistry = new DefaultConverterRegistry();
9599

96100
protected Thread connector = new Thread(new Runnable() {
97101
@Override
@@ -279,6 +283,18 @@ public TarantoolSchemaMeta getSchemaMeta() {
279283
return schemaMeta;
280284
}
281285

286+
@Override
287+
@SuppressWarnings("unchecked")
288+
public TarantoolResultSet executeRequest(TarantoolRequestSpec requestSpec) {
289+
TarantoolRequest request = requestSpec.toTarantoolRequest(getSchemaMeta());
290+
List<Object> result = (List<Object>) syncGet(exec(request));
291+
return new InMemoryResultSet(result, isSingleResultRow(request.getCode()), converterRegistry);
292+
}
293+
294+
private boolean isSingleResultRow(Code code) {
295+
return code == Code.EVAL || code == Code.CALL || code == Code.OLD_CALL;
296+
}
297+
282298
/**
283299
* Executes an operation with default timeout.
284300
*

0 commit comments

Comments
 (0)