From ea1745a8864a026d447a1f662fba1d65a0d96975 Mon Sep 17 00:00:00 2001 From: Vladimir Buhtoyarov Date: Mon, 25 Mar 2024 14:59:07 +0300 Subject: [PATCH] #467 apply unified style builders for all JDBC integrations --- .../jdbc/SQLProxyConfigurationBuilder.java | 1 + .../bucket4j/mariadb/Bucket4jMariaDB.java | 3 +- ...riaDBSelectForUpdateBasedProxyManager.java | 13 ++--- .../github/bucket4j/mssql/Bucket4jMSSQL.java | 50 ++++++++++++++++++ ...MSSQLSelectForUpdateBasedProxyManager.java | 39 +++++++++----- .../github/bucket4j/mysql/Bucket4jMySQL.java | 50 ++++++++++++++++++ ...MySQLSelectForUpdateBasedProxyManager.java | 45 +++++++++------- .../bucket4j/oracle/Bucket4jOracle.java | 50 ++++++++++++++++++ ...racleSelectForUpdateBasedProxyManager.java | 51 ++++++++++++------- .../postgresql/Bucket4jPostgreSQL.java | 6 +-- ...reSQLSelectForUpdateBasedProxyManager.java | 15 +----- ...stgreSQLadvisoryLockBasedProxyManager.java | 16 +----- 12 files changed, 247 insertions(+), 92 deletions(-) create mode 100644 bucket4j-mssql/src/main/java/io/github/bucket4j/mssql/Bucket4jMSSQL.java create mode 100644 bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/Bucket4jMySQL.java create mode 100644 bucket4j-oracle/src/main/java/io/github/bucket4j/oracle/Bucket4jOracle.java diff --git a/bucket4j-core/src/main/java/io/github/bucket4j/distributed/jdbc/SQLProxyConfigurationBuilder.java b/bucket4j-core/src/main/java/io/github/bucket4j/distributed/jdbc/SQLProxyConfigurationBuilder.java index 528f3ba23..63d837579 100644 --- a/bucket4j-core/src/main/java/io/github/bucket4j/distributed/jdbc/SQLProxyConfigurationBuilder.java +++ b/bucket4j-core/src/main/java/io/github/bucket4j/distributed/jdbc/SQLProxyConfigurationBuilder.java @@ -29,6 +29,7 @@ * @author Maxim Bartkov * The class to build {@link SQLProxyConfiguration} */ +@Deprecated public final class SQLProxyConfigurationBuilder { ClientSideConfig clientSideConfig; BucketTableSettings tableSettings; diff --git a/bucket4j-mariadb/src/main/java/io/github/bucket4j/mariadb/Bucket4jMariaDB.java b/bucket4j-mariadb/src/main/java/io/github/bucket4j/mariadb/Bucket4jMariaDB.java index b653aa02f..192d662ca 100644 --- a/bucket4j-mariadb/src/main/java/io/github/bucket4j/mariadb/Bucket4jMariaDB.java +++ b/bucket4j-mariadb/src/main/java/io/github/bucket4j/mariadb/Bucket4jMariaDB.java @@ -6,7 +6,6 @@ import io.github.bucket4j.distributed.jdbc.AbstractJdbcProxyManagerBuilder; import io.github.bucket4j.distributed.jdbc.PrimaryKeyMapper; -import io.github.bucket4j.distributed.jdbc.SQLProxyConfigurationBuilder; /** * Entry point for MariaDB integration @@ -40,7 +39,7 @@ public MariaDBSelectForUpdateBasedProxyManager build() { * * @param primaryKeyMapper object responsible for setting primary key value in prepared statement. * - * @return {@link SQLProxyConfigurationBuilder} + * @return this builder instance */ public MariaDBSelectForUpdateBasedProxyManagerBuilder primaryKeyMapper(PrimaryKeyMapper primaryKeyMapper) { super.primaryKeyMapper = (PrimaryKeyMapper) Objects.requireNonNull(primaryKeyMapper); diff --git a/bucket4j-mariadb/src/main/java/io/github/bucket4j/mariadb/MariaDBSelectForUpdateBasedProxyManager.java b/bucket4j-mariadb/src/main/java/io/github/bucket4j/mariadb/MariaDBSelectForUpdateBasedProxyManager.java index 4ce141051..5c574bc5e 100644 --- a/bucket4j-mariadb/src/main/java/io/github/bucket4j/mariadb/MariaDBSelectForUpdateBasedProxyManager.java +++ b/bucket4j-mariadb/src/main/java/io/github/bucket4j/mariadb/MariaDBSelectForUpdateBasedProxyManager.java @@ -22,7 +22,6 @@ import io.github.bucket4j.BucketExceptions; import io.github.bucket4j.distributed.jdbc.PrimaryKeyMapper; import io.github.bucket4j.distributed.jdbc.SQLProxyConfiguration; -import io.github.bucket4j.distributed.jdbc.SQLProxyConfigurationBuilder; import io.github.bucket4j.distributed.proxy.generic.select_for_update.AbstractSelectForUpdateBasedProxyManager; import io.github.bucket4j.distributed.proxy.generic.select_for_update.LockAndGetResult; import io.github.bucket4j.distributed.proxy.generic.select_for_update.SelectForUpdateBasedTransaction; @@ -40,13 +39,9 @@ import java.util.Optional; /** - * @author Maxim Bartkov - * The extension of Bucket4j library addressed to support MySQL - * To start work with the MariaDB extension you must create a table, which will include the possibility to work with buckets - * In order to do this, your table should include the next columns: id as a PRIMARY KEY (BIGINT) and state (BLOB) - * To define column names, {@link SQLProxyConfiguration} include {@link io.github.bucket4j.distributed.jdbc.BucketTableSettings} which takes settings for the table to work with Bucket4j + * The extension of Bucket4j library addressed to support MariaDB * - * @see {@link SQLProxyConfigurationBuilder} to get more information how to build {@link SQLProxyConfiguration} + *

This implementation solves transaction/concurrency related problems via "SELECT FOR UPDATE" SQL syntax. * * @param type of primary key */ @@ -71,9 +66,9 @@ public MariaDBSelectForUpdateBasedProxyManager(MariaDBSelectForUpdateBasedProxyM } /** - * - * @param configuration {@link SQLProxyConfiguration} configuration. + * @deprecated use {@link Bucket4jMariaDB#selectForUpdateBasedBuilder(DataSource)} instead */ + @Deprecated public MariaDBSelectForUpdateBasedProxyManager(SQLProxyConfiguration configuration) { super(configuration.getClientSideConfig()); this.dataSource = Objects.requireNonNull(configuration.getDataSource()); diff --git a/bucket4j-mssql/src/main/java/io/github/bucket4j/mssql/Bucket4jMSSQL.java b/bucket4j-mssql/src/main/java/io/github/bucket4j/mssql/Bucket4jMSSQL.java new file mode 100644 index 000000000..93fb50950 --- /dev/null +++ b/bucket4j-mssql/src/main/java/io/github/bucket4j/mssql/Bucket4jMSSQL.java @@ -0,0 +1,50 @@ +package io.github.bucket4j.mssql; + +import java.util.Objects; + +import javax.sql.DataSource; + +import io.github.bucket4j.distributed.jdbc.AbstractJdbcProxyManagerBuilder; +import io.github.bucket4j.distributed.jdbc.PrimaryKeyMapper; + +/** + * Entry point for MSSQL integration + */ +public class Bucket4jMSSQL { + + /** + * Returns the builder for {@link MSSQLSelectForUpdateBasedProxyManager} + * + * @param dataSource + * + * @return new instance of {@link MSSQLSelectForUpdateBasedProxyManager} + */ + public static MSSQLSelectForUpdateBasedProxyManagerBuilder selectForUpdateBasedBuilder(DataSource dataSource) { + return new MSSQLSelectForUpdateBasedProxyManagerBuilder<>(dataSource, PrimaryKeyMapper.LONG); + } + + public static class MSSQLSelectForUpdateBasedProxyManagerBuilder extends AbstractJdbcProxyManagerBuilder, MSSQLSelectForUpdateBasedProxyManagerBuilder> { + + public MSSQLSelectForUpdateBasedProxyManagerBuilder(DataSource dataSource, PrimaryKeyMapper primaryKeyMapper) { + super(dataSource, primaryKeyMapper); + } + + @Override + public MSSQLSelectForUpdateBasedProxyManager build() { + return new MSSQLSelectForUpdateBasedProxyManager<>(this); + } + + /** + * Specifies the type of primary key. + * + * @param primaryKeyMapper object responsible for setting primary key value in prepared statement. + * + * @return this builder instance + */ + public MSSQLSelectForUpdateBasedProxyManagerBuilder primaryKeyMapper(PrimaryKeyMapper primaryKeyMapper) { + super.primaryKeyMapper = (PrimaryKeyMapper) Objects.requireNonNull(primaryKeyMapper); + return (MSSQLSelectForUpdateBasedProxyManagerBuilder) this; + } + } + +} diff --git a/bucket4j-mssql/src/main/java/io/github/bucket4j/mssql/MSSQLSelectForUpdateBasedProxyManager.java b/bucket4j-mssql/src/main/java/io/github/bucket4j/mssql/MSSQLSelectForUpdateBasedProxyManager.java index 1f105e288..87273b5cb 100644 --- a/bucket4j-mssql/src/main/java/io/github/bucket4j/mssql/MSSQLSelectForUpdateBasedProxyManager.java +++ b/bucket4j-mssql/src/main/java/io/github/bucket4j/mssql/MSSQLSelectForUpdateBasedProxyManager.java @@ -20,8 +20,8 @@ package io.github.bucket4j.mssql; import io.github.bucket4j.BucketExceptions; +import io.github.bucket4j.distributed.jdbc.PrimaryKeyMapper; import io.github.bucket4j.distributed.jdbc.SQLProxyConfiguration; -import io.github.bucket4j.distributed.proxy.ClientSideConfig; import io.github.bucket4j.distributed.proxy.generic.select_for_update.AbstractSelectForUpdateBasedProxyManager; import io.github.bucket4j.distributed.proxy.generic.select_for_update.LockAndGetResult; import io.github.bucket4j.distributed.proxy.generic.select_for_update.SelectForUpdateBasedTransaction; @@ -37,33 +37,48 @@ import java.util.Optional; /** - * @author Vladimir Bukhtoyarov + * The extension of Bucket4j library addressed to support "Microsoft SQL Server" + * + *

This implementation solves transaction/concurrency related problems via "SELECT WITH(ROWLOCK, UPDLOCK)" + * which can be considered as comparable equivalent of "SELECT FOR UPDATE" from SQL Standard syntax. * * @param type of primary key */ public class MSSQLSelectForUpdateBasedProxyManager extends AbstractSelectForUpdateBasedProxyManager { private final DataSource dataSource; - private final SQLProxyConfiguration configuration; + private final PrimaryKeyMapper primaryKeyMapper; private final String removeSqlQuery; private final String updateSqlQuery; private final String insertSqlQuery; private final String selectSqlQuery; + MSSQLSelectForUpdateBasedProxyManager(Bucket4jMSSQL.MSSQLSelectForUpdateBasedProxyManagerBuilder builder) { + super(builder.getClientSideConfig()); + this.dataSource = builder.getDataSource(); + this.primaryKeyMapper = builder.getPrimaryKeyMapper(); + this.removeSqlQuery = MessageFormat.format("DELETE FROM {0} WHERE {1} = ?", builder.getTableName(), builder.getIdColumnName()); + this.updateSqlQuery = MessageFormat.format("UPDATE {0} SET {1}=? WHERE {2}=?", builder.getTableName(), builder.getStateColumnName(), builder.getIdColumnName()); + this.insertSqlQuery = MessageFormat.format( + "INSERT INTO {0}({1},{2}) VALUES(?, null)", + builder.getTableName(), builder.getIdColumnName(), builder.getStateColumnName()); + this.selectSqlQuery = MessageFormat.format("SELECT {0} as state FROM {1} WITH(ROWLOCK, UPDLOCK) WHERE {2} = ?", builder.getStateColumnName(), builder.getTableName(), builder.getIdColumnName()); + } + /** - * - * @param configuration {@link SQLProxyConfiguration} configuration. + * @deprecated use {@link Bucket4jMSSQL#selectForUpdateBasedBuilder(DataSource)} instead */ + @Deprecated public MSSQLSelectForUpdateBasedProxyManager(SQLProxyConfiguration configuration) { super(configuration.getClientSideConfig()); this.dataSource = Objects.requireNonNull(configuration.getDataSource()); - this.configuration = configuration; + this.primaryKeyMapper = configuration.getPrimaryKeyMapper(); this.removeSqlQuery = MessageFormat.format("DELETE FROM {0} WHERE {1} = ?", configuration.getTableName(), configuration.getIdName()); this.updateSqlQuery = MessageFormat.format("UPDATE {0} SET {1}=? WHERE {2}=?", configuration.getTableName(), configuration.getStateName(), configuration.getIdName()); this.insertSqlQuery = MessageFormat.format( "INSERT INTO {0}({1},{2}) VALUES(?, null)", configuration.getTableName(), configuration.getIdName(), configuration.getStateName()); - this.selectSqlQuery = MessageFormat.format("SELECT {0} FROM {1} WITH(ROWLOCK, UPDLOCK) WHERE {2} = ?", configuration.getStateName(), configuration.getTableName(), configuration.getIdName()); + this.selectSqlQuery = MessageFormat.format("SELECT {0} as state FROM {1} WITH(ROWLOCK, UPDLOCK) WHERE {2} = ?", configuration.getStateName(), configuration.getTableName(), configuration.getIdName()); } @Override @@ -107,10 +122,10 @@ public void commit(Optional requestTimeoutNanos) { public LockAndGetResult tryLockAndGet(Optional requestTimeoutNanos) { try (PreparedStatement selectStatement = connection.prepareStatement(selectSqlQuery)) { applyTimeout(selectStatement, requestTimeoutNanos); - configuration.getPrimaryKeyMapper().set(selectStatement, 1, key); + primaryKeyMapper.set(selectStatement, 1, key); try (ResultSet rs = selectStatement.executeQuery()) { if (rs.next()) { - byte[] data = rs.getBytes(configuration.getStateName()); + byte[] data = rs.getBytes("state"); return LockAndGetResult.locked(data); } else { return LockAndGetResult.notLocked(); @@ -125,7 +140,7 @@ public LockAndGetResult tryLockAndGet(Optional requestTimeoutNanos) { public boolean tryInsertEmptyData(Optional requestTimeoutNanos) { try (PreparedStatement insertStatement = connection.prepareStatement(insertSqlQuery)) { applyTimeout(insertStatement, requestTimeoutNanos); - configuration.getPrimaryKeyMapper().set(insertStatement, 1, key); + primaryKeyMapper.set(insertStatement, 1, key); return insertStatement.executeUpdate() > 0; } catch (SQLException e) { if (e.getErrorCode() == 1205) { @@ -147,7 +162,7 @@ public void update(byte[] data, RemoteBucketState newState, Optional reque try (PreparedStatement updateStatement = connection.prepareStatement(updateSqlQuery)) { applyTimeout(updateStatement, requestTimeoutNanos); updateStatement.setBytes(1, data); - configuration.getPrimaryKeyMapper().set(updateStatement, 2, key); + primaryKeyMapper.set(updateStatement, 2, key); updateStatement.executeUpdate(); } } catch (SQLException e) { @@ -172,7 +187,7 @@ public void release() { public void removeProxy(K key) { try (Connection connection = dataSource.getConnection()) { try(PreparedStatement removeStatement = connection.prepareStatement(removeSqlQuery)) { - configuration.getPrimaryKeyMapper().set(removeStatement, 1, key); + primaryKeyMapper.set(removeStatement, 1, key); removeStatement.executeUpdate(); } } catch (SQLException e) { diff --git a/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/Bucket4jMySQL.java b/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/Bucket4jMySQL.java new file mode 100644 index 000000000..171b0ef66 --- /dev/null +++ b/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/Bucket4jMySQL.java @@ -0,0 +1,50 @@ +package io.github.bucket4j.mysql; + +import java.util.Objects; + +import javax.sql.DataSource; + +import io.github.bucket4j.distributed.jdbc.AbstractJdbcProxyManagerBuilder; +import io.github.bucket4j.distributed.jdbc.PrimaryKeyMapper; + +/** + * Entry point for MySQL integration + */ +public class Bucket4jMySQL { + + /** + * Returns the builder for {@link MySQLSelectForUpdateBasedProxyManager} + * + * @param dataSource + * + * @return new instance of {@link MySQLSelectForUpdateBasedProxyManager} + */ + public static MySQLSelectForUpdateBasedProxyManagerBuilder selectForUpdateBasedBuilder(DataSource dataSource) { + return new MySQLSelectForUpdateBasedProxyManagerBuilder<>(dataSource, PrimaryKeyMapper.LONG); + } + + public static class MySQLSelectForUpdateBasedProxyManagerBuilder extends AbstractJdbcProxyManagerBuilder, MySQLSelectForUpdateBasedProxyManagerBuilder> { + + public MySQLSelectForUpdateBasedProxyManagerBuilder(DataSource dataSource, PrimaryKeyMapper primaryKeyMapper) { + super(dataSource, primaryKeyMapper); + } + + @Override + public MySQLSelectForUpdateBasedProxyManager build() { + return new MySQLSelectForUpdateBasedProxyManager<>(this); + } + + /** + * Specifies the type of primary key. + * + * @param primaryKeyMapper object responsible for setting primary key value in prepared statement. + * + * @return this builder instance + */ + public MySQLSelectForUpdateBasedProxyManagerBuilder primaryKeyMapper(PrimaryKeyMapper primaryKeyMapper) { + super.primaryKeyMapper = (PrimaryKeyMapper) Objects.requireNonNull(primaryKeyMapper); + return (MySQLSelectForUpdateBasedProxyManagerBuilder) this; + } + } + +} diff --git a/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/MySQLSelectForUpdateBasedProxyManager.java b/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/MySQLSelectForUpdateBasedProxyManager.java index 002b9f7ba..f4f4a4b48 100644 --- a/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/MySQLSelectForUpdateBasedProxyManager.java +++ b/bucket4j-mysql/src/main/java/io/github/bucket4j/mysql/MySQLSelectForUpdateBasedProxyManager.java @@ -21,12 +21,13 @@ import com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException; import io.github.bucket4j.BucketExceptions; +import io.github.bucket4j.distributed.jdbc.PrimaryKeyMapper; import io.github.bucket4j.distributed.jdbc.SQLProxyConfiguration; -import io.github.bucket4j.distributed.jdbc.SQLProxyConfigurationBuilder; import io.github.bucket4j.distributed.proxy.generic.select_for_update.AbstractSelectForUpdateBasedProxyManager; import io.github.bucket4j.distributed.proxy.generic.select_for_update.LockAndGetResult; import io.github.bucket4j.distributed.proxy.generic.select_for_update.SelectForUpdateBasedTransaction; import io.github.bucket4j.distributed.remote.RemoteBucketState; +import io.github.bucket4j.mysql.Bucket4jMySQL.MySQLSelectForUpdateBasedProxyManagerBuilder; import javax.sql.DataSource; import java.sql.Connection; @@ -38,37 +39,45 @@ import java.util.Optional; /** - * @author Maxim Bartkov - * The extension of Bucket4j library addressed to support MySQL - * To start work with the MySQL extension you must create a table, which will include the possibility to work with buckets - * In order to do this, your table should include the next columns: id as a PRIMARY KEY (BIGINT) and state (BYTEA) - * To define column names, {@link SQLProxyConfiguration} include {@link io.github.bucket4j.distributed.jdbc.BucketTableSettings} which takes settings for the table to work with Bucket4j - * @see {@link SQLProxyConfigurationBuilder} to get more information how to build {@link SQLProxyConfiguration} + * The extension of Bucket4j library addressed to support MySQL + * + *

This implementation solves transaction/concurrency related problems via "SELECT FOR UPDATE" SQL syntax. * * @param type of primary key */ public class MySQLSelectForUpdateBasedProxyManager extends AbstractSelectForUpdateBasedProxyManager { private final DataSource dataSource; - private final SQLProxyConfiguration configuration; + private final PrimaryKeyMapper primaryKeyMapper; private final String removeSqlQuery; private final String updateSqlQuery; private final String insertSqlQuery; private final String selectSqlQuery; + MySQLSelectForUpdateBasedProxyManager(MySQLSelectForUpdateBasedProxyManagerBuilder builder) { + super(builder.getClientSideConfig()); + this.dataSource = builder.getDataSource(); + this.primaryKeyMapper = builder.getPrimaryKeyMapper(); + this.removeSqlQuery = MessageFormat.format("DELETE FROM {0} WHERE {1} = ?", builder.getTableName(), builder.getIdColumnName()); + updateSqlQuery = MessageFormat.format("UPDATE {0} SET {1}=? WHERE {2}=?", builder.getTableName(), builder.getStateColumnName(), builder.getIdColumnName()); + insertSqlQuery = MessageFormat.format("INSERT IGNORE INTO {0}({1}, {2}) VALUES(?, null)", + builder.getTableName(), builder.getIdColumnName(), builder.getStateColumnName()); + selectSqlQuery = MessageFormat.format("SELECT {0} as state FROM {1} WHERE {2} = ? FOR UPDATE", builder.getStateColumnName(), builder.getTableName(), builder.getIdColumnName()); + } + /** - * - * @param configuration {@link SQLProxyConfiguration} configuration. + * @deprecated use {@link Bucket4jMySQL#selectForUpdateBasedBuilder(DataSource)} */ + @Deprecated public MySQLSelectForUpdateBasedProxyManager(SQLProxyConfiguration configuration) { super(configuration.getClientSideConfig()); this.dataSource = Objects.requireNonNull(configuration.getDataSource()); - this.configuration = configuration; + this.primaryKeyMapper = configuration.getPrimaryKeyMapper(); this.removeSqlQuery = MessageFormat.format("DELETE FROM {0} WHERE {1} = ?", configuration.getTableName(), configuration.getIdName()); updateSqlQuery = MessageFormat.format("UPDATE {0} SET {1}=? WHERE {2}=?", configuration.getTableName(), configuration.getStateName(), configuration.getIdName()); insertSqlQuery = MessageFormat.format("INSERT IGNORE INTO {0}({1}, {2}) VALUES(?, null)", - configuration.getTableName(), configuration.getIdName(), configuration.getStateName(), configuration.getIdName()); - selectSqlQuery = MessageFormat.format("SELECT {0} FROM {1} WHERE {2} = ? FOR UPDATE", configuration.getStateName(), configuration.getTableName(), configuration.getIdName()); + configuration.getTableName(), configuration.getIdName(), configuration.getStateName()); + selectSqlQuery = MessageFormat.format("SELECT {0} as state FROM {1} WHERE {2} = ? FOR UPDATE", configuration.getStateName(), configuration.getTableName(), configuration.getIdName()); } @Override @@ -96,7 +105,7 @@ public void update(byte[] data, RemoteBucketState newState, Optional reque try (PreparedStatement updateStatement = connection.prepareStatement(updateSqlQuery)) { applyTimeout(updateStatement, requestTimeoutNanos); updateStatement.setBytes(1, data); - configuration.getPrimaryKeyMapper().set(updateStatement, 2, key); + primaryKeyMapper.set(updateStatement, 2, key); updateStatement.executeUpdate(); } } catch (SQLException e) { @@ -135,12 +144,12 @@ public void commit(Optional requestTimeoutNanos) { public LockAndGetResult tryLockAndGet(Optional requestTimeoutNanos) { try (PreparedStatement selectStatement = connection.prepareStatement(selectSqlQuery)) { applyTimeout(selectStatement, requestTimeoutNanos); - configuration.getPrimaryKeyMapper().set(selectStatement, 1, key); + primaryKeyMapper.set(selectStatement, 1, key); try (ResultSet rs = selectStatement.executeQuery()) { if (!rs.next()) { return LockAndGetResult.notLocked(); } - byte[] bucketStateBeforeTransaction = rs.getBytes(configuration.getStateName()); + byte[] bucketStateBeforeTransaction = rs.getBytes("state"); return LockAndGetResult.locked(bucketStateBeforeTransaction); } } catch (SQLException e) { @@ -152,7 +161,7 @@ public LockAndGetResult tryLockAndGet(Optional requestTimeoutNanos) { public boolean tryInsertEmptyData(Optional requestTimeoutNanos) { try (PreparedStatement insertStatement = connection.prepareStatement(insertSqlQuery)) { applyTimeout(insertStatement, requestTimeoutNanos); - configuration.getPrimaryKeyMapper().set(insertStatement, 1, key); + primaryKeyMapper.set(insertStatement, 1, key); insertStatement.executeUpdate(); return true; } catch (MySQLTransactionRollbackException conflict) { @@ -170,7 +179,7 @@ public boolean tryInsertEmptyData(Optional requestTimeoutNanos) { public void removeProxy(K key) { try (Connection connection = dataSource.getConnection()) { try(PreparedStatement removeStatement = connection.prepareStatement(removeSqlQuery)) { - configuration.getPrimaryKeyMapper().set(removeStatement, 1, key); + primaryKeyMapper.set(removeStatement, 1, key); removeStatement.executeUpdate(); } } catch (SQLException e) { diff --git a/bucket4j-oracle/src/main/java/io/github/bucket4j/oracle/Bucket4jOracle.java b/bucket4j-oracle/src/main/java/io/github/bucket4j/oracle/Bucket4jOracle.java new file mode 100644 index 000000000..863795e87 --- /dev/null +++ b/bucket4j-oracle/src/main/java/io/github/bucket4j/oracle/Bucket4jOracle.java @@ -0,0 +1,50 @@ +package io.github.bucket4j.oracle; + +import java.util.Objects; + +import javax.sql.DataSource; + +import io.github.bucket4j.distributed.jdbc.AbstractJdbcProxyManagerBuilder; +import io.github.bucket4j.distributed.jdbc.PrimaryKeyMapper; + +/** + * Entry point for Oracle Database integration + */ +public class Bucket4jOracle { + + /** + * Returns the builder for {@link OracleSelectForUpdateBasedProxyManager} + * + * @param dataSource + * + * @return new instance of {@link OracleSelectForUpdateBasedProxyManager} + */ + public static OracleSelectForUpdateBasedProxyManagerBuilder selectForUpdateBasedBuilder(DataSource dataSource) { + return new OracleSelectForUpdateBasedProxyManagerBuilder<>(dataSource, PrimaryKeyMapper.LONG); + } + + public static class OracleSelectForUpdateBasedProxyManagerBuilder extends AbstractJdbcProxyManagerBuilder, OracleSelectForUpdateBasedProxyManagerBuilder> { + + public OracleSelectForUpdateBasedProxyManagerBuilder(DataSource dataSource, PrimaryKeyMapper primaryKeyMapper) { + super(dataSource, primaryKeyMapper); + } + + @Override + public OracleSelectForUpdateBasedProxyManager build() { + return new OracleSelectForUpdateBasedProxyManager<>(this); + } + + /** + * Specifies the type of primary key. + * + * @param primaryKeyMapper object responsible for setting primary key value in prepared statement. + * + * @return this builder instance + */ + public OracleSelectForUpdateBasedProxyManagerBuilder primaryKeyMapper(PrimaryKeyMapper primaryKeyMapper) { + super.primaryKeyMapper = (PrimaryKeyMapper) Objects.requireNonNull(primaryKeyMapper); + return (OracleSelectForUpdateBasedProxyManagerBuilder) this; + } + } + +} diff --git a/bucket4j-oracle/src/main/java/io/github/bucket4j/oracle/OracleSelectForUpdateBasedProxyManager.java b/bucket4j-oracle/src/main/java/io/github/bucket4j/oracle/OracleSelectForUpdateBasedProxyManager.java index 5d00b9f55..c4a8fbe2e 100644 --- a/bucket4j-oracle/src/main/java/io/github/bucket4j/oracle/OracleSelectForUpdateBasedProxyManager.java +++ b/bucket4j-oracle/src/main/java/io/github/bucket4j/oracle/OracleSelectForUpdateBasedProxyManager.java @@ -20,10 +20,8 @@ package io.github.bucket4j.oracle; import io.github.bucket4j.BucketExceptions; -import io.github.bucket4j.distributed.jdbc.BucketTableSettings; +import io.github.bucket4j.distributed.jdbc.PrimaryKeyMapper; import io.github.bucket4j.distributed.jdbc.SQLProxyConfiguration; -import io.github.bucket4j.distributed.jdbc.SQLProxyConfigurationBuilder; -import io.github.bucket4j.distributed.proxy.ClientSideConfig; import io.github.bucket4j.distributed.proxy.generic.select_for_update.AbstractSelectForUpdateBasedProxyManager; import io.github.bucket4j.distributed.proxy.generic.select_for_update.LockAndGetResult; import io.github.bucket4j.distributed.proxy.generic.select_for_update.SelectForUpdateBasedTransaction; @@ -40,38 +38,55 @@ import java.util.Optional; /** - * @author Vladimir Bukhtoyarov + * The extension of Bucket4j library addressed to support "Oracle database". + * + *

This implementation solves transaction/concurrency related problems via "SELECT FOR UPDATE" SQL syntax. * * @param type of primary key */ public class OracleSelectForUpdateBasedProxyManager extends AbstractSelectForUpdateBasedProxyManager { private final DataSource dataSource; - private final SQLProxyConfiguration configuration; + private final PrimaryKeyMapper primaryKeyMapper; private final String removeSqlQuery; private final String updateSqlQuery; private final String insertSqlQuery; private final String selectSqlQuery; + OracleSelectForUpdateBasedProxyManager(Bucket4jOracle.OracleSelectForUpdateBasedProxyManagerBuilder builder) { + super(builder.getClientSideConfig()); + this.dataSource = builder.getDataSource(); + this.primaryKeyMapper = builder.getPrimaryKeyMapper(); + this.removeSqlQuery = MessageFormat.format("DELETE FROM {0} WHERE {1} = ?", builder.getTableName(), builder.getIdColumnName()); + this.updateSqlQuery = MessageFormat.format("UPDATE {0} SET {1}=? WHERE {2}=?", builder.getTableName(), builder.getStateColumnName(), builder.getIdColumnName()); + this.insertSqlQuery = MessageFormat.format( + "MERGE INTO {0} b1\n" + + "USING (SELECT ? {1} FROM dual) b2\n" + + "ON (b1.{1} = b2.{1})\n" + + "WHEN NOT matched THEN\n" + + "INSERT ({1}, {2}) VALUES (?, null)", + builder.getTableName(), builder.getIdColumnName(), builder.getStateColumnName()); + this.selectSqlQuery = MessageFormat.format("SELECT {0} as state FROM {1} WHERE {2} = ? FOR UPDATE", builder.getStateColumnName(), builder.getTableName(), builder.getIdColumnName()); + } + /** - * - * @param configuration {@link SQLProxyConfiguration} configuration. + * @deprecated use {@link Bucket4jOracle#selectForUpdateBasedBuilder(DataSource)} */ + @Deprecated public OracleSelectForUpdateBasedProxyManager(SQLProxyConfiguration configuration) { super(configuration.getClientSideConfig()); this.dataSource = Objects.requireNonNull(configuration.getDataSource()); - this.configuration = configuration; + this.primaryKeyMapper = configuration.getPrimaryKeyMapper(); this.removeSqlQuery = MessageFormat.format("DELETE FROM {0} WHERE {1} = ?", configuration.getTableName(), configuration.getIdName()); this.updateSqlQuery = MessageFormat.format("UPDATE {0} SET {1}=? WHERE {2}=?", configuration.getTableName(), configuration.getStateName(), configuration.getIdName()); - //this.insertSqlQuery = MessageFormat.format("INSERT INTO {0}({1}, {2}) VALUES(?, null) ON CONFLICT({3}) DO NOTHING", this.insertSqlQuery = MessageFormat.format( "MERGE INTO {0} b1\n" + "USING (SELECT ? {1} FROM dual) b2\n" + "ON (b1.{1} = b2.{1})\n" + "WHEN NOT matched THEN\n" + "INSERT ({1}, {2}) VALUES (?, null)", - configuration.getTableName(), configuration.getIdName(), configuration.getStateName(), configuration.getIdName()); - this.selectSqlQuery = MessageFormat.format("SELECT {0} FROM {1} WHERE {2} = ? FOR UPDATE", configuration.getStateName(), configuration.getTableName(), configuration.getIdName()); + configuration.getTableName(), configuration.getIdName(), configuration.getStateName()); + this.selectSqlQuery = MessageFormat.format("SELECT {0} as state FROM {1} WHERE {2} = ? FOR UPDATE", configuration.getStateName(), configuration.getTableName(), configuration.getIdName()); } @Override @@ -115,10 +130,10 @@ public void commit(Optional requestTimeoutNanos) { public LockAndGetResult tryLockAndGet(Optional requestTimeoutNanos) { try (PreparedStatement selectStatement = connection.prepareStatement(selectSqlQuery)) { applyTimeout(selectStatement, requestTimeoutNanos); - configuration.getPrimaryKeyMapper().set(selectStatement, 1, key); + primaryKeyMapper.set(selectStatement, 1, key); try (ResultSet rs = selectStatement.executeQuery()) { if (rs.next()) { - byte[] data = rs.getBytes(configuration.getStateName()); + byte[] data = rs.getBytes("state"); return LockAndGetResult.locked(data); } else { return LockAndGetResult.notLocked(); @@ -133,8 +148,8 @@ public LockAndGetResult tryLockAndGet(Optional requestTimeoutNanos) { public boolean tryInsertEmptyData(Optional requestTimeoutNanos) { try (PreparedStatement insertStatement = connection.prepareStatement(insertSqlQuery)) { applyTimeout(insertStatement, requestTimeoutNanos); - configuration.getPrimaryKeyMapper().set(insertStatement, 1, key); - configuration.getPrimaryKeyMapper().set(insertStatement, 2, key); + primaryKeyMapper.set(insertStatement, 1, key); + primaryKeyMapper.set(insertStatement, 2, key); return insertStatement.executeUpdate() > 0; } catch (SQLIntegrityConstraintViolationException integrityException) { return false; @@ -149,7 +164,7 @@ public void update(byte[] data, RemoteBucketState newState, Optional reque try (PreparedStatement updateStatement = connection.prepareStatement(updateSqlQuery)) { applyTimeout(updateStatement, requestTimeoutNanos); updateStatement.setBytes(1, data); - configuration.getPrimaryKeyMapper().set(updateStatement, 2, key); + primaryKeyMapper.set(updateStatement, 2, key); updateStatement.executeUpdate(); } } catch (SQLException e) { @@ -165,16 +180,14 @@ public void release() { throw new BucketExceptions.BucketExecutionException(e); } } - }; - } @Override public void removeProxy(K key) { try (Connection connection = dataSource.getConnection()) { try(PreparedStatement removeStatement = connection.prepareStatement(removeSqlQuery)) { - configuration.getPrimaryKeyMapper().set(removeStatement, 1, key); + primaryKeyMapper.set(removeStatement, 1, key); removeStatement.executeUpdate(); } } catch (SQLException e) { diff --git a/bucket4j-postgresql/src/main/java/io/github/bucket4j/postgresql/Bucket4jPostgreSQL.java b/bucket4j-postgresql/src/main/java/io/github/bucket4j/postgresql/Bucket4jPostgreSQL.java index 42fb3099b..dac5c4487 100644 --- a/bucket4j-postgresql/src/main/java/io/github/bucket4j/postgresql/Bucket4jPostgreSQL.java +++ b/bucket4j-postgresql/src/main/java/io/github/bucket4j/postgresql/Bucket4jPostgreSQL.java @@ -7,7 +7,6 @@ import io.github.bucket4j.distributed.jdbc.AbstractJdbcProxyManagerBuilder; import io.github.bucket4j.distributed.jdbc.LockIdSupplier; import io.github.bucket4j.distributed.jdbc.PrimaryKeyMapper; -import io.github.bucket4j.distributed.jdbc.SQLProxyConfigurationBuilder; /** * Entry point for PostgreSQL integration @@ -36,7 +35,6 @@ public static PostgreSQLSelectForUpdateBasedProxyManagerBuilder selectForU return new PostgreSQLSelectForUpdateBasedProxyManagerBuilder<>(dataSource, PrimaryKeyMapper.LONG); } - public static class PostgreSQLadvisoryLockBasedProxyManagerBuilder extends AbstractJdbcProxyManagerBuilder, PostgreSQLadvisoryLockBasedProxyManagerBuilder> { private LockIdSupplier lockIdSupplier = (LockIdSupplier) LockIdSupplier.DEFAULT; @@ -67,7 +65,7 @@ public PostgreSQLadvisoryLockBasedProxyManagerBuilder lockIdSupplier(LockIdSu * * @param primaryKeyMapper object responsible for setting primary key value in prepared statement. * - * @return {@link SQLProxyConfigurationBuilder} + * @return this builder instance */ public PostgreSQLadvisoryLockBasedProxyManagerBuilder primaryKeyMapper(PrimaryKeyMapper primaryKeyMapper) { super.primaryKeyMapper = (PrimaryKeyMapper) Objects.requireNonNull(primaryKeyMapper); @@ -96,7 +94,7 @@ public PostgreSQLSelectForUpdateBasedProxyManager build() { * * @param primaryKeyMapper object responsible for setting primary key value in prepared statement. * - * @return {@link SQLProxyConfigurationBuilder} + * @return this builder instance */ public PostgreSQLSelectForUpdateBasedProxyManagerBuilder primaryKeyMapper(PrimaryKeyMapper primaryKeyMapper) { super.primaryKeyMapper = (PrimaryKeyMapper) Objects.requireNonNull(primaryKeyMapper); diff --git a/bucket4j-postgresql/src/main/java/io/github/bucket4j/postgresql/PostgreSQLSelectForUpdateBasedProxyManager.java b/bucket4j-postgresql/src/main/java/io/github/bucket4j/postgresql/PostgreSQLSelectForUpdateBasedProxyManager.java index 93e7fff14..9a56bf681 100644 --- a/bucket4j-postgresql/src/main/java/io/github/bucket4j/postgresql/PostgreSQLSelectForUpdateBasedProxyManager.java +++ b/bucket4j-postgresql/src/main/java/io/github/bucket4j/postgresql/PostgreSQLSelectForUpdateBasedProxyManager.java @@ -20,10 +20,8 @@ package io.github.bucket4j.postgresql; import io.github.bucket4j.BucketExceptions; -import io.github.bucket4j.distributed.jdbc.BucketTableSettings; import io.github.bucket4j.distributed.jdbc.PrimaryKeyMapper; import io.github.bucket4j.distributed.jdbc.SQLProxyConfiguration; -import io.github.bucket4j.distributed.jdbc.SQLProxyConfigurationBuilder; import io.github.bucket4j.distributed.proxy.generic.select_for_update.AbstractSelectForUpdateBasedProxyManager; import io.github.bucket4j.distributed.proxy.generic.select_for_update.LockAndGetResult; import io.github.bucket4j.distributed.proxy.generic.select_for_update.SelectForUpdateBasedTransaction; @@ -40,20 +38,9 @@ import java.util.Optional; /** - * @author Maxim Bartkov - * * The extension of Bucket4j library addressed to support PostgreSQL - * To start work with the PostgreSQL extension you must create a table, which will include the possibility to work with buckets - * In order to do this, your table should include the next columns: id as a PRIMARY KEY (BIGINT) and state (BYTEA) - * To define column names, {@link SQLProxyConfiguration} include {@link BucketTableSettings} which takes settings for the table to work with Bucket4j. - * - *

This implementation solves transaction related problems via Based on SELECT FOR UPDATE SQL syntax. - * This prevents them from being modified or deleted by other transactions until the current transaction ends. - * That is, other transactions that attempt UPDATE, DELETE, or SELECT FOR UPDATE of these rows will be blocked until the current transaction ends. - * Also, if an UPDATE, DELETE, or SELECT FOR UPDATE from another transaction has already locked a selected row or rows, SELECT FOR UPDATE will wait for the other transaction to complete, and will then lock and return the updated row (or no row, if the row was deleted). - * Within a SERIALIZABLE transaction, however, an error will be thrown if a row to be locked has changed since the transaction started. * - * @see {@link SQLProxyConfigurationBuilder} to get more information how to build {@link SQLProxyConfiguration} + *

This implementation solves transaction/concurrency related problems via "SELECT FOR UPDATE" SQL syntax. * * @param type of primary key */ diff --git a/bucket4j-postgresql/src/main/java/io/github/bucket4j/postgresql/PostgreSQLadvisoryLockBasedProxyManager.java b/bucket4j-postgresql/src/main/java/io/github/bucket4j/postgresql/PostgreSQLadvisoryLockBasedProxyManager.java index 90f610693..e45ab449a 100644 --- a/bucket4j-postgresql/src/main/java/io/github/bucket4j/postgresql/PostgreSQLadvisoryLockBasedProxyManager.java +++ b/bucket4j-postgresql/src/main/java/io/github/bucket4j/postgresql/PostgreSQLadvisoryLockBasedProxyManager.java @@ -20,10 +20,8 @@ package io.github.bucket4j.postgresql; import io.github.bucket4j.BucketExceptions; -import io.github.bucket4j.distributed.jdbc.BucketTableSettings; import io.github.bucket4j.distributed.jdbc.PrimaryKeyMapper; import io.github.bucket4j.distributed.jdbc.SQLProxyConfiguration; -import io.github.bucket4j.distributed.jdbc.SQLProxyConfigurationBuilder; import io.github.bucket4j.distributed.jdbc.LockIdSupplier; import io.github.bucket4j.distributed.proxy.generic.pessimistic_locking.AbstractLockBasedProxyManager; import io.github.bucket4j.distributed.proxy.generic.pessimistic_locking.LockBasedTransaction; @@ -40,19 +38,9 @@ import java.util.Optional; /** - * @author Maxim Bartkov * The extension of Bucket4j library addressed to support PostgreSQL - * To start work with the PostgreSQL extension you must create a table, which will include the possibility to work with buckets - * In order to do this, your table should include the next columns: id as a PRIMARY KEY (BIGINT) and state (BYTEA) - * To define column names, {@link SQLProxyConfiguration} include {@link BucketTableSettings} which takes settings for the table to work with Bucket4j. * - *

This implementation solves transaction related problems via pg_advisory_xact_lock - * locks an application-defined resource, which can be identified either by a single 64-bit key value or two 32-bit key values (note that these two key spaces do not overlap). - * If another session already holds a lock on the same resource identifier, this function will wait until the resource becomes available. - * The lock is exclusive. - * Multiple lock requests stack so that if the same resource is locked three times it must then be unlocked three times to be released for other sessions use. - * The lock is automatically released at the end of the current transaction and cannot be released explicitly. - * @see {@link SQLProxyConfigurationBuilder} to get more information how to build {@link SQLProxyConfiguration} + *

This implementation solves transaction/concurrency related problems via pg_advisory_xact_lock * * @param type of primary key */ @@ -89,7 +77,7 @@ public PostgreSQLadvisoryLockBasedProxyManager(SQLProxyConfiguration configur this.removeSqlQuery = MessageFormat.format("DELETE FROM {0} WHERE {1} = ?", configuration.getTableName(), configuration.getIdName()); this.updateSqlQuery = MessageFormat.format("UPDATE {0} SET {1}=? WHERE {2}=?", configuration.getTableName(), configuration.getStateName(), configuration.getIdName()); this.insertSqlQuery = MessageFormat.format("INSERT INTO {0}({1}, {2}) VALUES(?, ?)", configuration.getTableName(), configuration.getIdName(), configuration.getStateName()); - this.selectSqlQuery = MessageFormat.format("SELECT {0} FROM {1} WHERE {2} = ?", configuration.getStateName(), configuration.getTableName(), configuration.getIdName()); + this.selectSqlQuery = MessageFormat.format("SELECT {0} as state FROM {1} WHERE {2} = ?", configuration.getStateName(), configuration.getTableName(), configuration.getIdName()); } @Override