diff --git a/api/src/main/java/me/zort/sqllib/api/Query.java b/api/src/main/java/me/zort/sqllib/api/Query.java index 76c4c2d..e4e51b2 100644 --- a/api/src/main/java/me/zort/sqllib/api/Query.java +++ b/api/src/main/java/me/zort/sqllib/api/Query.java @@ -1,5 +1,7 @@ package me.zort.sqllib.api; +import java.sql.SQLException; + /** * This class represents a query. * @author ZorTik @@ -21,4 +23,6 @@ default boolean isAncestor() { return getAncestor() == this; } + default void errorSignal(SQLException e) {} + } diff --git a/api/src/main/java/me/zort/sqllib/api/data/QueryRowsResult.java b/api/src/main/java/me/zort/sqllib/api/data/QueryRowsResult.java index a465136..b9a363d 100644 --- a/api/src/main/java/me/zort/sqllib/api/data/QueryRowsResult.java +++ b/api/src/main/java/me/zort/sqllib/api/data/QueryRowsResult.java @@ -2,10 +2,10 @@ import lombok.Getter; -import java.util.ArrayList; +import java.util.LinkedList; @Getter -public class QueryRowsResult extends ArrayList implements QueryResult { +public class QueryRowsResult extends LinkedList implements QueryResult { private final boolean successful; private String rejectMessage = null; diff --git a/core/src/main/java/me/zort/sqllib/SQLConnectionBuilder.java b/core/src/main/java/me/zort/sqllib/SQLConnectionBuilder.java index 230aba5..8546dbc 100644 --- a/core/src/main/java/me/zort/sqllib/SQLConnectionBuilder.java +++ b/core/src/main/java/me/zort/sqllib/SQLConnectionBuilder.java @@ -8,6 +8,7 @@ import me.zort.sqllib.internal.factory.SQLConnectionFactory; import me.zort.sqllib.internal.impl.DefaultSQLEndpoint; import me.zort.sqllib.internal.impl.SQLEndpointImpl; +import me.zort.sqllib.pool.SQLConnectionPool; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -16,6 +17,7 @@ import java.sql.SQLException; import java.util.Objects; +@SuppressWarnings("unused") public final class SQLConnectionBuilder implements Cloneable { public static @NotNull SQLConnectionBuilder of(String address, int port, String database, String username, String password) { @@ -27,15 +29,11 @@ public final class SQLConnectionBuilder implements Cloneable { } public static @NotNull SQLConnectionBuilder ofSQLite(String path) { - SQLConnectionBuilder builder = of(new SQLEndpointImpl("jdbc:sqlite:" + path, null, null)); - builder.withDriver("org.sqlite.JDBC"); - return builder; + return of(new SQLEndpointImpl("jdbc:sqlite:" + path, null, null)).withDriver("org.sqlite.JDBC"); } public static SQLConnectionBuilder of(SQLEndpoint endpoint) { - if(!endpoint.isValid()) { - throw new SQLEndpointNotValidException(endpoint); - } + if(!endpoint.isValid()) throw new SQLEndpointNotValidException(endpoint); return new SQLConnectionBuilder(endpoint); } @@ -53,9 +51,7 @@ public SQLConnectionBuilder(@NotNull String address, int port, @NotNull String d public SQLConnectionBuilder(@Nullable SQLEndpoint endpoint) { this.endpoint = endpoint; - this.jdbc = endpoint != null - ? endpoint.buildJdbc() - : null; + this.jdbc = endpoint != null ? endpoint.buildJdbc() : null; } public @NotNull SQLConnectionBuilder withEndpoint(final SQLEndpoint endpoint) { @@ -65,10 +61,7 @@ public SQLConnectionBuilder(@Nullable SQLEndpoint endpoint) { } public @NotNull SQLConnectionBuilder withParam(final @NotNull String key, final @NotNull String value) { - if (endpoint != null) { - jdbc += (jdbc.contains("?") ? "&" : "?"); - jdbc += key + "=" + value; - } + if (endpoint != null) jdbc += (jdbc.contains("?") ? "&" : "?") + (key + "=" + value); return this; } @@ -92,11 +85,13 @@ public SQLConnectionBuilder(@Nullable SQLEndpoint endpoint) { driver = Constants.DEFAULT_DRIVER; } SQLConnectionFactory connectionFactory = new BuilderSQLConnectionFactory(this, driver); - if(jdbc.contains("jdbc:sqlite")) { - return new SQLiteDatabaseConnectionImpl(connectionFactory, options); - } else { - return new SQLDatabaseConnectionImpl(connectionFactory, options); - } + return jdbc.contains("jdbc:sqlite") + ? new SQLiteDatabaseConnectionImpl(connectionFactory, options) + : new SQLDatabaseConnectionImpl(connectionFactory, options); + } + + public @NotNull SQLConnectionPool createPool(final @NotNull SQLConnectionPool.Options options) { + return new SQLConnectionPool(this, options); } @Override diff --git a/core/src/main/java/me/zort/sqllib/SQLDatabaseConnection.java b/core/src/main/java/me/zort/sqllib/SQLDatabaseConnection.java index 233428b..27e1c9a 100644 --- a/core/src/main/java/me/zort/sqllib/SQLDatabaseConnection.java +++ b/core/src/main/java/me/zort/sqllib/SQLDatabaseConnection.java @@ -1,6 +1,7 @@ package me.zort.sqllib; import lombok.AllArgsConstructor; +import lombok.Data; import lombok.Getter; import me.zort.sqllib.api.Query; import me.zort.sqllib.api.SQLConnection; @@ -12,6 +13,8 @@ import me.zort.sqllib.internal.impl.QueryResultImpl; import me.zort.sqllib.internal.query.*; import me.zort.sqllib.internal.query.part.SetStatement; +import me.zort.sqllib.transaction.Transaction; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -41,6 +44,12 @@ public SQLDatabaseConnection(final @NotNull SQLConnectionFactory connectionFacto SQLConnectionRegistry.register(this); } + /** + * @deprecated Use {@link SQLDatabaseConnection#createProxy(Class)} instead. + */ + @Deprecated + public abstract T createGate(Class mappingInterface); + /** * Constructs a mapping repository based on provided interface. * The interface should follow rules for creating mapping repositories @@ -72,8 +81,8 @@ public SQLDatabaseConnection(final @NotNull SQLConnectionFactory connectionFacto * @return Mapping repository. * @param Type of mapping repository. */ - public abstract T createGate(Class mappingInterface); - public abstract T createGate(Class mappingInterface, @NotNull StatementMappingOptions options); + public abstract T createProxy(Class mappingInterface); + public abstract T createProxy(Class mappingInterface, @NotNull StatementMappingOptions options); public abstract boolean buildEntitySchema(String tableName, Class entityClass); /** @@ -112,27 +121,21 @@ public SQLDatabaseConnection(final @NotNull SQLConnectionFactory connectionFacto */ public abstract QueryResult exec(Query query); public abstract QueryResult exec(String query); + @ApiStatus.Experimental + public abstract Transaction beginTransaction(); + @ApiStatus.Experimental + public abstract void closeTransaction(); + @ApiStatus.Experimental + @Nullable + public abstract Transaction getTransaction(); + public abstract boolean isTransactionActive(); protected abstract DefsVals buildDefsVals(Object obj); public abstract boolean isLogSqlErrors(); public abstract boolean isDebug(); - /** - * Saves this mapping object into database using upsert query. - *

- * All mapping strategies are described in: - * {@link SQLDatabaseConnection#query(Query, Class)}. - * - * @param table Table to save into. - * @param obj The object to save. - * @return Result of the query. - */ - // by default, it creates and upsert request. - public QueryResult save(final @NotNull String table, final @NotNull Object obj) { - DefsVals defsVals = buildDefsVals(obj); - - if(defsVals == null) return new QueryResultImpl(false); - - return save(obj).table(table).execute(); + public UpsertQuery save(final @NotNull String table, final @NotNull Object obj) { + if(buildDefsVals(obj) == null) throw new IllegalArgumentException("Cannot create save query! (defsVals == null)"); + return save(obj).table(table); } public UpsertQuery save(final @NotNull Object obj) { @@ -240,4 +243,10 @@ protected static class DefsVals { private final String[] defs; private final SQLDatabaseConnectionImpl.UnknownValueWrapper[] vals; } + + @AllArgsConstructor + @Data + public static class UnknownValueWrapper { + private Object object; + } } diff --git a/core/src/main/java/me/zort/sqllib/SQLDatabaseConnectionImpl.java b/core/src/main/java/me/zort/sqllib/SQLDatabaseConnectionImpl.java index 0b72a51..0494393 100644 --- a/core/src/main/java/me/zort/sqllib/SQLDatabaseConnectionImpl.java +++ b/core/src/main/java/me/zort/sqllib/SQLDatabaseConnectionImpl.java @@ -24,6 +24,7 @@ import me.zort.sqllib.mapping.DefaultResultAdapter; import me.zort.sqllib.mapping.DefaultStatementMappingFactory; import me.zort.sqllib.pool.PooledSQLDatabaseConnection; +import me.zort.sqllib.transaction.Transaction; import me.zort.sqllib.util.Validator; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -50,6 +51,10 @@ @SuppressWarnings("unused") public class SQLDatabaseConnectionImpl extends PooledSQLDatabaseConnection { + @NotNull static SQLDatabaseOptions defaultOptions() { + return new SQLDatabaseOptions(DEFAULT_AUTO_RECONNECT, DEFAULT_DEBUG, DEFAULT_LOG_SQL_ERRORS, DEFAULT_NAMING_STRATEGY, DEFAULT_GSON); + } + // --***-- Default Constants --***-- public static final boolean DEFAULT_AUTO_RECONNECT = true; @@ -68,6 +73,8 @@ public class SQLDatabaseConnectionImpl extends PooledSQLDatabaseConnection { private transient ObjectMapper objectMapper; @Setter private transient Logger logger; + @Getter(onMethod_ = {@Nullable, @ApiStatus.Experimental}) + private transient Transaction transaction; private int errorCount = 0; /** @@ -88,15 +95,14 @@ public SQLDatabaseConnectionImpl(final @NotNull SQLConnectionFactory connectionF */ public SQLDatabaseConnectionImpl(final @NotNull SQLConnectionFactory connectionFactory, @Nullable SQLDatabaseOptions options) { super(connectionFactory); - if (options == null) - options = new SQLDatabaseOptions( - DEFAULT_AUTO_RECONNECT, DEFAULT_DEBUG, DEFAULT_LOG_SQL_ERRORS, DEFAULT_NAMING_STRATEGY, DEFAULT_GSON); + if (options == null) options = defaultOptions(); this.options = options; this.objectMapper = new DefaultObjectMapper(this); this.mappingFactory = new DefaultStatementMappingFactory(); this.mappingResultAdapter = new DefaultResultAdapter(); this.errorStateHandlers = new CopyOnWriteArrayList<>(); + this.transaction = null; this.logger = Logger.getGlobal(); // Default backup value resolvers. @@ -138,7 +144,7 @@ public void addErrorHandler(final @NotNull ErrorStateObserver observer) { } /** - * Constructs a mapping repository based on provided interface. + * Constructs a mapping proxy based on provided interface. * The interface should follow rules for creating mapping repositories * in this library. * @@ -146,11 +152,21 @@ public void addErrorHandler(final @NotNull ErrorStateObserver observer) { * @return Mapping repository. * @param Type of mapping repository. * - * @see SQLDatabaseConnection#createGate(Class, StatementMappingOptions) + * @see SQLDatabaseConnection#createProxy(Class, StatementMappingOptions) + */ + public T createProxy(Class mappingInterface) { + return createProxy(mappingInterface, new StatementMappingOptions.Builder().build()); + } + + /** + * Replaced with {@link SQLDatabaseConnection#createProxy(Class)}. + * + * @deprecated Will be removed in future releases. */ + @Deprecated @Override public T createGate(Class mappingInterface) { - return createGate(mappingInterface, new StatementMappingOptions.Builder().build()); + return createProxy(mappingInterface); } /** @@ -185,7 +201,7 @@ public T createGate(Class mappingInterface) { * @param Type of mapping repository. */ @SuppressWarnings("unchecked") - public final T createGate(final @NotNull Class mappingInterface, final @NotNull StatementMappingOptions options) { + public final T createProxy(final @NotNull Class mappingInterface, final @NotNull StatementMappingOptions options) { Objects.requireNonNull(mappingInterface, "Mapping interface cannot be null!"); Objects.requireNonNull(options, "Options cannot be null!"); @@ -218,7 +234,7 @@ public final T createGate(final @NotNull Class mappingInterface, final @N public final boolean buildEntitySchema(final @NotNull String tableName, final @NotNull Class entityClass) { Objects.requireNonNull(entityClass, "Entity class cannot be null!"); - ObjectTableConverter converter = new ObjectTableConverter(this, tableName, entityClass); + TableSchemaBuilder converter = new TableSchemaBuilder(this, tableName, entityClass); String query = converter.buildTableQuery(); return exec(() -> query).isSuccessful(); @@ -276,10 +292,10 @@ public QueryRowsResult query(final @NotNull String query) { return query(() -> query); } - @NotNull - QueryRowsResult query(final @NotNull Query query, boolean isRetry) { + @NotNull QueryRowsResult query(final @NotNull Query query, boolean isRetry) { Objects.requireNonNull(query); - if(!handleAutoReconnect()) return new QueryRowsResult<>(false, "Cannot connect to database!"); + if(!handleAutoReconnect()) + return new QueryRowsResult<>(false, "Cannot connect to database!"); try(PreparedStatement stmt = buildStatement(query); ResultSet resultSet = stmt.executeQuery()) { @@ -304,6 +320,7 @@ QueryRowsResult query(final @NotNull Query query, boolean isRetry) { logSqlError(e); notifyError(ErrorCode.QUERY_FATAL); + query.errorSignal(e); return new QueryRowsResult<>(false, e.getMessage()); } } @@ -326,7 +343,7 @@ public QueryResult exec(final @NotNull String query) { return exec(() -> query); } - private QueryResult exec(final @NotNull Query query, boolean isRetry) { + @NotNull QueryResult exec(final @NotNull Query query, boolean isRetry) { if(!handleAutoReconnect()) { return new QueryResultImpl(false, "Cannot connect to database!"); } @@ -341,13 +358,14 @@ private QueryResult exec(final @NotNull Query query, boolean isRetry) { logSqlError(e); notifyError(ErrorCode.QUERY_FATAL); + query.errorSignal(e); return new QueryResultImpl(false, e.getMessage()); } } @SuppressWarnings("unchecked") @Nullable - protected DefsVals buildDefsVals(Object obj) { + protected final DefsVals buildDefsVals(Object obj) { Objects.requireNonNull(obj); Class aClass = obj.getClass(); @@ -387,6 +405,34 @@ protected DefsVals buildDefsVals(Object obj) { return new DefsVals(defs, vals); } + @ApiStatus.Experimental + @SneakyThrows(SQLException.class) + public final Transaction beginTransaction() { + Connection rawConnection = getConnection(); + if (transaction != null && transaction.isActive()) { + throw new IllegalStateException("There is already an active transaction!"); + } else if(rawConnection == null) { + throw new IllegalStateException("Connection is not established!"); + } + rawConnection.setAutoCommit(false); + return transaction = new Transaction(this); + } + + @ApiStatus.Experimental + @SneakyThrows + public final void closeTransaction() { + Transaction transaction = getTransaction(); + if (transaction != null && transaction.isActive()) transaction.commit(); + this.transaction = null; + } + + public final void rollback() throws SQLException { + if (transaction == null || !transaction.isActive()) { + throw new IllegalStateException("There is no active transaction!"); + } + transaction.rollback(); + } + @SuppressWarnings("all") private boolean handleAutoReconnect() { if(options.isAutoReconnect() && !isConnected()) { @@ -430,6 +476,11 @@ public final boolean isDebug() { return options.isDebug(); } + @Override + public final boolean isTransactionActive() { + return transaction != null && transaction.isActive(); + } + @SuppressWarnings("all") private void notifyError(int code) { errorCount++; @@ -456,7 +507,7 @@ private PreparedStatement buildStatement(Query query) throws SQLException { } @RequiredArgsConstructor - private static class DefaultStatementFactory implements StatementFactory { + static class DefaultStatementFactory implements StatementFactory { private final Query query; @@ -469,12 +520,6 @@ public PreparedStatement prepare(Connection connection) throws SQLException { } } - @AllArgsConstructor - @Data - public static class UnknownValueWrapper { - private Object object; - } - public interface ErrorStateObserver { void onErrorState(int code); } diff --git a/core/src/main/java/me/zort/sqllib/SQLiteDatabaseConnectionImpl.java b/core/src/main/java/me/zort/sqllib/SQLiteDatabaseConnectionImpl.java index a537176..beb73ad 100644 --- a/core/src/main/java/me/zort/sqllib/SQLiteDatabaseConnectionImpl.java +++ b/core/src/main/java/me/zort/sqllib/SQLiteDatabaseConnectionImpl.java @@ -5,10 +5,7 @@ import me.zort.sqllib.api.data.QueryRowsResult; import me.zort.sqllib.api.data.Row; import me.zort.sqllib.internal.factory.SQLConnectionFactory; -import me.zort.sqllib.internal.impl.QueryResultImpl; -import me.zort.sqllib.internal.query.InsertQuery; -import me.zort.sqllib.internal.query.UpdateQuery; -import me.zort.sqllib.internal.query.UpsertQuery; +import me.zort.sqllib.internal.query.*; import me.zort.sqllib.internal.query.part.SetStatement; import me.zort.sqllib.util.PrimaryKey; import org.jetbrains.annotations.NotNull; @@ -25,6 +22,7 @@ * @author ZorTik */ public class SQLiteDatabaseConnectionImpl extends SQLDatabaseConnectionImpl { + private final SQLiteDatabaseConnectionImpl identity = this; @SuppressWarnings("unused") public SQLiteDatabaseConnectionImpl(final @NotNull SQLConnectionFactory connectionFactory) { @@ -49,11 +47,9 @@ public SQLiteDatabaseConnectionImpl(final @NotNull SQLConnectionFactory connecti */ @NotNull @Override - public final QueryResult save(@NotNull String table, @NotNull Object obj) { + public final UpsertQuery save(@NotNull String table, @NotNull Object obj) { DefsVals defsVals = buildDefsVals(obj); - if(defsVals == null) { - return new QueryResultImpl(false); - } + if(defsVals == null) throw new IllegalArgumentException("Cannot create save query! (defsVals == null)"); String[] defs = defsVals.getDefs(); UnknownValueWrapper[] vals = defsVals.getVals(); @@ -91,7 +87,7 @@ public final QueryResult save(@NotNull String table, @NotNull Object obj) { if(primaryKey == null) { debug("No primary key found for object " + obj.getClass().getName() + ", so we can't build update condition."); debug("Performing insert query instead: " + insert.buildQuery()); - return insert.execute(); + return new UpsertQueryDecorator(insert); } SetStatement setStmt = update().table(table).set(); @@ -101,23 +97,22 @@ public final QueryResult save(@NotNull String table, @NotNull Object obj) { UpdateQuery update = setStmt.also() .where().isEqual(primaryKey.getColumn(), primaryKey.getValue()) .also(); - return upsert(table, primaryKey, insert, update); + return new UpsertQueryDecorator(upsert(table, primaryKey, insert, update)); } /** - * Simulates upsert query for SQLite. - * It selects rows with limit 1 to check whether there is a row present - * matching the current request and then it performs either insert or - * update according to the result. + * Builds an upsert query for defined table and primary key. + * This returns either a provided insert or update query depending + * on upsert situation. * * @param table Table to upsert into. - * @param primaryKey Primary key of the object. - * @param insert Insert query. - * @param update Update query. - * @return Result of the query. + * @param primaryKey Primary key to use. + * @param insert Insert query to use. + * @param update Update query to use. + * @return Either insert or update query. */ - @NotNull - public final QueryResult upsert(final @NotNull String table, + @Nullable + public final QueryNode upsert(final @NotNull String table, final @NotNull PrimaryKey primaryKey, final @NotNull InsertQuery insert, final @NotNull UpdateQuery update) { @@ -129,22 +124,50 @@ public final QueryResult upsert(final @NotNull String table, .obtainAll(); if(!selectResult.isSuccessful()) { // Not successful, we'll skip other queries. - return new QueryResultImpl(false); - } - if(selectResult.isEmpty()) { - // No results, we'll insert. - return exec(insert); - } else { - return exec(update); + return null; } + return selectResult.isEmpty() ? insert : update; } @NotNull @Override - public QueryResult exec(final @NotNull Query query) { + public QueryResult exec(@NotNull Query query) { if (query instanceof UpsertQuery && ((UpsertQuery) query).getAssignedSaveObject() != null) - return save(((UpsertQuery) query).getTable(), ((UpsertQuery) query).getAssignedSaveObject()); + query = save(((UpsertQuery) query).getTable(), ((UpsertQuery) query).getAssignedSaveObject()); return super.exec(query); } + + class UpsertQueryDecorator extends UpsertQuery { + private final QueryNode query; + + public UpsertQueryDecorator(QueryNode query) { + super(identity); + this.query = query; + } + + @Override + public QueryDetails buildQueryDetails() { + return query.buildQueryDetails(); + } + + @Override + public UpsertQuery into(String table, String... defs) { + notAvailable(); + return null; + } + @Override + public UpsertQuery table(String table) { + notAvailable(); + return null; + } + @Override + public UpsertQuery values(Object... values) { + notAvailable(); + return null; + } + private void notAvailable() { + throw new UnsupportedOperationException("You can't modify upsert query in SQLite mode!"); + } + } } diff --git a/core/src/main/java/me/zort/sqllib/ObjectTableConverter.java b/core/src/main/java/me/zort/sqllib/TableSchemaBuilder.java similarity index 99% rename from core/src/main/java/me/zort/sqllib/ObjectTableConverter.java rename to core/src/main/java/me/zort/sqllib/TableSchemaBuilder.java index 3b9c531..04d048f 100644 --- a/core/src/main/java/me/zort/sqllib/ObjectTableConverter.java +++ b/core/src/main/java/me/zort/sqllib/TableSchemaBuilder.java @@ -13,7 +13,7 @@ import java.util.Objects; @RequiredArgsConstructor -final class ObjectTableConverter { +final class TableSchemaBuilder { private final SQLDatabaseConnection connection; private final String tableName; diff --git a/core/src/main/java/me/zort/sqllib/internal/query/DeleteQuery.java b/core/src/main/java/me/zort/sqllib/internal/query/DeleteQuery.java index bcf1927..70650f4 100644 --- a/core/src/main/java/me/zort/sqllib/internal/query/DeleteQuery.java +++ b/core/src/main/java/me/zort/sqllib/internal/query/DeleteQuery.java @@ -16,6 +16,10 @@ public class DeleteQuery extends QueryNode> implements Executive, C @Getter private final SQLDatabaseConnection connection; + public DeleteQuery() { + this(null); + } + public DeleteQuery(@Nullable SQLDatabaseConnection connection) { this(connection, null); } diff --git a/core/src/main/java/me/zort/sqllib/internal/query/InsertQuery.java b/core/src/main/java/me/zort/sqllib/internal/query/InsertQuery.java index 7bfecc9..282ad10 100644 --- a/core/src/main/java/me/zort/sqllib/internal/query/InsertQuery.java +++ b/core/src/main/java/me/zort/sqllib/internal/query/InsertQuery.java @@ -22,6 +22,10 @@ public class InsertQuery extends QueryNode> implements Executive, C @Getter private final SQLDatabaseConnection connection; + public InsertQuery() { + this(null); + } + public InsertQuery(@Nullable SQLDatabaseConnection connection) { this(connection, null); } diff --git a/core/src/main/java/me/zort/sqllib/internal/query/QueryNode.java b/core/src/main/java/me/zort/sqllib/internal/query/QueryNode.java index a404aad..8401405 100644 --- a/core/src/main/java/me/zort/sqllib/internal/query/QueryNode.java +++ b/core/src/main/java/me/zort/sqllib/internal/query/QueryNode.java @@ -166,7 +166,7 @@ public QueryRowsResult obtainAll(Class mapTo) { } private void requireResultSetAware() { - if (!(this instanceof ResultSetAware)) { + if (!generatesResultSet()) { throw new IllegalStateException("This query node is not ResultSetAware! (Did you mean execute()?)"); } } @@ -179,6 +179,10 @@ public QueryNode getAncestor() { return current; } + public boolean generatesResultSet() { + return this instanceof ResultSetAware; + } + private void debug(String message) { if (getAncestor() instanceof Executive && ((Executive) getAncestor()).getConnection() instanceof SQLDatabaseConnectionImpl) { diff --git a/core/src/main/java/me/zort/sqllib/internal/query/SelectQuery.java b/core/src/main/java/me/zort/sqllib/internal/query/SelectQuery.java index ab8fed2..0136518 100644 --- a/core/src/main/java/me/zort/sqllib/internal/query/SelectQuery.java +++ b/core/src/main/java/me/zort/sqllib/internal/query/SelectQuery.java @@ -19,6 +19,10 @@ public class SelectQuery extends QueryNode> implements Executive, C @Getter private final SQLDatabaseConnection connection; + public SelectQuery() { + this(null); + } + public SelectQuery(@Nullable SQLDatabaseConnection connection, String... cols) { this(connection, null, Arrays.asList(cols)); } diff --git a/core/src/main/java/me/zort/sqllib/internal/query/UpdateQuery.java b/core/src/main/java/me/zort/sqllib/internal/query/UpdateQuery.java index 69520e2..00103f9 100644 --- a/core/src/main/java/me/zort/sqllib/internal/query/UpdateQuery.java +++ b/core/src/main/java/me/zort/sqllib/internal/query/UpdateQuery.java @@ -17,16 +17,16 @@ public class UpdateQuery extends QueryNode> implements Executive, C @Getter private final SQLDatabaseConnection connection; + public UpdateQuery() { + this(null); + } + public UpdateQuery(@Nullable SQLDatabaseConnection connection) { this(connection, null); } public UpdateQuery(@Nullable SQLDatabaseConnection connection, @Nullable String table) { - this(connection, table, QueryPriority.GENERAL.getPrior()); - } - - protected UpdateQuery(@Nullable SQLDatabaseConnection connection, @Nullable String table, int priority) { - super(null, new ArrayList<>(), priority); + super(null, new ArrayList<>(), QueryPriority.GENERAL.getPrior()); this.table = table; this.connection = connection; } diff --git a/core/src/main/java/me/zort/sqllib/pool/SQLConnectionPool.java b/core/src/main/java/me/zort/sqllib/pool/SQLConnectionPool.java index 86880f7..3fab21d 100644 --- a/core/src/main/java/me/zort/sqllib/pool/SQLConnectionPool.java +++ b/core/src/main/java/me/zort/sqllib/pool/SQLConnectionPool.java @@ -35,14 +35,20 @@ public static final class Options { private long borrowObjectTimeout = 5000L; // Block or throw an exception when the pool is exhausted private boolean blockWhenExhausted = true; + // Drop invalid connections + private boolean checkConnectionValidity = true; + // Time in seconds to wait while checking the validity of a connection + private int checkConnectionValidityTimeout = 3; } private final SQLConnectionBuilder builder; private final int maxConnections; private final long borrowObjectTimeout; private final boolean blockWhenExhausted; + private final boolean checkConnectionValidity; + private final int checkConnectionValidityTimeout; - private int errorCount = 0; + private volatile int errorCount = 0; // --***-- Pooled connection caches --***-- private final Queue freeConnections = new ConcurrentLinkedQueue<>(); @@ -65,6 +71,8 @@ public SQLConnectionPool(final @NotNull SQLConnectionBuilder from, final @NotNul this.maxConnections = poolOptions.maxConnections; this.borrowObjectTimeout = poolOptions.borrowObjectTimeout; this.blockWhenExhausted = poolOptions.blockWhenExhausted; + this.checkConnectionValidity = poolOptions.checkConnectionValidity; + this.checkConnectionValidityTimeout = poolOptions.checkConnectionValidityTimeout; } /** @@ -77,7 +85,11 @@ public SQLConnectionPool(final @NotNull SQLConnectionBuilder from, final @NotNul @NotNull public SQLDatabaseConnection getResource() throws SQLException { freeConnections.removeIf(this::expired); - PooledSQLDatabaseConnection polled = freeConnections.poll(); + PooledSQLDatabaseConnection polled; + do { + polled = freeConnections.poll(); + } while(checkConnectionValidity && polled != null && !checkValidity(polled)); + if (polled == null && size() < maxConnections) { polled = establishObject(); } else if(polled == null) { @@ -102,9 +114,9 @@ public SQLDatabaseConnection getResource() throws SQLException { private PooledSQLDatabaseConnection establishObject() throws SQLException { SQLDatabaseConnection polled_ = builder.build(); - if (!(polled_ instanceof PooledSQLDatabaseConnection)) { + if (!(polled_ instanceof PooledSQLDatabaseConnection)) throw new SQLException("Builder does not produce a pooled connection."); - } + PooledSQLDatabaseConnection polled = (PooledSQLDatabaseConnection) polled_; polled.setAssignedPool(this); polled.connect(); @@ -112,26 +124,33 @@ private PooledSQLDatabaseConnection establishObject() throws SQLException { SQLException error = polled.getLastError(); if (error != null) throw error; - if (polled instanceof SQLDatabaseConnectionImpl) { - ((SQLDatabaseConnectionImpl) polled).addErrorHandler(code -> { - errorCount++; - // Remove the connection from the pool and disconnect - // on fatal errors. - freeConnections.remove(polled); - usedConnections.remove(polled); - polled.disconnect(); - }); - } + if (polled instanceof SQLDatabaseConnectionImpl) + ((SQLDatabaseConnectionImpl) polled).addErrorHandler(code -> handleConnectionError(polled)); return polled; } + synchronized void handleConnectionError(PooledSQLDatabaseConnection polled) { + errorCount++; + // Remove the connection from the pool and disconnect + // on fatal errors. + freeConnections.remove(polled); + usedConnections.remove(polled); + polled.disconnect(); + } + void releaseObject(PooledSQLDatabaseConnection connection) { connection.setLastUsed(System.currentTimeMillis()); freeConnections.add(connection); usedConnections.remove(connection); } + private boolean checkValidity(SQLDatabaseConnection connection) throws SQLException { + if (!connection.isConnected()) return false; + assert connection.getConnection() != null; + return connection.getConnection().isValid(checkConnectionValidityTimeout); + } + public int size() { return usedConnections.size() + freeConnections.size(); } diff --git a/core/src/main/java/me/zort/sqllib/transaction/FlowResult.java b/core/src/main/java/me/zort/sqllib/transaction/FlowResult.java new file mode 100644 index 0000000..b29bb6b --- /dev/null +++ b/core/src/main/java/me/zort/sqllib/transaction/FlowResult.java @@ -0,0 +1,22 @@ +package me.zort.sqllib.transaction; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import me.zort.sqllib.api.data.QueryResult; +import me.zort.sqllib.api.data.QueryRowsResult; + +public final class FlowResult extends QueryRowsResult { + @Setter(AccessLevel.PROTECTED) + @Getter + private int brokenIndex = -1; + + public FlowResult(final boolean successful) { + super(successful); + } + + public FlowResult(final boolean successful, String rejectMessage) { + super(successful, rejectMessage); + } + +} diff --git a/core/src/main/java/me/zort/sqllib/transaction/FlowStep.java b/core/src/main/java/me/zort/sqllib/transaction/FlowStep.java new file mode 100644 index 0000000..b1048d9 --- /dev/null +++ b/core/src/main/java/me/zort/sqllib/transaction/FlowStep.java @@ -0,0 +1,16 @@ +package me.zort.sqllib.transaction; + +import me.zort.sqllib.SQLDatabaseConnection; +import me.zort.sqllib.api.data.QueryResult; + +public interface FlowStep { + + Status execute(SQLDatabaseConnection connection); + QueryResult getResult(); // Result if the #execute returned SUCCESS, otherwise null + boolean isOptional(); + + enum Status { + SUCCESS, BREAK, CONTINUE + } + +} diff --git a/core/src/main/java/me/zort/sqllib/transaction/Transaction.java b/core/src/main/java/me/zort/sqllib/transaction/Transaction.java new file mode 100644 index 0000000..a4fa6ef --- /dev/null +++ b/core/src/main/java/me/zort/sqllib/transaction/Transaction.java @@ -0,0 +1,62 @@ +package me.zort.sqllib.transaction; + +import me.zort.sqllib.SQLDatabaseConnection; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Objects; + +public final class Transaction { + + private final SQLDatabaseConnection databaseConnection; + private final Connection connection; + private boolean committed = false; + + public Transaction(SQLDatabaseConnection connection) { + this.databaseConnection = connection; + this.connection = connection.getConnection(); + Objects.requireNonNull(this.connection, "Connection is not established!"); + } + + public void verify() { + if (databaseConnection.getTransaction() != this) + throw new IllegalStateException("Transaction is not assigned!"); + } + + public TransactionFlow.Builder flow() { + verify(); + return new TransactionFlow.Builder(this); + } + + public TransactionFlow flow(final FlowStep[] steps, final TransactionFlow.Options options) { + verify(); + return new TransactionFlow(this, steps, options); + } + + public SQLDatabaseConnection getDatabaseConnection() { + return databaseConnection; + } + + public void commit() throws SQLException { + verify(); + if (!committed) { + connection.commit(); + committed = true; + } + } + + public void rollback() throws SQLException { + verify(); + if (committed) throw new IllegalStateException("Transaction already committed!"); + connection.rollback(); + } + + public boolean isActive() { + return !committed; + } + + void close() { + databaseConnection.closeTransaction(); + } + +} diff --git a/core/src/main/java/me/zort/sqllib/transaction/TransactionFlow.java b/core/src/main/java/me/zort/sqllib/transaction/TransactionFlow.java new file mode 100644 index 0000000..2b0439d --- /dev/null +++ b/core/src/main/java/me/zort/sqllib/transaction/TransactionFlow.java @@ -0,0 +1,127 @@ +package me.zort.sqllib.transaction; + +import lombok.*; +import me.zort.sqllib.api.data.QueryResult; +import me.zort.sqllib.internal.query.QueryNode; +import me.zort.sqllib.transaction.step.FlowStepImpl; +import me.zort.sqllib.util.Arrays; +import org.jetbrains.annotations.NotNull; + +import java.sql.SQLException; + +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public class TransactionFlow { + + private final Transaction transaction; + private final FlowStep[] steps; + private final Options options; + private boolean executed = false; + private int index = -1; + + public FlowResult execute() { + return executeNext(steps.length); + } + + @SneakyThrows(SQLException.class) + public FlowResult executeNext(int count) { + if (executed) throw new IllegalStateException("TransactionFlow already fully executed!"); + + QueryResult[] results = new QueryResult[steps.length]; + int maxIndex = index + count; + int lastIndex = index; + for (int i = 0; i <= maxIndex; i++) { + FlowStep.Status status = steps[i].execute(transaction.getDatabaseConnection()); + if (status.equals(FlowStep.Status.BREAK)) { + if(options.rollbackOnFailure) { + transaction.rollback(); + index = -1; + } + break; + } + results[i] = status.equals(FlowStep.Status.SUCCESS) ? steps[i].getResult() : null; + lastIndex = i; + } + boolean success = true; + final int lastIndexFinal = lastIndex; + while (lastIndex < results.length -1) { + success = false; + results[++lastIndex] = null; + } + + FlowResult result = success + ? new FlowResult(true) + : new FlowResult(false, + String.format("Transaction failed at step %d!", lastIndexFinal+1)); + + result.addAll(java.util.Arrays.asList(results)); + if (!result.isSuccessful()) result.setBrokenIndex(lastIndexFinal+1); + + if (lastIndexFinal >= steps.length - 1) { + executed = true; + + if (options.commitOnSuccess && result.isSuccessful()) + transaction.commit(); + + if (options.autoClose) close(); + } + if (result.isSuccessful()) index = lastIndexFinal; + return result; + } + + private void close() { + transaction.close(); + } + + @SuppressWarnings("unused") + public static final class Builder { + private final Transaction transaction; + private final Options options; + private FlowStep[] steps = new FlowStep[0]; + + public Builder(Transaction transaction) { + this.transaction = transaction; + this.options = new Options(); + } + + public @NotNull Builder step(final @NotNull QueryNode node) { + return step(node, false); + } + + public @NotNull Builder step(final @NotNull QueryNode node, boolean optional) { + return step(new FlowStepImpl(node, optional)); + } + + public @NotNull Builder step(final @NotNull FlowStep step) { + steps = Arrays.add(steps, step); + return this; + } + + public @NotNull Builder rollbackOnFailure(boolean rollbackOnFailure) { + options.setRollbackOnFailure(rollbackOnFailure); + return this; + } + + public @NotNull Builder commitOnSuccess(boolean commitOnSuccess) { + options.setCommitOnSuccess(commitOnSuccess); + return this; + } + + public @NotNull Builder autoClose(boolean autoClose) { + options.setAutoClose(autoClose); + return this; + } + + public @NotNull TransactionFlow create() { + return new TransactionFlow(transaction, steps, options); + } + } + + @NoArgsConstructor + @Data + public static final class Options { + private boolean rollbackOnFailure = true; + private boolean commitOnSuccess = true; + private boolean autoClose = true; + } + +} diff --git a/core/src/main/java/me/zort/sqllib/transaction/step/FlowStepImpl.java b/core/src/main/java/me/zort/sqllib/transaction/step/FlowStepImpl.java new file mode 100644 index 0000000..acf6499 --- /dev/null +++ b/core/src/main/java/me/zort/sqllib/transaction/step/FlowStepImpl.java @@ -0,0 +1,32 @@ +package me.zort.sqllib.transaction.step; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import me.zort.sqllib.SQLDatabaseConnection; +import me.zort.sqllib.api.Query; +import me.zort.sqllib.api.data.QueryResult; +import me.zort.sqllib.internal.query.QueryNode; +import me.zort.sqllib.transaction.FlowStep; + +@RequiredArgsConstructor +public class FlowStepImpl implements FlowStep { + private final Query query; + @Getter + private final boolean optional; + @Getter + private QueryResult result = null; + + @Override + public Status execute(SQLDatabaseConnection connection) { + if (!(query instanceof QueryNode)) throw new IllegalStateException("FlowStepImpl accepts only QueryNode!"); + + QueryNode node = (QueryNode) query; + node = node.getAncestor(); + QueryResult localResult = node.generatesResultSet() + ? connection.query(node) + : connection.exec(node); + + if (localResult.isSuccessful()) result = localResult; + return localResult.isSuccessful() ? Status.SUCCESS : (optional ? Status.CONTINUE : Status.BREAK); + } +} diff --git a/core/src/main/java/me/zort/sqllib/util/DefsVals.java b/core/src/main/java/me/zort/sqllib/util/DefsVals.java deleted file mode 100644 index c582e5b..0000000 --- a/core/src/main/java/me/zort/sqllib/util/DefsVals.java +++ /dev/null @@ -1,7 +0,0 @@ -package me.zort.sqllib.util; - -public final class DefsVals { - - - -} diff --git a/examples/src/main/java/me/zort/sqllib/QuickStart.java b/examples/src/main/java/me/zort/sqllib/QuickStart.java index 1ab7b0e..4f59985 100644 --- a/examples/src/main/java/me/zort/sqllib/QuickStart.java +++ b/examples/src/main/java/me/zort/sqllib/QuickStart.java @@ -75,7 +75,7 @@ public User(String firstname, String lastname) { public void saveUser() { User user = new User("John", "Doe"); - QueryResult result = connection.save("users", user); + QueryResult result = connection.save("users", user).execute(); } public void loadUser() { diff --git a/src/test/java/me/zort/sqllib/test/TestCase1.java b/src/test/java/me/zort/sqllib/test/TestCase1.java index 6cd1f56..d58aba4 100644 --- a/src/test/java/me/zort/sqllib/test/TestCase1.java +++ b/src/test/java/me/zort/sqllib/test/TestCase1.java @@ -3,7 +3,6 @@ import lombok.AllArgsConstructor; import lombok.extern.log4j.Log4j2; import me.zort.sqllib.SQLConnectionBuilder; -import me.zort.sqllib.SQLDatabaseConnectionImpl; import me.zort.sqllib.internal.annotation.NullableField; import me.zort.sqllib.internal.annotation.PrimaryKey; import me.zort.sqllib.pool.SQLConnectionPool; @@ -14,6 +13,7 @@ import me.zort.sqllib.api.data.Row; import me.zort.sqllib.api.provider.Select; import me.zort.sqllib.internal.impl.DefaultSQLEndpoint; +import me.zort.sqllib.transaction.FlowResult; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.config.Configurator; import org.junit.jupiter.api.*; @@ -42,7 +42,6 @@ public void prepareLogging() { Configurator.setAllLevels("", Level.ALL); } - @Timeout(15) @BeforeAll public void prepare() { System.out.println("Preparing test case..."); @@ -76,11 +75,10 @@ public void prepare() { System.out.println("Tables prepared, test cases ready"); } - @Timeout(10) @Test public void test1_Upsert() { System.out.println("Testing upsert (save)..."); - assertTrue(connection.save(TABLE_NAME, user1).isSuccessful()); + assertTrue(connection.save(TABLE_NAME, user1).execute().isSuccessful()); System.out.println("Save successful"); System.out.println("Testing upsert..."); assertTrue(connection.upsert() @@ -93,7 +91,6 @@ public void test1_Upsert() { System.out.println("Upsert successful"); } - @Timeout(10) @Test public void test2_Select() { System.out.println("Testing select..."); @@ -108,7 +105,6 @@ public void test2_Select() { System.out.println("Select successful"); } - @Timeout(10) @Test public void test3_Update() { System.out.println("Testing update..."); @@ -128,7 +124,6 @@ public void test3_Update() { assertEquals(300, rowOptional.get().get("points")); } - @Timeout(10) @Test public void test4_Security() { // SQL Injection check @@ -143,7 +138,6 @@ public void test4_Security() { test2_Select(); } - @Timeout(10) @Test public void test5_Delete() { QueryResult result = connection.delete() @@ -154,7 +148,6 @@ public void test5_Delete() { assertNull(result.getRejectMessage()); } - @Timeout(10) @Test public void test6_Pool() { SQLConnectionPool.Options options = new SQLConnectionPool.Options(); @@ -164,7 +157,7 @@ public void test6_Pool() { SQLConnectionPool pool = new SQLConnectionPool(builder, options); try (SQLDatabaseConnection connection = pool.getResource()) { System.out.println("Got connection from pool"); - assertTrue(connection.save(TABLE_NAME, user1).isSuccessful()); + assertTrue(connection.save(TABLE_NAME, user1).execute().isSuccessful()); } catch(SQLException e) { throw new RuntimeException(e); } @@ -186,8 +179,22 @@ public void test6_Pool() { assertEquals(0, pool.size()); } + @Test + public void test6_Transactions() { + FlowResult result1 = connection.beginTransaction() + .flow() + .step(connection.save(TABLE_NAME, user1)) + .step(connection.select() + .from(TABLE_NAME) + .where().isEqual("nickname", user1.getNickname())) + .create() + .execute(); + assertTrue(result1.isSuccessful()); + assertEquals(2, result1.size()); + assertTrue(result1.get(1) instanceof QueryRowsResult); + assertEquals(1, ((QueryRowsResult) result1.get(1)).size()); + } - @Timeout(5) @Test public void test7_Close() { System.out.println("Closing connection...");