Skip to content

Commit

Permalink
Merge pull request #2509 from hgschmie/function-arguments
Browse files Browse the repository at this point in the history
Add support for function arguments
  • Loading branch information
hgschmie committed Oct 13, 2023
2 parents 8c5ec4a + 1883721 commit 270d158
Show file tree
Hide file tree
Showing 9 changed files with 492 additions and 105 deletions.
1 change: 1 addition & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Unreleased

- Move from Kotlin 1.5 to 1.6 as 1.5 is deprecated and will be removed.
- Add support for Function arguments, similar to Consumer arguments, to SQL objects (#2326)

# 3.41.3
- Fix regression introduced by #2481 where `-` at the end of named parameters get swallowed. (#2499, thanks @gokristian for reporting).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import org.jdbi.v3.core.mapper.ColumnMapperFactory;

@Reversed
class ReversedStringMapperFactory implements ColumnMapperFactory {
public final class ReversedStringMapperFactory implements ColumnMapperFactory {
@Override
public Optional<ColumnMapper<?>> build(Type type, ConfigRegistry config) {
if (String.class.equals(type)) {
Expand Down
26 changes: 20 additions & 6 deletions docs/src/adoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4174,11 +4174,13 @@ void insert(long id, String name);
----


==== Consumer arguments
==== Consumer and Function arguments

In addition to the regular method arguments for a SQL Object method, it is possible to use a single link:{jdkdocs}/java/util/function/Consumer.html[Consumer<T>^] argument in addition to other arguments.
===== Consumer arguments

Consumer arguments are actually a special return type for SQL operations. They can be used to consume the results from a SQL operation with a callback instead of returning a value from the method.
In addition to the regular method arguments for a SQL Object method, it is possible to use a single link:{jdkdocs}/java/util/function/Consumer.html[Consumer<T>^] or link:{jdkdocs}/java/util/function/Function.html[Function<T>^] argument in addition to other arguments.

A consumer argument is a special return type for SQL operations. They can be used to consume the results from a SQL operation with a callback instead of returning a value from the method.

Any SQL operation method that wants to use a consumer argument *must* return `void`. It can declare only a single consumer argument, but can have additional regular method arguments. The consumer argument can be in any position in the argument list. .

Expand Down Expand Up @@ -4206,6 +4208,18 @@ include::{exampledir}/BindMethodsTest.java[tags=consume-iterable-user-method]
[WARNING]
When using link:{jdkdocs}/java/util/function/Consumer.html[Consumer<Iterable>^] as a consumer argument, the link:{jdkdocs}/java/lang/Iterable.html[Iterable^] object passed into the consumer is *NOT* a general-purpose Iterable as it supports only a single invocation of the link:{jdkdocs}/java/lang/Iterable.html#iterator--[Iterable#iterator()^] method. Invoking it the iterator method again to obtain a second or subsequent iterator may throw link:{jdkdocs}/java/lang/IllegalStateException.html[IllegalStateException^].


===== Function arguments

A function argument can be used to collect or transform the result from a SQL operation. Similar to a consumer argument, it will receive the results of the query. A function argument only supports link:{jdkdocs}/java/util/stream/Stream.html[Stream^], link:{jdkdocs}/java/util/Iterator.html[Iterator^] or link:{jdkdocs}/java/lang/Iterable.html[Iterable^] as the function input type. The result type of the function must match the return type of the method itself.

[source,java,indent=0]
----
include::{exampledir}/BindMethodsTest.java[tags=function-user-method]
----

<1> The results of the SQL operation are passed as a stream into the Function. It returns a set which is then returned by the method.

[#sql-call-consumer-arguments]
===== `@SqlCall` Consumer and Function arguments

Expand Down Expand Up @@ -4400,11 +4414,11 @@ include::{exampledir}/LiveObjectsTest.java[tag=on-demand-default]
include::{exampledir}/LiveObjectsTest.java[tag=extension-default]
----

===== Use consumer arguments
===== Use consumer or function arguments

Using the method return value either directly or through an _interface default method_ has the drawback that the user code to process the cursor-type object must be written within the SQL Object type as it must be executed before closing the link:{jdbidocs}/core/Handle.html[Handle^] object.

An elegant way to sidestep this problem is using a <<Consumer arguments, consumer argument>> to provide a callback:
An elegant way to sidestep this problem is using a <<Consumer arguments, consumer argument>> or a <<Function, function argument>> to provide a callback:

[source,java,indent=0]
----
Expand Down Expand Up @@ -4434,7 +4448,7 @@ include::{exampledir}/LiveObjectsTest.java[tag=extension-callback]
<1> This code uses link:{jdbidocs}/core/Jdbi.html#useExtension-java.lang.Class-org.jdbi.v3.core.extension.ExtensionConsumer-[Jdbi#useExtension()^] instead of link:{jdbidocs}/core/Jdbi.html#withExtension-java.lang.Class-org.jdbi.v3.core.extension.ExtensionCallback-[Jdbi#withExtension()^] as the SQL Object method returns `void`.

[NOTE]
While using a consumer argument is a great way to deal with cursor-type objects, there is the drawback that the user code is called while holding the handle (or the database connection) open. If the callback does very expensive or slow processing, this may hold the connection for a very long time.
While using a consumer or a function argument is a great way to deal with cursor-type objects, there is the drawback that the user code is called while holding the handle (or the database connection) open. If the callback does very expensive or slow processing, this may hold the connection for a very long time.


[#sqlobject-mapper-annotations]
Expand Down
9 changes: 9 additions & 0 deletions docs/src/test/java/jdbi/doc/BindMethodsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;

import org.jdbi.v3.core.Handle;
import org.jdbi.v3.core.mapper.RowMapper;
Expand Down Expand Up @@ -225,6 +228,12 @@ public interface UserDao {
@RegisterConstructorMapper(User.class)
void consumeMultiUserIterable(Consumer<Iterable<User>> consumer); // <1>
// end::consume-iterable-user-method[]

// tag::function-user-method[]
@SqlQuery("SELECT * FROM users")
@RegisterConstructorMapper(User.class)
Set<User> mapUsers(Function<Stream<User>, Set<User>> function); // <1>
// end::function-user-method[]
}


Expand Down
40 changes: 35 additions & 5 deletions docs/src/test/java/jdbi/doc/LiveObjectsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -58,6 +60,10 @@ default List<String> getNames() { // <1>
interface CallbackDao {
@SqlQuery("SELECT name FROM users")
void getNamesAsStream(Consumer<Stream<String>> consumer);

@SqlQuery("SELECT name FROM users")
Set<String> getNamesAsSet(Function<Stream<String>, Set<String>> function);

}
// end::callback-dao[]

Expand Down Expand Up @@ -141,32 +147,56 @@ void testWithExtensionDefaultMethod() {

// tag::attach-callback[]
@Test
void testHandleAttachCallback() {
void testHandleAttachConsumer() {
try (Handle handle = jdbi.open()) { // <1>
CallbackDao dao = handle.attach(CallbackDao.class);
List<String> result = new ArrayList<>();
dao.getNamesAsStream(stream -> stream.forEach(result::add)); // <2>
assertThat(result).containsAll(names);
}
}

@Test
void testHandleAttachFunction() {
try (Handle handle = jdbi.open()) { // <1>
CallbackDao dao = handle.attach(CallbackDao.class);
Set<String> result = dao.getNamesAsSet(stream -> stream.collect(Collectors.toSet())); // <2>
assertThat(result).containsAll(names);
}
}
// end::attach-callback[]

// tag::on-demand-callback[]
@Test
void testOnDemandCallback() {
void testOnDemandConsumer() {
CallbackDao dao = jdbi.onDemand(CallbackDao.class);
List<String> result = new ArrayList<>();
dao.getNamesAsStream(stream -> stream.forEach(result::add));
assertThat(result).containsAll(names);
}

@Test
void testOnDemandFunction() {
CallbackDao dao = jdbi.onDemand(CallbackDao.class);
Set<String> result = dao.getNamesAsSet(stream -> stream.collect(Collectors.toSet()));
assertThat(result).containsAll(names);
}
// end::on-demand-callback[]

// tag::extension-callback[]
@Test
void testWithExtensionCallback() {
void testWithExtensionConsumer() {
List<String> result = new ArrayList<>();
jdbi.useExtension(CallbackDao.class, dao -> // <1>
dao.getNamesAsStream(stream -> stream.forEach(result::add)));
jdbi.useExtension(CallbackDao.class,
dao -> dao.getNamesAsStream(stream -> stream.forEach(result::add))); // <1>
assertThat(result).containsAll(names);
}

@Test
void testWithExtensionFunction() {
Set<String> result = jdbi.withExtension(CallbackDao.class,
dao -> dao.getNamesAsSet(stream -> stream.collect(Collectors.toSet())));

assertThat(result).containsAll(names);
}
// end::extension-callback[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,13 @@ public void apply(SqlStatement<?> stmt, Object[] args) throws SQLException {
}
return Stream.empty();
}
if (parameter.getType() == Function.class && this instanceof SqlCallHandler) {
if (parameter.getType() == Function.class
// SqlCallHandler supports Function argument for OutParameters
&& (this instanceof SqlCallHandler
// SqlBatchHandler, SqlQueryHandler and SqlUpdateHandler support Function arguments
|| this instanceof SqlBatchHandler
|| this instanceof SqlQueryHandler
|| this instanceof SqlUpdateHandler)) {
return Stream.empty();
}

Expand Down
Loading

0 comments on commit 270d158

Please sign in to comment.