[Java] Les insertions Batch Insert sont combinées en une seule lorsque vous utilisez MySQL DB dans Doma.

Ce que je veux faire cette fois

Doma et Doma2 sont populaires dans SIer [source], et ce sont des frameworks d'accès à la base de données que vous voyez souvent dans les projets SI.

Cependant, Doma2 a des restrictions, telles que le fait d'être inaccessible à partir de plusieurs threads (une exception est levée lorsque vous le faites). La fonction d'insertion avec plusieurs enregistrements dans la clause values n'est pas fournie en standard ... (@ BatchInsert est une instruction d'insertion Spécifications à lancer plusieurs fois).

Donc, cette fois, je l'ai écrit pour que la fonction d'insertion lorsqu'il y a plusieurs enregistrements dans la clause values puisse être utilisée à partir de Doma.

Postscript

Au fur et à mesure que je l'écrivais, j'ai appris dans les commentaires que si vous définissez rewriteBatchedStatements = true dans le JDBC MySQL, il semble que plusieurs inserts seront combinés en un seul insert, je publierai donc un exemple de réglage.

Exemple de réglage

Dans l'environnement que j'ai essayé cette fois, je devais en mettre deux.

--1. Définissez rewriteBatchedStatements = true dans JDBC --2 Changer BatchSize par défaut

En ce qui concerne le nombre 2, selon cet article, 100 ou moins, c'est bien, alors je l'ai réglé à 100 cette fois. J'ai essayé d'obtenir et d'ajouter 1 million d'éléments via le journal de JDBC \ -Kagamihoge

Méthode de réglage

Comment définir pour chaque méthode Dao

batchSize peut être défini. Il n'est pas nécessaire sauf s'il est défini dans Config décrit plus loin ou que vous souhaitez le définir pour chaque requête.

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

Comment définir avec Config

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);//【ici】
        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;//【ici】
    }

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

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

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

À propos, ↓ semble être le paramètre recommandé de MySQL JDBC par l'auteur HikariCP. MySQL Configuration · brettwooldridge/HikariCP Wiki

Puisqu'un insert est assemblé dans JDBC, il y a plusieurs insertions dans le journal Doma, mais si vous regardez le general_log du côté DB, il est correctement rassemblé dans un insert.

Code que j'ai écrit par moi-même (laissez-le pour le moment)

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();
    /**
     *Exécuter l'insertion avec des données à valeurs multiples.Si le nombre d'entrées est différent de 0, l'instruction d'insertion assemblée est mise en cache..
     *S'il n'y a aucune entrée, l'assembly d'instruction d'insertion et le traitement du cache ne seront pas exécutés..
     *
     * @param entityList Liste des entités à définir dans les valeurs.
     * @param <E>insérer le type de données
     * @return Renvoie la valeur de résultat de executeUpdate.S'il y a 0 entrées, executeUpdate n'est pas exécuté et 0 est renvoyé..
     */
    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 {
    /**
     *Exécuter l'insertion avec des données à valeurs multiples.Si le nombre d'entrées est différent de 0, l'instruction d'insertion assemblée est mise en cache..
     *S'il n'y a aucune entrée, l'assembly d'instruction d'insertion et le traitement du cache ne seront pas exécutés..
     *
     * @Param connectionSupplier Connection acquisition fonction
     * @param entityList Liste des entités à définir dans les valeurs.
     * @param <E>insérer le type de données
     * @return Renvoie la valeur de résultat de executeUpdate.S'il y a 0 entrées, executeUpdate n'est pas exécuté et 0 est renvoyé..
     */
    <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;
    }
}

/**Contenu à mettre en cache*/
@RequiredArgsConstructor
class _SQLWithMethod {
    final CharSequence sql;
    final CharSequence values;
    final List<Method> methodsList;
}

/**Méthodes utilisées dans BulkDao*/
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( //Convertir du nom de la méthode au nom de la colonne
                                            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, depuis que je l'ai écrit dans l'environnement java11, j'écris tout le temps var, donc ça ne fonctionne pas avec Java 8 tel quel ... En outre, il est supposé que vous utilisez lombok. db est une hypothèse de cas de serpent inférieur.

Exemple d'utilisation

@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);

J'ai conçu ce code

Objet de classe mis en cache et instruction Insert. Comme la clé du cache est générique, je dois la prendre à partir de l'objet réel (il peut y avoir d'autres moyens, mais je pense que c'est ennuyeux) ...

Impressions

Par exemple, pour spécifier un index tel que prepareStatement # setObject Le type fonctionnel est incompatible avec le String Buider conçu pour avoir un état. Il existe également des exceptions de contrôle.

Recommended Posts

[Java] Les insertions Batch Insert sont combinées en une seule lorsque vous utilisez MySQL DB dans Doma.
Jusqu'à INSERT S3 objet dans EC2 DB avec Lambda @ java: AWS
Jusqu'à ce que l'objet S3 soit INSERT dans EC2 DB avec Lambda @ java: Java [Suite]
Jusqu'à ce que l'objet S3 soit INSERT dans EC2 DB avec Lambda @ java: Java [Partie 2]
Jusqu'à INSERT S3 objet dans EC2 DB avec Lambda @ java: Java [Partie 1]
twitter4j java Stocke les tweets recherchés dans DB (MySQL).
Exécuter un lot avec docker-compose avec Java batch
[Template] Connexion MySQL avec Java
Essayez la connexion DB avec Java
Connectez-vous à DB avec Java
Connectez-vous à MySQL 8 avec Java
Erreur lors de la lecture avec java