繁体   English   中英

批量 INSERT 到 2 个相关表,同时避免 SQL 注入

[英]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.      |

orderItemIdorder_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 清理messageamount来防止 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM