#15 - Add BindMarkers abstraction.

We now expose a bind marker API that allows to model a vendor-specific bind marker strategy. The currently supported database systems (Postgres, MsSql) implement indexed respective named strategies that differ in how placeholders and identifiers are generated.

Original pull request: #19.
This commit is contained in:
Mark Paluch
2018-11-19 15:08:08 +01:00
parent f85a9a084f
commit 9700c3c087
8 changed files with 512 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
package org.springframework.data.r2dbc.dialect;
import io.r2dbc.spi.Statement;
/**
* A bind marker represents a single bindable parameter within a query. Bind markers are dialect-specific and provide a
* {@link #getPlaceholder() placeholder} that is used in the actual query.
*
* @author Mark Paluch
* @see Statement#bind
* @see BindMarkers
* @see BindMarkersFactory
*/
public interface BindMarker {
/**
* Returns the database-specific placeholder for a given substitution.
*
* @return the database-specific placeholder for a given substitution.
*/
String getPlaceholder();
/**
* Bind the given {@code value} to the {@link Statement} using the underlying binding strategy.
*
* @param statement the statement to bind the value to.
* @param value the actual value. Must not be {@literal null}. Use {@link #bindNull(Statement, Class)} for
* {@literal null} values.
* @see Statement#bind
*/
void bindValue(Statement<?> statement, Object value);
/**
* Bind a {@literal null} value to the {@link Statement} using the underlying binding strategy.
*
* @param statement the statement to bind the value to.
* @param valueType value type, must not be {@literal null}.
* @see Statement#bindNull
*/
void bindNull(Statement<?> statement, Class<?> valueType);
}

View File

@@ -0,0 +1,36 @@
package org.springframework.data.r2dbc.dialect;
/**
* Bind markers represent placeholders in SQL queries for substitution for an actual parameter. Using bind markers
* allows creating safe queries so query strings are not required to contain escaped values but rather the driver
* encodes parameter in the appropriate representation.
* <p/>
* {@link BindMarkers} is stateful and can be only used for a single binding pass of one or more parameters. It
* maintains bind indexes/bind parameter names.
*
* @author Mark Paluch
* @see BindMarker
* @see BindMarkersFactory
* @see io.r2dbc.spi.Statement#bind
*/
@FunctionalInterface
public interface BindMarkers {
/**
* Creates a new {@link BindMarker}.
*
* @return a new {@link BindMarker}.
*/
BindMarker next();
/**
* Creates a new {@link BindMarker} that accepts a {@code nameHint}. Implementations are allowed to consider/ignore
* the name hint to create more expressive bind markers.
*
* @param nameHint an optional name hint.
* @return a new {@link BindMarker}.
*/
default BindMarker next(String nameHint) {
return next();
}
}

View File

@@ -0,0 +1,53 @@
package org.springframework.data.r2dbc.dialect;
import org.springframework.util.Assert;
/**
* This class creates new {@link BindMarkers} instances to bind parameter for a specific {@link io.r2dbc.spi.Statement}.
*
* @author Mark Paluch
* @see BindMarkers
* @see io.r2dbc.spi.Statement
*/
@FunctionalInterface
public interface BindMarkersFactory {
/**
* Create a new {@link BindMarkers} instance.
*
* @return a new {@link BindMarkers} instance.
*/
BindMarkers create();
/**
* Create index-based {@link BindMarkers}.
*
* @param prefix bind parameter prefix.
* @param beginWith the first index to use.
* @return a {@link BindMarkersFactory} using {@code prefix} and {@code beginWith}.
*/
static BindMarkersFactory indexed(String prefix, int beginWith) {
Assert.notNull(prefix, "Prefix must not be null!");
return () -> new IndexedBindMarkers(prefix, beginWith);
}
/**
* Create named {@link BindMarkers}. Named bind markers can support {@link BindMarkers#next(String) name hints}.
* Typically, named markers use name hints. If no namehint is given, named bind markers use a counter to generate
* unique bind markers.
*
* @param prefix bind parameter prefix.
* @param indexPrefix prefix for bind markers that were created by incrementing a counter to generate a unique bind
* marker.
* @param nameLimit maximal length of parameter names when using name hints.
* @return a {@link BindMarkersFactory} using {@code prefix} and {@code beginWith}.
*/
static BindMarkersFactory named(String prefix, String indexPrefix, int nameLimit) {
Assert.notNull(prefix, "Prefix must not be null!");
Assert.notNull(indexPrefix, "Index prefix must not be null!");
return () -> new NamedBindMarkers(prefix, indexPrefix, nameLimit);
}
}

View File

@@ -0,0 +1,87 @@
package org.springframework.data.r2dbc.dialect;
import io.r2dbc.spi.Statement;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
/**
* Index-based bind marker. This implementation creates indexed bind markers using a numeric index and an optional
* prefix for bind markers to be represented within the query string.
*
* @author Mark Paluch
*/
class IndexedBindMarkers implements BindMarkers {
private static final AtomicIntegerFieldUpdater<org.springframework.data.r2dbc.dialect.IndexedBindMarkers> COUNTER_INCREMENTER = AtomicIntegerFieldUpdater
.newUpdater(org.springframework.data.r2dbc.dialect.IndexedBindMarkers.class, "counter");
// access via COUNTER_INCREMENTER
@SuppressWarnings("unused") private volatile int counter;
private final String prefix;
/**
* Creates a new {@link IndexedBindMarker} instance given {@code prefix} and {@code beginWith}.
*
* @param prefix bind parameter prefix.
* @param beginWith the first index to use.
*/
IndexedBindMarkers(String prefix, int beginWith) {
this.counter = beginWith;
this.prefix = prefix;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarkers#next()
*/
@Override
public BindMarker next() {
int index = COUNTER_INCREMENTER.getAndIncrement(this);
return new IndexedBindMarker(prefix + "" + index, index);
}
/**
* A single indexed bind marker.
*/
static class IndexedBindMarker implements BindMarker {
private final String placeholder;
private int index;
IndexedBindMarker(String placeholder, int index) {
this.placeholder = placeholder;
this.index = index;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarker#getPlaceholder()
*/
@Override
public String getPlaceholder() {
return placeholder;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindValue(io.r2dbc.spi.Statement, java.lang.Object)
*/
@Override
public void bindValue(Statement<?> statement, Object value) {
statement.bind(this.index, value);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindNull(io.r2dbc.spi.Statement, java.lang.Class)
*/
@Override
public void bindNull(Statement<?> statement, Class<?> valueType) {
statement.bindNull(this.index, valueType);
}
}
}

View File

@@ -0,0 +1,129 @@
package org.springframework.data.r2dbc.dialect;
import io.r2dbc.spi.Statement;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import org.springframework.util.Assert;
/**
* Name-based bind markers.
*
* @author Mark Paluch
*/
class NamedBindMarkers implements BindMarkers {
private static final AtomicIntegerFieldUpdater<NamedBindMarkers> COUNTER_INCREMENTER = AtomicIntegerFieldUpdater
.newUpdater(NamedBindMarkers.class, "counter");
// access via COUNTER_INCREMENTER
@SuppressWarnings("unused") private volatile int counter;
private final String prefix;
private final String indexPrefix;
private final int nameLimit;
NamedBindMarkers(String prefix, String indexPrefix, int nameLimit) {
this.prefix = prefix;
this.indexPrefix = indexPrefix;
this.nameLimit = nameLimit;
}
@Override
public BindMarker next() {
String name = nextName();
return new NamedBindMarker(prefix + name, name);
}
@Override
public BindMarker next(String nameHint) {
Assert.notNull(nameHint, "Name hint must not be null");
String name = nextName();
String filteredNameHint = filter(nameHint);
if (!filteredNameHint.isEmpty()) {
name += "_" + filteredNameHint;
}
if (name.length() > nameLimit) {
name = name.substring(0, nameLimit);
}
return new NamedBindMarker(prefix + name, name);
}
private String nextName() {
int index = COUNTER_INCREMENTER.getAndIncrement(this);
return indexPrefix + index;
}
private static String filter(CharSequence input) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
char ch = input.charAt(i);
// ascii letter or digit
if (Character.isLetterOrDigit(ch) && ch < 127) {
builder.append(ch);
}
}
return builder.toString();
}
/**
* A single named bind marker.
*/
static class NamedBindMarker implements BindMarker {
private final String placeholder;
private final String identifier;
NamedBindMarker(String placeholder, String identifier) {
this.placeholder = placeholder;
this.identifier = identifier;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarker#getPlaceholder()
*/
@Override
public String getPlaceholder() {
return this.placeholder;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindValue(io.r2dbc.spi.Statement, java.lang.Object)
*/
@Override
public void bindValue(Statement<?> statement, Object value) {
statement.bind(this.identifier, value);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindNull(io.r2dbc.spi.Statement, java.lang.Class)
*/
@Override
public void bindNull(Statement<?> statement, Class<?> valueType) {
statement.bindNull(this.identifier, valueType);
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* Dialects abstract the SQL dialect of the underlying database.
*/
@NonNullApi
package org.springframework.data.r2dbc.dialect;
import org.springframework.lang.NonNullApi;