Java 批插入到2个相关表中,同时避免SQL注入

Java 批插入到2个相关表中,同时避免SQL注入,java,mysql,foreign-keys,bulkinsert,Java,Mysql,Foreign Keys,Bulkinsert,我正在使用Java8、JDBC和MySql。我想在两个表中插入大量数据(2000行)。这些表具有1对1的关系。第一个表是订单项目: | id | amount | |:--------|----------------:| | 1 | 20 | | 2 | 25 | | 3 | 30 | 第二个表是交付详细信息: | orderItemId |

我正在使用Java8、JDBC和MySql。我想在两个表中插入大量数据(2000行)。这些表具有1对1的关系。第一个表是订单项目:

| id      | amount          |
|:--------|----------------:|
| 1       | 20              |
| 2       | 25              |
| 3       | 30              |
第二个表是交付详细信息:

| orderItemId     | message    |
|----------------:|:-----------|
| 1               | hello.     |
| 2               | salut.     |
| 3               | ciao.      |
orderItemId
order\u items
的外键

数据在此类中表示:

public class OrderItemDelivery {

    @SerializedName("amount")
    private BigDecimal amount = null;

    @SerializedName("message")
    private String message = null;


    // getters and setters below
    ...
    ...

}
我需要批量执行插入以缩短执行时间<代码>列表orderItemDeliveries包含2000个项目。我目前的代码是:

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();
因此,要使其起作用,
id
的顺序必须是顺序的,并且
orderItemDeliveries
的顺序不能在列表的第一个循环到第二个循环之间更改

这感觉有点不舒服,但很有效。我遗漏了什么吗?

准备好的声明是否可能

这一点很好,但由于它是1:1关系,您可以为每个表使用单独的序列键或自动增量键,而不是
last\u insert\u id()
,因为它们为相关记录生成相同的值。在具有并发事务的oltp设置中,我不会这样做,但由于您正在批处理,这可能是合理的。如果可以的话,您可以提前强制两个表独占访问

让应用程序跟踪键值也是一个选项,而不是使用一个autoinc字段。不幸的是,与Oracle相反,mysql不允许直接从序列中选择下一个值。例如,这种方法:使用一个带有字段MAX的MAXKEY表。假设您要插入10行,MAX为200。独占锁定MAXKEY,选择MAX(现在您知道,您的密钥可以从200+1开始),将MAXKEY更新为200+10,提交(释放锁)。将201…210用于2组带有准备好的查询的批处理插入

您可以使用存储过程接受两个表的值并分别插入其中的一个(请参阅),再次使用
last\u insert\u 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");
                }
            }
        }
    }
}
private void插入项(连接、集合orderItemDeliveries)
抛出SQLException
{
try(PreparedStatement pst1=connection.prepareStatement(“插入订单项目(金额)值(?)”),新字符串[]{“id”});
PreparedStatement pst2=connection.prepareStatement(“插入交付详细信息(orderItemId,message)值(?,)”)
{
for(OrderItemDelivery OrderItemDelivery:orderItemDeliveries)
{
setString(1,orderItemDelivery.getAmount());
int x=pst1.executeUpdate();
如果(x!=1)
{
抛出新的SQLException(“未插入行”);
}
try(ResultSet rs=pst1.getGeneratedKeys())
{
如果(rs.next())
{
long id=rs.getLong(1);
//TODO填充第二条准备好的语句中的值并调用executeUpdate()。
}
其他的
{
抛出新的SQLException(“未生成Id”);
}
}
}
}
}

注意:您必须先试用;并非所有db供应商都实现了
getGeneratedKeys
方法。如果您没有,只需调用
LAST\u INSERT\u ID
:它应该也能工作。下面是我使用
getGeneratedKeys()
完成的操作:

String orderItemSql=“在订单中插入项目(金额)值(?);
对于(int x=1;x
因此,要使其工作,ID的顺序必须是顺序的,并且orderItemDeliveries的顺序在列表的第一个循环到第二个循环之间不能改变


这感觉有点不舒服,但很有效

我建议您专注于避免SQL注入,因为这是一个安全问题,即使这意味着放弃优化。因此,您应该使用
PreparedStatement
executeUpdate
。甚至可以使用
PreparedStatement
?我需要在
订单项目中执行1次插入,然后在
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");
                }
            }
        }
    }
}
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();