diff --git a/src/database/DB.java b/src/database/DB.java index aa2e5449..39d118ac 100644 --- a/src/database/DB.java +++ b/src/database/DB.java @@ -3,7 +3,6 @@ package database; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.math.BigDecimal; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -168,65 +167,6 @@ public class DB { return result; } - /** - * Format table and column names into an INSERT INTO ... SQL statement. - *

- * Full form is: - *

- * INSERT INTO table (column, ...) VALUES (?, ...) ON DUPLICATE KEY UPDATE column=?, ... - *

- * Note that HSQLDB needs to put into mySQL compatibility mode first via "SET DATABASE SQL SYNTAX MYS TRUE". - * - * @param table - * @param columns - * @return String - */ - public static String formatInsertWithPlaceholders(String table, String... columns) { - String[] placeholders = new String[columns.length]; - Arrays.setAll(placeholders, (int i) -> "?"); - - StringBuilder output = new StringBuilder(); - output.append("INSERT INTO "); - output.append(table); - output.append(" ("); - output.append(String.join(", ", columns)); - output.append(") VALUES ("); - output.append(String.join(", ", placeholders)); - output.append(") ON DUPLICATE KEY UPDATE "); - output.append(String.join("=?, ", columns)); - output.append("=?"); - return output.toString(); - } - - /** - * Binds Objects to PreparedStatement based on INSERT INTO ... ON DUPLICATE KEY UPDATE ... - *

- * Note that each object is bound to two place-holders based on this SQL syntax: - *

- * INSERT INTO table (column, ...) VALUES (?, ...) ON DUPLICATE KEY UPDATE column=?, ... - *

- * Requires that mySQL SQL syntax support is enabled during connection. - * - * @param preparedStatement - * @param objects - * @throws SQLException - */ - public static void bindInsertPlaceholders(PreparedStatement preparedStatement, Object... objects) throws SQLException { - for (int i = 0; i < objects.length; ++i) { - Object object = objects[i]; - - // Special treatment for BigDecimals so that they retain their "scale", - // which would otherwise be assumed as 0. - if (object instanceof BigDecimal) { - preparedStatement.setBigDecimal(i + 1, (BigDecimal) object); - preparedStatement.setBigDecimal(i + objects.length + 1, (BigDecimal) object); - } else { - preparedStatement.setObject(i + 1, object); - preparedStatement.setObject(i + objects.length + 1, object); - } - } - } - /** * Execute SQL using byte[] as 1st placeholder. *

diff --git a/src/database/SaveHelper.java b/src/database/SaveHelper.java new file mode 100644 index 00000000..bb159a71 --- /dev/null +++ b/src/database/SaveHelper.java @@ -0,0 +1,132 @@ +package database; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Database helper for building, and executing, INSERT INTO ... ON DUPLICATE KEY UPDATE ... statements. + *

+ * Columns, and corresponding values, are bound via close-coupled pairs in a chain thus: + *

+ * {@code SaveHelper helper = new SaveHelper(connection, "TableName"); }
+ * {@code helper.bind("column_name", someColumnValue).bind("column2", columnValue2); }
+ * {@code helper.execute(); }
+ * + */ +public class SaveHelper { + + private Connection connection; + private String table; + + private List columns = new ArrayList(); + private List objects = new ArrayList(); + + /** + * Construct a SaveHelper, using SQL Connection and table name. + * + * @param connection + * @param table + */ + public SaveHelper(Connection connection, String table) { + this.connection = connection; + this.table = table; + } + + /** + * Add a column, and bound value, to be saved when execute() is called. + * + * @param column + * @param value + * @return the same SaveHelper object + */ + public SaveHelper bind(String column, Object value) { + columns.add(column); + objects.add(value); + return this; + } + + /** + * Build PreparedStatement using bound column-value pairs then execute it. + *

+ * Note that after this call, the SaveHelper's Connection is set to null and so this object is not reusable. + * + * @return the result from {@link PreparedStatement#execute()} + * @throws SQLException + */ + public boolean execute() throws SQLException { + String sql = this.formatInsertWithPlaceholders(); + + PreparedStatement preparedStatement = connection.prepareStatement(sql); + + this.bindValues(preparedStatement); + + try { + return preparedStatement.execute(); + } finally { + this.connection = null; + } + } + + /** + * Format table and column names into an INSERT INTO ... SQL statement. + *

+ * Full form is: + *

+ * INSERT INTO table (column, ...) VALUES (?, ...) ON DUPLICATE KEY UPDATE column=?, ... + *

+ * Note that HSQLDB needs to put into mySQL compatibility mode first via "SET DATABASE SQL SYNTAX MYS TRUE" or "sql.syntax_mys=true" in connection URL. + * + * @return String + */ + private String formatInsertWithPlaceholders() { + String[] placeholders = new String[this.columns.size()]; + Arrays.setAll(placeholders, (int i) -> "?"); + + StringBuilder output = new StringBuilder(); + output.append("INSERT INTO "); + output.append(this.table); + output.append(" ("); + output.append(String.join(", ", this.columns)); + output.append(") VALUES ("); + output.append(String.join(", ", placeholders)); + output.append(") ON DUPLICATE KEY UPDATE "); + output.append(String.join("=?, ", this.columns)); + output.append("=?"); + return output.toString(); + } + + /** + * Binds objects to PreparedStatement based on INSERT INTO ... ON DUPLICATE KEY UPDATE ... + *

+ * Note that each object is bound to two place-holders based on this SQL syntax: + *

+ * INSERT INTO table (column, ...) VALUES (?, ...) ON DUPLICATE KEY UPDATE column=?, ... + *

+ * Requires that mySQL SQL syntax support is enabled during connection. + * + * @param preparedStatement + * @throws SQLException + */ + private void bindValues(PreparedStatement preparedStatement) throws SQLException { + for (int i = 0; i < this.objects.size(); ++i) { + Object object = this.objects.get(i); + + // Special treatment for BigDecimals so that they retain their "scale", + // which would otherwise be assumed as 0. + if (object instanceof BigDecimal) { + preparedStatement.setBigDecimal(i + 1, (BigDecimal) object); + preparedStatement.setBigDecimal(i + this.objects.size() + 1, (BigDecimal) object); + } else { + preparedStatement.setObject(i + 1, object); + preparedStatement.setObject(i + this.objects.size() + 1, object); + } + } + + } + +} diff --git a/src/qora/assets/Asset.java b/src/qora/assets/Asset.java index 52ad82ff..f87d6b9a 100644 --- a/src/qora/assets/Asset.java +++ b/src/qora/assets/Asset.java @@ -1,10 +1,10 @@ package qora.assets; import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.SQLException; import database.DB; +import database.SaveHelper; import qora.account.PublicKeyAccount; public class Asset { @@ -37,11 +37,10 @@ public class Asset { // Load/Save public void save(Connection connection) throws SQLException { - String sql = DB.formatInsertWithPlaceholders("Assets", "asset", "owner", "asset_name", "description", "quantity", "is_divisible", "reference"); - PreparedStatement preparedStatement = connection.prepareStatement(sql); - DB.bindInsertPlaceholders(preparedStatement, this.key, this.owner.getAddress(), this.name, this.description, this.quantity, this.isDivisible, - this.reference); - preparedStatement.execute(); + SaveHelper saveHelper = new SaveHelper(connection, "Assets"); + saveHelper.bind("asset", this.key).bind("owner", this.owner.getAddress()).bind("asset_name", this.name).bind("description", this.description) + .bind("quantity", this.quantity).bind("is_divisible", this.isDivisible).bind("reference", this.reference); + saveHelper.execute(); if (this.key == null) this.key = DB.callIdentity(connection); diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index f002b04a..65e05a8f 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -19,6 +19,7 @@ import com.google.common.primitives.Longs; import database.DB; import database.NoDataFoundException; +import database.SaveHelper; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; import qora.assets.Asset; @@ -290,13 +291,15 @@ public class Block { } protected void save(Connection connection) throws SQLException { - String sql = DB.formatInsertWithPlaceholders("Blocks", "signature", "version", "reference", "transaction_count", "total_fees", "transactions_signature", - "height", "generation", "generating_balance", "generator", "generator_signature", "AT_data", "AT_fees"); - PreparedStatement preparedStatement = connection.prepareStatement(sql); - DB.bindInsertPlaceholders(preparedStatement, this.getSignature(), this.version, this.reference, this.transactionCount, this.totalFees, - this.transactionsSignature, this.height, new Timestamp(this.timestamp), this.generatingBalance, this.generator.getPublicKey(), - this.generatorSignature, this.atBytes, this.atFees); - preparedStatement.execute(); + SaveHelper saveHelper = new SaveHelper(connection, "Blocks"); + + saveHelper.bind("signature", this.getSignature()).bind("version", this.version).bind("reference", this.reference) + .bind("transaction_count", this.transactionCount).bind("total_fees", this.totalFees).bind("transactions_signature", this.transactionsSignature) + .bind("height", this.height).bind("generation", new Timestamp(this.timestamp)).bind("generating_balance", this.generatingBalance) + .bind("generator", this.generator.getPublicKey()).bind("generator_signature", this.generatorSignature).bind("AT_data", this.atBytes) + .bind("AT_fees", this.atFees); + + saveHelper.execute(); } // Navigation diff --git a/src/qora/block/BlockTransaction.java b/src/qora/block/BlockTransaction.java index 4f899f6b..318cd317 100644 --- a/src/qora/block/BlockTransaction.java +++ b/src/qora/block/BlockTransaction.java @@ -11,6 +11,7 @@ import org.json.simple.JSONObject; import database.DB; import database.NoDataFoundException; +import database.SaveHelper; import qora.transaction.Transaction; import qora.transaction.TransactionFactory; @@ -107,10 +108,9 @@ public class BlockTransaction { } protected void save(Connection connection) throws SQLException { - String sql = DB.formatInsertWithPlaceholders("BlockTransactions", "block_signature", "sequence", "transaction_signature"); - PreparedStatement preparedStatement = connection.prepareStatement(sql); - DB.bindInsertPlaceholders(preparedStatement, this.blockSignature, this.sequence, this.transactionSignature); - preparedStatement.execute(); + SaveHelper saveHelper = new SaveHelper(connection, "BlockTransactions"); + saveHelper.bind("block_signature", this.blockSignature).bind("sequence", this.sequence).bind("transaction_signature", this.transactionSignature); + saveHelper.execute(); } // Navigation diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java index b5059195..0c03e058 100644 --- a/src/qora/transaction/GenesisTransaction.java +++ b/src/qora/transaction/GenesisTransaction.java @@ -5,7 +5,6 @@ import java.io.IOException; import java.math.BigDecimal; import java.nio.ByteBuffer; import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; @@ -18,6 +17,7 @@ import com.google.common.primitives.Longs; import database.DB; import database.NoDataFoundException; +import database.SaveHelper; import qora.account.Account; import qora.account.GenesisAccount; import qora.account.PrivateKeyAccount; @@ -103,10 +103,9 @@ public class GenesisTransaction extends Transaction { public void save(Connection connection) throws SQLException { super.save(connection); - String sql = DB.formatInsertWithPlaceholders("GenesisTransactions", "signature", "recipient", "amount"); - PreparedStatement preparedStatement = connection.prepareStatement(sql); - DB.bindInsertPlaceholders(preparedStatement, this.signature, this.recipient.getAddress(), this.amount); - preparedStatement.execute(); + SaveHelper saveHelper = new SaveHelper(connection, "GenesisTransactions"); + saveHelper.bind("signature", this.signature).bind("recipient", this.recipient.getAddress()).bind("amount", this.amount); + saveHelper.execute(); } // Converters diff --git a/src/qora/transaction/PaymentTransaction.java b/src/qora/transaction/PaymentTransaction.java index 80d79297..a38e40ee 100644 --- a/src/qora/transaction/PaymentTransaction.java +++ b/src/qora/transaction/PaymentTransaction.java @@ -5,7 +5,6 @@ import java.io.IOException; import java.math.BigDecimal; import java.nio.ByteBuffer; import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -17,6 +16,7 @@ import com.google.common.primitives.Longs; import database.DB; import database.NoDataFoundException; +import database.SaveHelper; import qora.account.Account; import qora.account.PublicKeyAccount; import utils.Base58; @@ -110,10 +110,9 @@ public class PaymentTransaction extends Transaction { public void save(Connection connection) throws SQLException { super.save(connection); - String sql = DB.formatInsertWithPlaceholders("PaymentTransactions", "signature", "sender", "recipient", "amount"); - PreparedStatement preparedStatement = connection.prepareStatement(sql); - DB.bindInsertPlaceholders(preparedStatement, this.signature, this.sender.getPublicKey(), this.recipient.getAddress(), this.amount); - preparedStatement.execute(); + SaveHelper saveHelper = new SaveHelper(connection, "PaymentTransactions"); + saveHelper.bind("signature", this.signature).bind("sender", this.sender.getPublicKey()).bind("recipient", this.recipient.getAddress()).bind("amount", this.amount); + saveHelper.execute(); } // Converters diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index d0018b3d..02cf337f 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -4,7 +4,6 @@ import java.math.BigDecimal; import java.math.MathContext; import java.nio.ByteBuffer; import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; @@ -17,6 +16,7 @@ import org.json.simple.JSONObject; import database.DB; import database.NoDataFoundException; +import database.SaveHelper; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; import qora.block.Block; @@ -231,11 +231,11 @@ public abstract class Transaction { } protected void save(Connection connection) throws SQLException { - String sql = DB.formatInsertWithPlaceholders("Transactions", "signature", "reference", "type", "creator", "creation", "fee", "milestone_block"); - PreparedStatement preparedStatement = connection.prepareStatement(sql); - DB.bindInsertPlaceholders(preparedStatement, this.signature, this.reference, this.type.value, this.creator.getPublicKey(), - new Timestamp(this.timestamp), this.fee, null); - preparedStatement.execute(); + SaveHelper saveHelper = new SaveHelper(connection, "Transactions"); + saveHelper.bind("signature", this.signature).bind("reference", this.reference).bind("type", this.type.value) + .bind("creator", this.creator.getPublicKey()).bind("creation", new Timestamp(this.timestamp)).bind("fee", this.fee) + .bind("milestone_block", null); + saveHelper.execute(); } // Navigation