AutoInsertExecutor.java

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

import java.util.ArrayList;
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 AutoInsertImpl}のクエリ実行処理の移譲先です。
 *
 * @version 0.3
 * @author T.TSUCHIE
 *
 */
public class AutoInsertExecutor {

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

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

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

    /**
     * クエリのパラメータです。
     */
    private final MapSqlParameterSource paramSource = new MapSqlParameterSource();

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

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

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

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

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

        prepareSqlParam();
        prepareInsertOperation();
    }

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

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

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

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

            // パラメータの組み立て
            Object propertyValue = PropertyValueInvoker.getEmbeddedPropertyValue(propertyMeta, query.getEntity());

            Optional<GenerationType> generationType = propertyMeta.getIdGenerationType();
            if(propertyMeta.isId() && generationType.isPresent()) {
                if(generationType.get() == GenerationType.IDENTITY) {
                    //IDENTITYの場合は、クエリ実行後に取得するため、対象のカラム情報を一時保存しておく。
                    usingIdentityKeyColumnNames.add(columnName);
                    continue;
                } else {
                    propertyValue = getNextVal(propertyMeta);
                    PropertyValueInvoker.setEmbeddedPropertyValue(propertyMeta, query.getEntity(), propertyValue);
                }
            }

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

            }

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

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

        }
    }


    /**
     * 挿入対象のプロパティか判定します。
     * @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プロパティのメタ情報
     * @return 生成した主キーの値
     */
    private Object getNextVal(final PropertyMeta propertyMeta) {

        IdGenerator generator = propertyMeta.getIdGenerator().get();
        IdGenerationContext generationContext = propertyMeta.getIdGenerationContext().get();

        // トランザクションは別にする。
        return context.txRequiresNew().execute(action -> {
            return generator.generateValue(generationContext);
        });
    }

    /**
     * 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.execute(paramSource);

        } else {
            final KeyHolder keyHolder = insertOperation.executeAndReturnKeyHolder(paramSource);

            // 生成した主キーをエンティティに設定する
            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(), propertyValue);

            }

            return 1;
        }

    }
}