[英]Batch INSERT into 2 related tables while avoid SQL Injection
我正在使用 Java 8、JDBC 和 MySql。 我想将大量数据(2,000 行)插入 2 个表中。 这些表具有 1 对 1 的关系。 第一个表是order_items
:
| id | amount |
|:--------|----------------:|
| 1 | 20 |
| 2 | 25 |
| 3 | 30 |
第二个表是delivery_details
:
| orderItemId | message |
|----------------:|:-----------|
| 1 | hello. |
| 2 | salut. |
| 3 | ciao. |
orderItemId
是order_items
的外键。
数据在此 class 中表示:
public class OrderItemDelivery {
@SerializedName("amount")
private BigDecimal amount = null;
@SerializedName("message")
private String message = null;
// getters and setters below
...
...
}
我需要批量执行插入以缩短执行时间。 List<OrderItemDelivery> orderItemDeliveries
包含 2,000 个项目。 我目前的代码是:
Connection connection = this.hikariDataSource.getConnection();
connection.setAutoCommit(false);
Statement statement = connection.createStatement();
for (int x = 0; x < orderItemDeliveries.size(); x++) {
sql = String.format("INSERT INTO order_items (amount) VALUES ('%s')", orderItemDelivery.getAmount());
statement.addBatch(sql);
sql = String.format("INSERT INTO `delivery_details` (`orderItemId`, `message`) VALUES (LAST_INSERT_ID(), '%s')", orderItemDelivery.getMessage());
statement.addBatch(sql);
}
statement.executeBatch();
statement.close();
connection.setAutoCommit(true);
connection.close();
这确实很有效,但这里的限制是它对 SQL 注入开放。 如果我要使用PreparedStatement
,我需要一个用于order_items
批次和一个用于delivery_details
批次。 然后LAST_INSERT_ID()
将不起作用。
有没有办法解决? 就我所见,没有。 而且我需要通过使用 Java 清理message
和amount
来防止 SQL 注入,这似乎有局限性。 例如message
可以包含撇号和表情符号。 谁能想到另一种解决方案?
编辑
这是我提出的一个非常有效的解决方案:
String orderItemSql = "INSERT INTO order_items (amount) VALUES (?) ";
for (int x = 1; x < orderItemDeliveries.size(); x++) {
orderItemSql += ", (?)";
}
PreparedStatement preparedStatement = connection.prepareStatement(orderItemSql, Statement.RETURN_GENERATED_KEYS);
int i = 1;
for (int x = 0; x < orderItemDeliveries.size(); x++) {
preparedStatement.setDouble(i++, orderItemDelivery.getAmount().doubleValue());
}
preparedStatement.executeUpdate();
Long ids[] = new Long[orderItemDeliveries.size()];
ResultSet rs = preparedStatement.getGeneratedKeys();
int x = 0;
while (rs.next()) {
ids[x] = rs.getLong(1);
x++;
}
String deliveryDetails = "INSERT INTO `delivery_details` (`orderItemId`, `message`) VALUES (?, ?)";
for (x = 1; x < orderItemDeliveries.size(); x++) {
deliveryDetails += ", (?)";
}
preparedStatement = connection.prepareStatement(deliveryDetails);
i = 1;
for (x = 0; x < orderItemDeliveries.size(); x++) {
orderItemDelivery = orderItemDeliveries.get(x);
preparedStatement.setLong(i++, ids[x]);
preparedStatement.setString(i++, orderItemDelivery.getMessage());
}
preparedStatement.executeUpdate();
因此,为此, ids
的顺序必须是连续的,并且orderItemDeliveries
的顺序不能在列表的第一个循环和第二个循环之间改变。
这感觉有点hacky,但它有效。 我错过了什么吗?
这是我最终使用getGeneratedKeys()
所做的:
String orderItemSql = "INSERT INTO order_items (amount) VALUES (?) ";
for (int x = 1; x < orderItemDeliveries.size(); x++) {
orderItemSql += ", (?)";
}
PreparedStatement preparedStatement = connection.prepareStatement(orderItemSql, Statement.RETURN_GENERATED_KEYS);
int i = 1;
for (int x = 0; x < orderItemDeliveries.size(); x++) {
preparedStatement.setDouble(i++, orderItemDelivery.getAmount().doubleValue());
}
preparedStatement.executeUpdate();
Long ids[] = new Long[orderItemDeliveries.size()];
ResultSet rs = preparedStatement.getGeneratedKeys();
int x = 0;
while (rs.next()) {
ids[x] = rs.getLong(1);
x++;
}
String deliveryDetails = "INSERT INTO `delivery_details` (`orderItemId`, `message`) VALUES (?, ?)";
for (x = 1; x < orderItemDeliveries.size(); x++) {
deliveryDetails += ", (?)";
}
preparedStatement = connection.prepareStatement(deliveryDetails);
i = 1;
for (x = 0; x < orderItemDeliveries.size(); x++) {
orderItemDelivery = orderItemDeliveries.get(x);
preparedStatement.setLong(i++, ids[x]);
preparedStatement.setString(i++, orderItemDelivery.getMessage());
}
preparedStatement.executeUpdate();
因此,为此,id 的顺序必须是连续的,并且 orderItemDeliveries 的顺序不能在列表的第一个循环和第二个循环之间改变。
这感觉有点hacky,但它有效。
PreparedStatement 甚至有可能吗?
好点,但由于它是 1:1 关系,您可以为每个表使用单独的序列或 AUTO_INCREMENT 键,而不是last_insert_id()
,因为它们为相关记录生成相同的值。 在具有并发事务的 oltp 设置中,我不会那样做,但是因为无论如何您都在进行批处理,所以这可能是合理的。 如果可以的话,您可以通过提前独占锁定两个表来强制独占访问。
让应用程序跟踪键值也是一种选择,而不是使用一个 autoinc 字段。 不幸的是,mysql 不允许 select 直接从序列中获取下一个值,而不是 Oracle。 例如这样:使用带有字段 MAX 的 MAXKEY 表。 假设你要插入 10 行,MAX 为 200。 独占锁定 MAXKEY,select MAX(现在你知道,你的密钥可以从 200 + 1 开始),将 MAXKEY 更新为 200 + 10,提交(释放锁)。 将 201...210 用于 2 组带有准备好的查询的批量插入。
您可以使用存储过程来接受两个表的值并分别插入其中的 bot(请参阅此),再次使用last_insert_id()
并以批处理方式调用该过程(请参阅此)。
最终有 sql 消毒剂,也许在 org.apache.commons.lang.StringEscapeUtils.escapeSlq() 行上的东西可以做。
但是准备好的语句还添加了其他优化。 sql 仅与二维值数组一起发送到服务器一次。 解析后的查询可以被缓存并重用于后续调用。 您应该能够从中看到更多的性能改进。
字符串连接版本为每一行发送整个查询,它们都是不同的,需要解析并且在缓存中找不到。
我建议你试试这个。 即使它不是批处理方法,它也是基于PreparedStatement
的,它总是比内联 SQL 获得更好的性能:
private void insertItems(Connection connection, Collection<OrderItemDelivery> orderItemDeliveries)
throws SQLException
{
try (PreparedStatement pst1=connection.prepareStatement("INSERT INTO order_items (amount) VALUES (?)", new String[] { "id"});
PreparedStatement pst2=connection.prepareStatement("INSERT INTO delivery_details(orderItemId, message) VALUES (?, ?)"))
{
for (OrderItemDelivery orderItemDelivery : orderItemDeliveries)
{
pst1.setString(1, orderItemDelivery.getAmount());
int x=pst1.executeUpdate();
if (x != 1)
{
throw new SQLException("Row was not inserted");
}
try (ResultSet rs=pst1.getGeneratedKeys())
{
if (rs.next())
{
long id=rs.getLong(1);
// TODO Fill the values in 2nd prepared statement and call executeUpdate().
}
else
{
throw new SQLException("Id was not generated");
}
}
}
}
}
注意:您必须先尝试; 并非所有数据库供应商都实现getGeneratedKeys
方法。 如果你没有,只需调用LAST_INSERT_ID
替换生成的密钥片段:它应该工作相同。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.