-
Notifications
You must be signed in to change notification settings - Fork 19
ResultSet API for TarantoolClient #223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
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
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ To get the Java connector for Tarantool 1.6.9, visit | |
| * [Spring NamedParameterJdbcTemplate usage example](#spring-namedparameterjdbctemplate-usage-example) | ||
| * [JDBC](#JDBC) | ||
| * [Cluster support](#cluster-support) | ||
| * [Getting a result](#getting-a-result) | ||
| * [Logging](#logging) | ||
| * [Building](#building) | ||
| * [Where to get help](#where-to-get-help) | ||
|
|
@@ -418,6 +419,80 @@ against its integer IDs. | |
| 3. The client guarantees an order of synchronous requests per thread. Other cases such | ||
| as asynchronous or multi-threaded requests may be out of order before the execution. | ||
|
|
||
| ## Getting a result | ||
|
|
||
| Traditionally, when a response is parsed by the internal MsgPack implementation the client | ||
| will return it as a heterogeneous list of objects `List` that in most cases is inconvenient | ||
| for users to use. It requires a type guessing as well as a writing more boilerplate code to work | ||
| with typed data. Most of the methods which are provided by `TarantoolClientOps` (i.e. `select`) | ||
| return raw de-serialized data via `List`. | ||
|
|
||
| Consider a small example how it is usually used: | ||
|
|
||
| ```java | ||
| // get an untyped array of tuples | ||
| List<?> result = client.syncOps().execute(Requests.selectRequest("space", "pk")); | ||
| for (int i = 0; i < result.size(); i++) { | ||
| // get the first tuple (also untyped) | ||
| List<?> row = result.get(i); | ||
| // try to cast the first tuple as a couple of values | ||
| int id = (int) row.get(0); | ||
| String text = (String) row.get(1); | ||
| processEntry(id, text); | ||
| } | ||
| ``` | ||
|
|
||
| There is an additional way to work with data using `TarantoolClient.executeRequest(TarantoolRequestConvertible)` | ||
| method. This method returns a result wrapper over original data that allows to extract in a more | ||
| typed manner rather than it is directly provided by MsgPack serialization. The `executeRequest` | ||
| returns the `TarantoolResultSet` which provides a bunch of methods to get data. Inside the result | ||
| set the data is represented as a list of rows (tuples) where each row has columns (fields). | ||
| In general, it is possible that different rows have different size of their columns in scope of | ||
| the same result. | ||
|
|
||
| ```java | ||
| TarantoolResultSet result = client.executeRequest(Requests.selectRequest("space", "pk")); | ||
| while (result.next()) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Modern (1.8+) Java code is rarely dealing with bare iterators -- streams, for-each loops and their functional mirrors are the common practice. |
||
| long id = result.getLong(0); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have unsafe casts here? Or the user will get some kind of an unchecked exception? |
||
| String text = result.getString(1); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NPE here (or further) is inevitable. The underlying object is essentially a MsgPack array with a non-fixed length. So we should either return an Optional here or provide some kind of interface with checked and unchecked calls, like checked |
||
| processEntry(id, text); | ||
| } | ||
| ``` | ||
|
|
||
| The `TarantoolResultSet` provides an implicit conversation between types if it's possible. | ||
|
|
||
| Numeric types internally can represent each other if a type range allows to do it. For example, | ||
| byte 100 can be represented as a short, int and other types wider than byte. But 200 integer | ||
| cannot be narrowed to a byte because of overflow (byte range is [-128..127]). If a floating | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will the conversion be performed actually when calling But in this case we will have double conversion with two sources of possible errors: MessagePack entity-to-object and then object-to-value conversions -- not very safe and not very fast. Also the conversion and overflow/underflow shouldn't be implicit without user interference and checked exceptions. |
||
| point number is converted to a integer then the fraction part will be omitted. It is also | ||
| possible to convert a valid string to a number. | ||
|
|
||
| Boolean type can be obtained from numeric types such as byte, short, int, long, BigInteger, | ||
| float and double where 1 (1.0) means true and 0 (0.0) means false. Or it can be got from | ||
| a string using well-known patterns such as "1", "t|true", "y|yes", "on" for true and | ||
| "0", "f|false", "n|no", "off" for false respectively. | ||
|
|
||
| String type can be converted from a byte array and any numeric types. In case of `byte[]` | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| all bytes will be interpreted as a UTF-8 sequence. | ||
|
|
||
| There is a special method called `getObject(int, Map)` where a user can provide its own | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will these mappings take raw Object instances for conversion? |
||
| mapping functions to be applied if a designated type matches a value one. | ||
|
|
||
| For instance, using the following map each strings will be transformed to an upper case and | ||
| boolean values will be represented as strings "yes" or "no": | ||
|
|
||
| ```java | ||
| Map<Class<?>, Function<Object, Object>> mappers = new HashMap<>(); | ||
| mappers.put( | ||
| String.class, | ||
| v -> ((String) v).toUpperCase() | ||
| ); | ||
| mappers.put( | ||
| Boolean.class, | ||
| v -> (boolean) v ? "yes" : "no" | ||
| ); | ||
| ``` | ||
|
|
||
| ## Spring NamedParameterJdbcTemplate usage example | ||
|
|
||
| The JDBC driver uses `TarantoolClient` implementation to provide a communication with server. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| package org.tarantool; | ||
|
|
||
| import org.tarantool.conversion.ConverterRegistry; | ||
| import org.tarantool.conversion.NotConvertibleValueException; | ||
|
|
||
| import java.math.BigInteger; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Simple implementation of {@link TarantoolResultSet} | ||
| * that contains all tuples in local memory. | ||
| */ | ||
| class InMemoryResultSet implements TarantoolResultSet { | ||
|
|
||
| private final ConverterRegistry converterRegistry; | ||
| private final List<Object> results; | ||
|
|
||
| private int currentIndex; | ||
| private List<Object> currentTuple; | ||
|
|
||
| InMemoryResultSet(List<Object> rawResult, boolean asSingleResult, ConverterRegistry converterRegistry) { | ||
| currentIndex = -1; | ||
| this.converterRegistry = converterRegistry; | ||
|
|
||
| results = new ArrayList<>(); | ||
| ArrayList<Object> copiedResult = new ArrayList<>(rawResult); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this copying is needed? |
||
| if (asSingleResult) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not get the meaning of this flag. Is it for separating "array of array" server responses from "single object" ones? |
||
| results.add(copiedResult); | ||
| } else { | ||
| results.addAll(copiedResult); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public boolean next() { | ||
| if ((currentIndex + 1) < results.size()) { | ||
| currentTuple = getAsTuple(++currentIndex); | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean previous() { | ||
| if ((currentIndex - 1) >= 0) { | ||
| currentTuple = getAsTuple(--currentIndex); | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| @Override | ||
| public byte getByte(int columnIndex) { | ||
| return getTypedValue(columnIndex, Byte.class, (byte) 0); | ||
| } | ||
|
|
||
| @Override | ||
| public short getShort(int columnIndex) { | ||
| return getTypedValue(columnIndex, Short.class, (short) 0); | ||
| } | ||
|
|
||
| @Override | ||
| public int getInt(int columnIndex) { | ||
| return getTypedValue(columnIndex, Integer.class, 0); | ||
| } | ||
|
|
||
| @Override | ||
| public long getLong(int columnIndex) { | ||
| return getTypedValue(columnIndex, Long.class, 0L); | ||
| } | ||
|
|
||
| @Override | ||
| public float getFloat(int columnIndex) { | ||
| return getTypedValue(columnIndex, Float.class, 0.0f); | ||
| } | ||
|
|
||
| @Override | ||
| public double getDouble(int columnIndex) { | ||
| return getTypedValue(columnIndex, Double.class, 0.0d); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean getBoolean(int columnIndex) { | ||
| return getTypedValue(columnIndex, Boolean.class, false); | ||
| } | ||
|
|
||
| @Override | ||
| public byte[] getBytes(int columnIndex) { | ||
| return getTypedValue(columnIndex, byte[].class, null); | ||
| } | ||
|
|
||
| @Override | ||
| public String getString(int columnIndex) { | ||
| return getTypedValue(columnIndex, String.class, null); | ||
| } | ||
|
|
||
| @Override | ||
| public Object getObject(int columnIndex) { | ||
| return requireInRange(columnIndex); | ||
| } | ||
|
|
||
| @Override | ||
| public BigInteger getBigInteger(int columnIndex) { | ||
| return getTypedValue(columnIndex, BigInteger.class, null); | ||
| } | ||
|
|
||
| @Override | ||
| @SuppressWarnings("unchecked") | ||
| public List<Object> getList(int columnIndex) { | ||
| Object value = requireInRange(columnIndex); | ||
| if (value == null) { | ||
| return null; | ||
| } | ||
| if (value instanceof List<?>) { | ||
| return (List<Object>) value; | ||
| } | ||
| throw new NotConvertibleValueException(value.getClass(), List.class); | ||
| } | ||
|
|
||
| @Override | ||
| @SuppressWarnings("unchecked") | ||
| public Map<Object, Object> getMap(int columnIndex) { | ||
| Object value = requireInRange(columnIndex);; | ||
| if (value == null) { | ||
| return null; | ||
| } | ||
| if (value instanceof Map<?, ?>) { | ||
| return (Map<Object, Object>) value; | ||
| } | ||
| throw new NotConvertibleValueException(value.getClass(), Map.class); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isNull(int columnIndex) { | ||
| Object value = requireInRange(columnIndex); | ||
| return value == null; | ||
| } | ||
|
|
||
| @Override | ||
| public TarantoolTuple getTuple(int size) { | ||
| requireInRow(); | ||
| int capacity = size == 0 ? currentTuple.size() : size; | ||
| return new TarantoolTuple(currentTuple, capacity); | ||
| } | ||
|
|
||
| @Override | ||
| public int getRowSize() { | ||
| return (currentTuple != null) ? currentTuple.size() : -1; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isEmpty() { | ||
| return results.isEmpty(); | ||
| } | ||
|
|
||
| @Override | ||
| public void close() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like the "good old" destructor for C++ classes. Should the user be aware of this method and call it in an appropriate time? |
||
| results.clear(); | ||
| currentTuple = null; | ||
| currentIndex = -1; | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| private <R> R getTypedValue(int columnIndex, Class<R> type, R defaultValue) { | ||
| Object value = requireInRange(columnIndex); | ||
| if (value == null) { | ||
| return defaultValue; | ||
| } | ||
| if (type.isInstance(value)) { | ||
| return (R) value; | ||
| } | ||
| return converterRegistry.convert(value, type); | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| private List<Object> getAsTuple(int index) { | ||
| Object row = results.get(index); | ||
| return (List<Object>) row; | ||
| } | ||
|
|
||
| private Object requireInRange(int index) { | ||
| requireInRow(); | ||
| if (index < 1 || index > currentTuple.size()) { | ||
| throw new IndexOutOfBoundsException("Index out of range: " + index); | ||
| } | ||
| return currentTuple.get(index - 1); | ||
| } | ||
|
|
||
| private void requireInRow() { | ||
| if (currentIndex == -1) { | ||
| throw new IllegalArgumentException("Result set out of row position. Try call next() before."); | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| package org.tarantool; | ||
|
|
||
| import org.tarantool.conversion.ConverterRegistry; | ||
| import org.tarantool.conversion.DefaultConverterRegistry; | ||
| import org.tarantool.dsl.TarantoolRequestSpec; | ||
| import org.tarantool.logging.Logger; | ||
| import org.tarantool.logging.LoggerFactory; | ||
| import org.tarantool.protocol.ProtoConstants; | ||
|
|
@@ -92,6 +95,7 @@ public class TarantoolClientImpl extends TarantoolBase<Future<?>> implements Tar | |
| protected Thread writer; | ||
|
|
||
| protected TarantoolSchemaMeta schemaMeta = new TarantoolMetaSpacesCache(this); | ||
| protected ConverterRegistry converterRegistry = new DefaultConverterRegistry(); | ||
|
|
||
| protected Thread connector = new Thread(new Runnable() { | ||
| @Override | ||
|
|
@@ -279,6 +283,18 @@ public TarantoolSchemaMeta getSchemaMeta() { | |
| return schemaMeta; | ||
| } | ||
|
|
||
| @Override | ||
| @SuppressWarnings("unchecked") | ||
| public TarantoolResultSet executeRequest(TarantoolRequestSpec requestSpec) { | ||
| TarantoolRequest request = requestSpec.toTarantoolRequest(getSchemaMeta()); | ||
| List<Object> result = (List<Object>) syncGet(exec(request)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So every request is synchronous? Although, the internal transport is in fact synchronous, so just for curiousity. |
||
| return new InMemoryResultSet(result, isSingleResultRow(request.getCode()), converterRegistry); | ||
| } | ||
|
|
||
| private boolean isSingleResultRow(Code code) { | ||
| return code == Code.EVAL || code == Code.CALL || code == Code.OLD_CALL; | ||
| } | ||
|
|
||
| /** | ||
| * Executes an operation with default timeout. | ||
| * | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good idea, although it's better to provide the exact way of mapping MsgPack entities to resulting structures (List, Map or some custom object) in some kind of mapping layer, where the mapping may be customized. Ofc, the driver will provide some default way of mapping, say, into TarantoolResultSet objects.