Пакетная вставка в 2 связанных таблицы, избегая SQL инъекций - PullRequest
1 голос
/ 04 апреля 2020

Я использую Java 8, JDB C и MySql. Я хочу вставить большой объем данных (2000 строк) в 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.

Данные представлены в этом классе:

public class OrderItemDelivery {

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

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


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

}

Мне нужно выполнить вставки как пакет, чтобы сократить время выполнения. List<OrderItemDelivery> 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();

Это действительно эффективно, но здесь есть ограничение: SQL Инъекция. Если бы я использовал PreparedStatement, мне бы понадобился один для партии order_items и один для партии delivery_details. И тогда LAST_INSERT_ID() не будет работать.

Есть ли способ обойти это? Из того, что я видел, нет. И мне нужно предотвратить инъекцию SQL путем очистки message и amount с помощью Java, что, по-видимому, имеет ограничения. Например, 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 не должен изменяться между первым l oop в списке и вторым.

Это немного странно, но работает. Я что-то упустил?

Ответы [ 3 ]

1 голос
/ 07 апреля 2020

Вот что я в итоге сделал, используя 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();

Так, чтобы это работало, порядок идентификаторов должен быть последовательным, а порядок orderItemDeliveries не должен изменяться между первым l oop через список и второй.

Это кажется немного хакерским, но это работает.

0 голосов
/ 04 апреля 2020

Я предлагаю вам попробовать это. Даже если это не пакетный подход, он основан на 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: он должен работать так же.

0 голосов
/ 04 апреля 2020

Возможно ли это даже с PreparedStatement?

Хороший вопрос, но поскольку это отношение 1: 1, вы можете использовать отдельную последовательность или ключи AUTO_INCREMENT для каждой таблицы, а не last_insert_id(), учитывая, что они генерируют одинаковые значения для коррелированных записей. В настройке oltp с параллельными транзакциями я бы этого не сделал, но, поскольку вы все равно выполняете пакетирование, это может быть разумным. Вы можете принудительно установить эксклюзивный доступ, заблокировав обе таблицы исключительно заранее, если это возможно.

Позволить приложению отслеживать значения ключей также является опцией вместо использования одного поля autoin c. , К сожалению, mysql не позволяет напрямую выбирать следующее значение из последовательности, в отличие от Oracle. Например, используйте таблицу MAXKEY с полем MAX. Скажем, вы хотите вставить 10 строк, MAX на 200. Блокируйте исключительно MAXKEY, выберите MAX (теперь вы знаете, ваши ключи могут начинаться с 200 + 1), обновите MAXKEY до 200 + 10, подтвердите (сняв блокировку). используйте 201 ... 210 для 2 наборов пакетных вставок с подготовленными запросами.

Вы можете использовать хранимую процедуру, чтобы принимать значения для обеих таблиц и вставлять их отдельно в бот (см. this ), снова используя last_insert_id(), и вызывать процедуру в пакетном режиме (см. это ).

В конце концов есть sql дезинфицирующие средства, возможно, что-то на линии org. apache .commons.lang.StringEscapeUtils.escapeSlq () может сделать.

Но подготовленные операторы также добавляют другие оптимизаций. sql отправляется на сервер только один раз вместе с двумерным массивом значений. Проанализированный запрос может быть кэширован и использован повторно для последующих вызовов. Благодаря этому вы сможете увидеть еще большее улучшение производительности.

Версия конкатенации строк отправляет весь запрос для каждой строки, все они разные, их необходимо проанализировать и не найти в кэш.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...