[Java] Batch Insert Inserts werden beim Umgang mit MySQL DB in Doma zu einem zusammengefasst.

Was ich diesmal machen möchte

Doma und Doma2 sind in SIer [Quelle] beliebt und es handelt sich um DB-Zugriffsframeworks, die Sie häufig in SI-Projekten sehen.

Es gibt jedoch Einschränkungen für Doma2, und es kann nicht über mehrere Threads zugegriffen werden (eine Ausnahme wird ausgelöst, wenn dies tatsächlich ausgeführt wird). Die Einfügefunktion mit mehreren Datensätzen in der Werteklausel wird nicht standardmäßig bereitgestellt ... (@ BatchInsert ist eine Einfügeanweisung Spezifikationen, die mehrmals geworfen werden müssen).

Dieses Mal habe ich es so geschrieben, dass die Einfügefunktion von Doma verwendet werden kann, wenn die Werteklausel mehrere Datensätze enthält.

Nachtrag

Während ich es schrieb, erfuhr ich in den Kommentaren, dass, wenn Sie "rewriteBatchedStatements = true" in MySQL JDBC setzen, anscheinend mehrere Einfügungen zu einer Einfügung kombiniert werden, sodass ich ein Einstellungsbeispiel veröffentlichen werde.

Einstellungsbeispiel

In der Umgebung, die ich diesmal ausprobiert habe, musste ich zwei einstellen.

--1. Setzen Sie in JDBC rewriteBatchedStatements = true --2 Ändern Sie "BatchSize" von der Standardeinstellung

Was die Nummer 2 betrifft, so sind laut diesem Artikel 100 oder weniger gut, also habe ich sie diesmal auf 100 gesetzt. Ich habe versucht, 1 Million Artikel über das Tagebuch von JDBC \ -Kagamihoge zu erhalten und hinzuzufügen

Einstellmethode

Festlegen für jede Dao-Methode

batchSize kann eingestellt werden. Dies ist nur erforderlich, wenn es in der später beschriebenen Konfiguration festgelegt ist oder Sie es für jede Abfrage festlegen möchten.

@BatchInsert(batchSize = 100)
int[] batchInsert(List<Main.Entity2> entityList);

So stellen Sie mit Config ein

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.seasar.doma.SingletonConfig;
import org.seasar.doma.jdbc.Config;
import org.seasar.doma.jdbc.Naming;
import org.seasar.doma.jdbc.dialect.Dialect;
import org.seasar.doma.jdbc.dialect.MysqlDialect;
import org.seasar.doma.jdbc.tx.LocalTransactionDataSource;
import org.seasar.doma.jdbc.tx.LocalTransactionManager;
import org.seasar.doma.jdbc.tx.TransactionManager;

import javax.sql.DataSource;

@SingletonConfig
public class DBConfig implements Config {

    private static final DBConfig CONFIG = new DBConfig();

    private final Dialect dialect;

    private final LocalTransactionDataSource dataSource;

    private final TransactionManager transactionManager;

    private DBConfig() {
        dialect = new MysqlDialect();

        var config = new HikariConfig();

        config.setDriverClassName("org.mariadb.jdbc.Driver");
        config.setJdbcUrl("jdbc:mariadb://localhost:3306");
 
        config.addDataSourceProperty("user", "root");
        config.addDataSourceProperty("password", "password");

        config.addDataSourceProperty("cachePrepStmts", true);
        config.addDataSourceProperty("prepStmtCacheSize", 250);
        config.addDataSourceProperty("prepStmtCacheSqlLimit", 2048);
        config.addDataSourceProperty("useServerPrepStmts", true);
        config.addDataSourceProperty("useLocalSessionState", true);
        config.addDataSourceProperty("rewriteBatchedStatements", true);//【Hier】
        config.addDataSourceProperty("cacheResultSetMetadata", true);
        config.addDataSourceProperty("cacheServerConfiguration", true);
        config.addDataSourceProperty("elideSetAutoCommits", true);
        config.addDataSourceProperty("maintainTimeStats", false);
        config.addDataSourceProperty("maintainTimeStats", false);

        var hikariDataSource = new HikariDataSource(config);
        dataSource = new LocalTransactionDataSource(hikariDataSource);

        transactionManager = new LocalTransactionManager(
                dataSource.getLocalTransaction(getJdbcLogger()));
    }

    @Override
    public Dialect getDialect() {
        return dialect;
    }

    @Override
    public Naming getNaming() {
        return Naming.LENIENT_SNAKE_LOWER_CASE;
    }

    @Override
    public int getBatchSize() {
        return 100;//【Hier】
    }

    @Override
    public DataSource getDataSource() {
        return dataSource;
    }

    @Override
    public TransactionManager getTransactionManager() {
        return transactionManager;
    }

    public static DBConfig singleton() {
        return CONFIG;
    }
}

Übrigens scheint ↓ die empfohlene Einstellung von MySQL JDBC durch den HikariCP-Autor zu sein. MySQL Configuration · brettwooldridge/HikariCP Wiki

Da eine Einfügung in JDBC zusammengestellt wird, enthält das Doma-Protokoll mehrere Einfügungen. Wenn Sie sich jedoch das general_log auf der DB-Seite ansehen, wird es ordnungsgemäß in einer Einfügung zusammengestellt.

Code von mir geschrieben (lassen Sie es vorerst)

import org.seasar.doma.Dao;
import org.seasar.doma.jdbc.Config;

import java.sql.SQLException;
import java.util.List;

@Dao
public interface BulkDao {
    DBUtils dbUtils = new DBUtils();
    /**
     *Führen Sie die Einfügung mit Daten mit mehreren Werten aus.Wenn die Anzahl der Eingaben nicht 0 ist, wird die zusammengesetzte Einfügeanweisung zwischengespeichert..
     *Wenn 0 Eingaben vorhanden sind, werden die Assembly zum Einfügen von Anweisungen und die Cache-Verarbeitung nicht ausgeführt..
     *
     * @param entityList Liste der Entitäten, die in Werten festgelegt werden sollen.
     * @param <E>Datentyp einfügen
     * @return Gibt den Ergebniswert von executeUpdate zurück.Wenn 0 Eingaben vorhanden sind, wird executeUpdate nicht ausgeführt und 0 zurückgegeben..
     */
    default <E> int bulkInsertMultiValues(List<E> entityList) {
        return dbUtils.bulkInsertMultiValues(
                () -> {
                    try {
                        return Config.get(this).getDataSource().getConnection();
                    } catch (SQLException e) {
                        throw new RuntimeException(e);
                    }
                },
                entityList);
    }
}
import lombok.RequiredArgsConstructor;
import org.seasar.doma.Table;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class DBUtils {
    /**
     *Führen Sie die Einfügung mit Daten mit mehreren Werten aus.Wenn die Anzahl der Eingaben nicht 0 ist, wird die zusammengesetzte Einfügeanweisung zwischengespeichert..
     *Wenn 0 Eingaben vorhanden sind, wird die Einfügeanweisung nicht zusammengestellt und die Cache-Verarbeitung wird nicht ausgeführt..
     *
     * @param connectionSupplier Verbindungserfassungsfunktion
     * @param entityList Liste der Entitäten, die in Werten festgelegt werden sollen.
     * @param <E>Datentyp einfügen
     * @return Gibt den Ergebniswert von executeUpdate zurück.Wenn 0 Eingaben vorhanden sind, wird executeUpdate nicht ausgeführt und 0 zurückgegeben..
     */
    <E> int bulkInsertMultiValues(
            Supplier<Connection> connectionSupplier, List<E> entityList) {
        if (!entityList.isEmpty()) {
            var clazz = entityList.get(0).getClass();
            var cachedData = _BulkDaoMethod.getSqlWithMethod(clazz);

            var sql = new StringBuilder(cachedData.sql);

            var params = _BulkDaoMethod.getParams(entityList, cachedData, sql);

            return _BulkDaoMethod.executeUpdate(connectionSupplier, sql, params);
        }
        return 0;
    }
}

/**Inhalt, der zwischengespeichert werden soll*/
@RequiredArgsConstructor
class _SQLWithMethod {
    final CharSequence sql;
    final CharSequence values;
    final List<Method> methodsList;
}

/**In BulkDao verwendete Methoden*/
class _BulkDaoMethod {
    static ConcurrentHashMap<Class, _SQLWithMethod> cache = new ConcurrentHashMap<>();

    static _SQLWithMethod getSqlWithMethod(Class<?> clazz) {
        return cache.computeIfAbsent(
                clazz,
                _c -> {
                    Table tableAnnotation = clazz.getAnnotation(Table.class);
                    StringBuilder sql = new StringBuilder();
                    sql.append("insert into ").append(tableAnnotation.name());
                    var methodsList =
                            Arrays.stream(clazz.getDeclaredMethods())
                                    .filter(e -> e.getName().startsWith("get"))
                                    .collect(Collectors.toList());
                    var columuns =
                            methodsList.stream()
                                    .map( //Konvertieren Sie vom Methodennamen in den Spaltennamen
                                            e -> camelToSnake(e.getName().substring(3)))
                                    .collect(Collectors.toList());
                    sql.append(" (").append(String.join(",", columuns)).append(")").append("values ");
                    var values = new StringBuilder("(");
                    IntStream.range(0, columuns.size() - 1).forEach((i) -> values.append("?,"));
                    values.append("?),");
                    return new _SQLWithMethod(sql, values, methodsList);
                });
    }

    static <E> List<Object> getParams(
            List<E> entityList, _SQLWithMethod cachedData, StringBuilder sql) {
        var params = new ArrayList<>();
        entityList.forEach(
                values -> {
                    cachedData.methodsList.forEach(
                            method -> {
                                try {
                                    var p = method.invoke(values);
                                    params.add(p);
                                } catch (IllegalAccessException | InvocationTargetException e) {
                                    throw new RuntimeException(e);
                                }
                            });
                    sql.append(cachedData.values);
                });
        sql.deleteCharAt(sql.length() - 1);
        return params;
    }

    static int executeUpdate(
            Supplier<Connection> connectionSupplier, StringBuilder sql, List<Object> params) {
        Connection conn = connectionSupplier.get();
        try (var preparedStatement = conn.prepareStatement(sql.toString());){
            var i = 0;
            for (var p : params) {
                ++i;
                preparedStatement.setObject(i, p);
            }
            return preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    private static String camelToSnake(String snake) {
        var sb = new StringBuilder(snake.length()<<1);
        var input = snake.toCharArray();
        sb.append(Character.toLowerCase(input[0]));
        for (int i = 1; i < input.length; i++) {
            var c = input[i];
            if (Character.isUpperCase(c)) {
                sb.append('_').append(Character.toLowerCase(c));
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }

}

Ah, da ich es in der Java11-Umgebung geschrieben habe, schreibe ich die ganze Zeit "var", so dass es mit Java 8 nicht funktioniert, wie es ist ... Es wird auch davon ausgegangen, dass Sie Lombok verwenden. db ist eine Kleinschlangenfallannahme.

Anwendungsbeispiel

@org.seasar.doma.Table(name="kintai.work_time")
@org.seasar.doma.Entity
@Data
public static class Entity2{
    int workTimeId;
    String remarks;
}

var list = new ArrayList<ExampleEntity>()
list.add(exampleEntity0);
list.add(exampleEntity1);
BulkDao.bulkInsertMultiValues(list);

Ich habe diesen Code entwickelt

Zwischengespeichertes Klassenobjekt und Insert-Anweisung. Da der Schlüssel des Caches generisch ist, muss ich ihn dem eigentlichen Objekt entnehmen (es kann andere Möglichkeiten geben, aber ich finde es ärgerlich) ...

Impressionen

Zum Beispiel, um einen Index wie "prepareStatement # setObject" anzugeben Der Funktionstyp ist nicht kompatibel mit dem "String Buider", der für einen Status ausgelegt ist. Es gibt auch Prüfausnahmen.

Recommended Posts

[Java] Batch Insert Inserts werden beim Umgang mit MySQL DB in Doma zu einem zusammengefasst.
Bis INSERT S3 Objekt in EC2 DB mit Lambda @ java: AWS
Bis INSERT S3-Objekt in EC2 DB mit Lambda @ java: Java [Fortsetzung]
Bis INSERT S3-Objekt in EC2 DB mit Lambda @ java: Java [Teil 2]
Bis das S3-Objekt mit Lambda @ java in Java eingefügt wird: Java [Teil 1]
twitter4j java Speichern Sie die gesuchten Tweets in DB (MySQL).
Führen Sie Batch mit Docker-Compose mit Java-Batch aus
[Vorlage] MySQL-Verbindung mit Java
Versuchen Sie eine DB-Verbindung mit Java
Stellen Sie mit Java eine Verbindung zur Datenbank her
Stellen Sie mit Java eine Verbindung zu MySQL 8 her
Fehler beim Spielen mit Java