Better code for saving to database, using close-coupled column-value pairs

This commit is contained in:
catbref 2018-05-25 15:23:10 +01:00
parent 63be6b7e90
commit 216ed7c772
8 changed files with 165 additions and 93 deletions

View File

@ -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.
* <p>
* Full form is:
* <p>
* INSERT INTO <I>table</I> (<I>column</I>, ...) VALUES (?, ...) ON DUPLICATE KEY UPDATE <I>column</I>=?, ...
* <p>
* 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 ...
* <p>
* Note that each object is bound to <b>two</b> place-holders based on this SQL syntax:
* <p>
* INSERT INTO <I>table</I> (<I>column</I>, ...) VALUES (<b>?</b>, ...) ON DUPLICATE KEY UPDATE <I>column</I>=<b>?</b>, ...
* <p>
* 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.
* <p>

View File

@ -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.
* <p>
* Columns, and corresponding values, are bound via close-coupled pairs in a chain thus:
* <p>
* {@code SaveHelper helper = new SaveHelper(connection, "TableName"); }<br>
* {@code helper.bind("column_name", someColumnValue).bind("column2", columnValue2); }<br>
* {@code helper.execute(); }<br>
*
*/
public class SaveHelper {
private Connection connection;
private String table;
private List<String> columns = new ArrayList<String>();
private List<Object> objects = new ArrayList<Object>();
/**
* 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.
* <p>
* 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.
* <p>
* Full form is:
* <p>
* INSERT INTO <I>table</I> (<I>column</I>, ...) VALUES (?, ...) ON DUPLICATE KEY UPDATE <I>column</I>=?, ...
* <p>
* 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 ...
* <p>
* Note that each object is bound to <b>two</b> place-holders based on this SQL syntax:
* <p>
* INSERT INTO <I>table</I> (<I>column</I>, ...) VALUES (<b>?</b>, ...) ON DUPLICATE KEY UPDATE <I>column</I>=<b>?</b>, ...
* <p>
* 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);
}
}
}
}

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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