AutoBatchInsertExecutor.java

package com.github.mygreen.sqlmapper.core.query.auto;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.SqlParameterValue;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.jdbc.support.KeyHolder;

import com.github.mygreen.sqlmapper.core.SqlMapperContext;
import com.github.mygreen.sqlmapper.core.annotation.GeneratedValue.GenerationType;
import com.github.mygreen.sqlmapper.core.id.IdGenerationContext;
import com.github.mygreen.sqlmapper.core.id.IdGenerator;
import com.github.mygreen.sqlmapper.core.id.IdentityIdGenerator;
import com.github.mygreen.sqlmapper.core.meta.PropertyMeta;
import com.github.mygreen.sqlmapper.core.meta.PropertyValueInvoker;
import com.github.mygreen.sqlmapper.core.query.JdbcTemplateBuilder;
import com.github.mygreen.sqlmapper.core.type.ValueType;
import com.github.mygreen.sqlmapper.core.util.NumberConvertUtils;
import com.github.mygreen.sqlmapper.core.util.QueryUtils;


/**
 * バッチ挿入を行うSQLを自動生成するクエリを実行します。
 * {@link AutoBatchInsertImpl}のクエリ実行処理の移譲先です。
 *
 * @version 0.3
 * @author T.TSUCHIE
 *
 */
public class AutoBatchInsertExecutor {

    /**
     * バージョンプロパティの初期値
     */
    public static final long INITIAL_VERSION = AutoInsertExecutor.INITIAL_VERSION;

    /**
     * クエリ情報
     */
    private final AutoBatchInsertImpl<?> query;

    /**
     * 設定情報
     */
    private final SqlMapperContext context;

    /**
     * クエリのパラメータ - エンティティごとの設定
     */
    private MapSqlParameterSource[] batchParams;

    /**
     * 挿入するカラム名
     */
    private List<String> usingColumnNames = new ArrayList<>();

    /**
     * IDENTITYによる主キーの自動生成を使用するカラム名
     */
    private List<String> usingIdentityKeyColumnNames = new ArrayList<>();

    /**
     * レコードの挿入操作を行う処理
     */
    private SimpleJdbcInsert insertOperation;

    /**
     * シーケンスやテーブルによる主キーの生成したキー
     * <p>key=カラム名、value=全レコード分の生成したキーの値</p>
     */
    private Map<String, Object[]> generatedKeysMap = new HashMap<String, Object[]>();

    /**
     * 組み立てたクエリ情報を指定するコンストラクタ。
     * @param query クエリ情報
     */
    public AutoBatchInsertExecutor(AutoBatchInsertImpl<?> query) {
        this.query = query;
        this.context = query.getContext();
    }

    /**
     * クエリ実行の準備を行います。
     */
    private void prepare() {

        this.batchParams = new MapSqlParameterSource[query.getEntitySize()];

        prepareSqlParam();
        prepareInsertOperation();
    }

    /**
     * クエリ実行時のパラメータを準備します。
     */
    @SuppressWarnings({"rawtypes", "unchecked"})
    private void prepareSqlParam() {

        final int dataSize = query.getEntitySize();

        for(PropertyMeta propertyMeta : query.getEntityMeta().getAllColumnPropertyMeta()) {

            if(!isTargetProperty(propertyMeta)) {
                continue;
            }

            final String columnName = propertyMeta.getColumnMeta().getName();

            // 各レコードのパラメータを作成する。
            for(int i=0; i < dataSize; i++) {
                final MapSqlParameterSource paramSource = QueryUtils.get(batchParams, i);
                Object propertyValue = PropertyValueInvoker.getEmbeddedPropertyValue(propertyMeta, query.getEntity(i));

                Optional<GenerationType> generationType = propertyMeta.getIdGenerationType();
                if(propertyMeta.isId() && generationType.isPresent()) {
                    if(generationType.get() == GenerationType.IDENTITY) {
                        if(i == 0) {
                            //IDENTITYの場合は、クエリ実行後に取得するため、対象のカラム情報を1回だけ
                            usingIdentityKeyColumnNames.add(columnName);
                        }
                        continue;
                    } else {
                        propertyValue = getNextVal(propertyMeta, i);
                        PropertyValueInvoker.setEmbeddedPropertyValue(propertyMeta, query.getEntity(i), propertyValue);
                    }
                }

                if(propertyValue == null && propertyMeta.isVersion()) {
                    // バージョンキーが設定されていない場合、初期値設定する
                    propertyValue = NumberConvertUtils.convertNumber(propertyMeta.getPropertyType(), INITIAL_VERSION);
                    PropertyValueInvoker.setEmbeddedPropertyValue(propertyMeta, query.getEntity(i), propertyValue);

                }

                // クエリのパラメータの組み立て
                ValueType valueType = propertyMeta.getValueType();
                Object paramValue = valueType.getSqlParameterValue(propertyValue);
                if(paramValue instanceof SqlParameterValue) {
                    // SimpleJdbcInsert を使用する際は、テーブルのコンテキストを見るので、型情報は不要。
                    paramValue = ((SqlParameterValue)paramValue).getValue();
                }
                paramSource.addValue(columnName, paramValue);

            }

            // IDENTITYの主キーでない場合は通常カラムとして追加
            if(!usingIdentityKeyColumnNames.contains(columnName)) {
                usingColumnNames.add(columnName);
            }

        }
    }

    /**
     * 挿入対象のプロパティか判定します。
     * @param propertyMeta プロパティ情報
     * @return 挿入対象のとき、{@literal true} を返します。
     */
    private boolean isTargetProperty(final PropertyMeta propertyMeta) {

        if(propertyMeta.isId()) {
            return true;
        }

        if(propertyMeta.isVersion()) {
            return true;
        }

        if(propertyMeta.isTransient()) {
            return false;
        }

        final String propertyName = propertyMeta.getName();

        if(query.getIncludesProperties().contains(propertyName)) {
            return true;
        }

        if(query.getExcludesProperties().contains(propertyName)) {
            return false;
        }

        // 挿入対象が指定されているときは、その他はすべて抽出対象外とする。
        return query.getIncludesProperties().isEmpty();

    }

    /**
     * 主キーを生成する
     * @param propertyMeta 生成対象のIDプロパティのメタ情報
     * @param columnName 生成対象のカラム名
     * @param index 生成対象のレコードのインデックス
     * @return 生成した主キーの値
     */
    private Object getNextVal(final PropertyMeta propertyMeta, final int index) {

        IdGenerator generator = propertyMeta.getIdGenerator().get();
        String columnName = propertyMeta.getColumnMeta().getName();
        IdGenerationContext generationContext = propertyMeta.getIdGenerationContext().get();

        // 1レコードの主キーをまとめてキーを生成しておき、キャッシュしておく。
        Object[] generatedKeys = generatedKeysMap.computeIfAbsent(columnName, v ->
                context.txRequiresNew().execute(action -> {
                    return generator.generateValues(generationContext, query.getEntities().length);
                }));

         return generatedKeys[index];
    }

    /**
     * SQLの実行をする処理を組み立てます
     */
    private void prepareInsertOperation() {

        this.insertOperation = new SimpleJdbcInsert(getJdbcTemplate())
                .withTableName(query.getEntityMeta().getTableMeta().getFullName())
                .usingColumns(QueryUtils.toArray(usingColumnNames));

        if(!usingIdentityKeyColumnNames.isEmpty()) {
            insertOperation.usingGeneratedKeyColumns(QueryUtils.toArray(usingIdentityKeyColumnNames));
        }
    }

    /**
     * {@link JdbcTemplate}を取得します。
     * @return {@link JdbcTemplate}のインスタンス。
     */
    private JdbcTemplate getJdbcTemplate() {
        return JdbcTemplateBuilder.create(context.getDataSource(), context.getJdbcTemplateProperties())
                .queryTimeout(query.getQueryTimeout())
                .build();
    }

    /**
     * 挿入処理を実行します。
     * @return 挿入したレコード件数を返します。
     * @throws DuplicateKeyException 主キーなどのが一意制約に違反したとき。
     */
    public int[] execute() {

        prepare();

        if(this.usingIdentityKeyColumnNames.isEmpty()) {
            // 主キーがIDENTITYによる生成でない場合
            return insertOperation.executeBatch(batchParams);

        } else {
            // 1件ずつ処理する
            int dataSize = query.getEntities().length;
            int[] res = new int[dataSize];
            for(int i=0; i < dataSize; i++) {

                final KeyHolder keyHolder = insertOperation.executeAndReturnKeyHolder(batchParams[i]);
                // 生成した主キーをエンティティに設定する
                for(Map.Entry<String, Object> entry : keyHolder.getKeys().entrySet()) {

                    if(!usingIdentityKeyColumnNames.contains(entry.getKey())) {
                        continue;
                    }

                    PropertyMeta propertyMeta = query.getEntityMeta().getColumnPropertyMeta(entry.getKey()).orElseThrow();
                    IdentityIdGenerator idGenerator = (IdentityIdGenerator) propertyMeta.getIdGenerator().get();
                    Object propertyValue = idGenerator.generateValue((Number)entry.getValue());
                    PropertyValueInvoker.setEmbeddedPropertyValue(propertyMeta, query.getEntity(i), propertyValue);

                }

                res[i] = 1;

            }

            return res;
        }
    }
}