#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:
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user