    001/*
    002 * Copyright (c) 2009 The openGion Project.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *     http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    013 * either express or implied. See the License for the specific language
    014 * governing permissions and limitations under the License.
    015 */
    016package org.opengion.plugin.query;
    017
    018import org.opengion.hayabusa.db.AbstractQuery;
    019import org.opengion.hayabusa.db.DBTableModel;
    020import org.opengion.hayabusa.common.HybsSystemException;
    021import org.opengion.fukurou.util.ErrorMessage;
    022import org.opengion.fukurou.util.StringUtil;
    023// import org.opengion.fukurou.util.HybsDateUtil;               // 5.5.8.5 (2012/11/27)
    024import org.opengion.fukurou.model.Formatter;
    025import org.opengion.fukurou.db.DBUpdater;                               // 6.9.3.0 (2018/03/26)
    026import org.opengion.fukurou.system.Closer;                              // 7.3.0.0 (2021/01/06)
    027
    028import java.sql.Connection;
    029import java.sql.PreparedStatement;
    030// import java.sql.ParameterMetaData;
    031import java.sql.SQLException;
    032
    033/**
    034 * 引数引き当て(PreparedStatement) を利用した登録系Queryです。
    035 *
    036 * java.sql.PreparedStatement を用いて、データベース登録処理を行います。
    037 * 引数の指定方法は、DBTableModele のカラム名に対応する名称を、SQL文の[カラム名]形式で
    038 * 記述します。これを解析して、実際に実行する PreparedStatement に対応する文字列を
    039 * 作成します。
    040 * たとえば、INSERT INTO GEXX (CLM,NAME_JA,LABEL_NAME) VALUES ([CLM],[NAME_JA],[LABEL_NAME] )
    041 * と記述すれば、内部で、DBTableModele のカラム名に対応する値を取り出し、SQL文として、
    042 * INSERT INTO GEXX (CLM,NAME_JA,LABEL_NAME) VALUES (?,?,? ) を実行します。
    043 *
    044 * Query_JDBCTableUpdate との違いは、INSERT文とUPDATE文を渡しておき、
    045 * UPDATEの処理結果が、0件の場合は、INSERTを行います。
    046 * そのため、tableUpdateタグのBODY部に直接SQLを書くのではなく、tableUpdateParam ﾀｸﾞを2個書くことになります。
    047 *
    048 * 基本的に、tableUpdateタグのqueryTypeにJDBCTableUpdateと記述しておき、tableUpdateParam の
    049 * sqlType が MERGE の場合は、2種類のSQL文が作成され、自動的に、JDBCTableMerge が呼ばれます。
    050 * ※ つまり、通常は、queryType="JDBCTableUpdate" のままで、sqlType="MERGE" を指定すればよい。
    051 *
    052 * @og.formSample
    053 * ●使用例
    054 *
    055 *    ・JDBCTableUpdate のまま、sqlType="MERGE" を指定する場合。
    056 *    【entry.jsp】
    057 *        &lt;og:tableUpdate
    058 *            command   = "{&#064;command}"
    059 *            queryType = "JDBCTableUpdate"
    060 *            &lt;og:tableUpdateParam
    061 *                sqlType     = "MERGE"                // INSERT or UPDATE
    062 *                table       = "{&#064;TABLE_NAME}"    // 処理対象のテーブル名
    063 *                names       = "{&#064;names}"         // 処理対象のカラム名
    064 *                omitNames   = "{&#064;omitNames}"     // 処理対象外のカラム名
    065 *                where       = "{&#064;where}"         // 処理対象を特定するキー（INSERT時には使われず、UPDAET時に使われる。）
    066 *                constKeys   = "{&#064;constKeys}"     // 処理カラム名の中の固定情報カラム名
    067 *                constVals   = "{&#064;constVals}"     // 処理カラム名の中の固定情報設定値
    068 *            /&gt;
    069 *        &lt;/og:tableUpdate&gt;
    070 *
    071 *    ・JDBCTableMerge を直接的に指定する場合。
    072 *    【entry.jsp】
    073 *        &lt;og:tableUpdate
    074 *            command   = "{&#064;command}"
    075 *            queryType = "JDBCTableMerge"
    076 *            &lt;og:tableUpdateParam
    077 *                sqlType     = "INSERT"                // INSERT or UPDATE
    078 *                table       = "{&#064;TABLE_NAME}"    // 処理対象のテーブル名
    079 *                names       = "{&#064;names}"         // 処理対象のカラム名
    080 *                omitNames   = "{&#064;omitNames}"     // 処理対象外のカラム名
    081 *                constKeys   = "{&#064;constKeys}"     // 処理カラム名の中の固定情報カラム名
    082 *                constVals   = "{&#064;constVals}"     // 処理カラム名の中の固定情報設定値
    083 *            /&gt;
    084 *            &lt;og:tableUpdateParam
    085 *                sqlType     = "UPDATE"                // INSERT or UPDATE
    086 *                table       = "{&#064;TABLE_NAME}"    // 処理対象のテーブル名
    087 *                names       = "{&#064;names}"         // 処理対象のカラム名
    088 *                omitNames   = "{&#064;omitNames}"     // 処理対象外のカラム名
    089 *                where       = "{&#064;where}"         // 処理対象を特定するキー
    090 *                constKeys   = "{&#064;constKeys}"     // 処理カラム名の中の固定情報カラム名
    091 *                constVals   = "{&#064;constVals}"     // 処理カラム名の中の固定情報設定値
    092 *            /&gt;
    093 *        &lt;/og:tableUpdate&gt;
    094 *
    095 * @og.rev 7.2.9.1 (2020/10/23) 新規作成
    096 * @og.group データ編集
    097 *
    098 * @version  7.2
    099 * @author   Kazuhiko Hasegawa
    100 * @since    JDK11.0,
    101 */
    102public class Query_JDBCTableMerge extends AbstractQuery {
    103        /** このプログラムのVERSION文字列を設定します。   {@value} */
    104        private static final String VERSION = "7.4.1.0 (2021/04/23)" ;
    105
    106        /**
    107         * デフォルトコンストラクター
    108         *
    109         * @og.rev 7.2.9.1 (2020/10/23) 新規作成
    110         */
    111        public Query_JDBCTableMerge() { super(); }              // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。
    112
    113        /**
    114         * 引数配列付のクエリーを実行します。
    115         * 処理自体は, #execute() と同様に、各サブクラスの実装に依存します。
    116         * これは、PreparedQuery で使用する引数を配列でセットするものです。
    117         * select * from emp where deptno = ? and job = ? などの PreparedQuery の
    118         * [カラム名] 部分の引数を、DBTableModelから順番にセットしていきます。
    119         *
    120         * @og.rev 7.2.9.1 (2020/10/23) TableUpdateParamTag のマージ(UPDATE,INSERT)対応
    121         * @og.rev 7.3.0.0 (2021/01/06) SpotBugs:チェック例外でストリームやリソースのクリーンアップに失敗するかもしれないメソッド
    122         * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
    123         *
    124         * @param   rowNo 選択された行番号配列(登録する対象行)
    125         * @param   table DBTableModelオブジェクト(登録する元データ)
    126         */
    127        @Override
    128        public void execute( final int[] rowNo, final DBTableModel table ) {
    129
    130                int row = 0;                    // エラー時に表示するエラー行番号
    131                int executeCount = 0;   // 処理件数
    132
    133                // 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
    134//              final String[] sqls = getMergeStatement();              // UPDATE,INSERTの順番
    135                final String[] sqls = getMergeStatement();              // UPDATE,INSERT,SELECTの順番(UPDATE か SELECT はnull の可能性がある)
    136
    137                // 6.9.8.0 (2018/05/28) エラー時に、わかりやすいように、引数を、パラメータ順にします。
    138                String[] vals    = null;
    139                boolean isUpdate = false;               // update/insert の区別が分かるように
    140                final Connection conn = getConnection();
    141
    142                // 7.3.0.0 (2021/01/06) SpotBugs:チェック例外でストリームやリソースのクリーンアップに失敗するかもしれないメソッド
    143                // 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
    144                // ※ ﾄﾘｯｷｰ：updQuery は、selectの場合もあり得る。
    145//              final DBquery updQuery = new DBquery( table,sqls[0] );
    146                final DBquery updQuery = new DBquery( table,sqls[0] == null ? sqls[2] : sqls[0] );
    147                final DBquery insQuery = new DBquery( table,sqls[1] );
    148
    149                // 7.3.0.0 (2021/01/06) SpotBugs:チェック例外でストリームやリソースのクリーンアップに失敗するかもしれないメソッド
    150                try {
    151                        final boolean usePMeta = useParameterMetaData();
    152
    153                        final DBUpdater updDB = updQuery.getDBUpdater( conn,usePMeta );
    154                        final DBUpdater insDB = insQuery.getDBUpdater( conn,usePMeta );
    155
    156                        // 5.5.5.4 (2012/08/18) Timestamp オブジェクトを登録する場合
    157                        for( int i=0; i<rowNo.length; i++ ) {
    158                                row = rowNo[i];
    159                                vals = updQuery.getVals( row );
    160
    161                                isUpdate = true;
    162                                int cnt = updDB.update( vals ) ;
    163                                if( cnt == 0 ) {                                                // UPDATE を実行して、結果が 0 件なら
    164                                        vals = insQuery.getVals( row );
    165                                        isUpdate = false;
    166                                        cnt = insDB.update( vals );                     // INSERT 処理を行う。
    167                                }
    168                                executeCount += cnt ;
    169                        }
    170
    171                        setExecuteCount( executeCount );
    172                        setErrorCode( ErrorMessage.OK );
    173                }
    174                catch( final SQLException ex) {         // catch は、close() されてから呼ばれます。
    175                        setErrorCode( ErrorMessage.EXCEPTION );
    176                        final StringBuilder errMsg = new StringBuilder( BUFFER_MIDDLE )
    177                                .append( ex.getMessage() ).append( ':' ).append( ex.getSQLState() ).append( CR )
    178                                .append( isUpdate ? "UPDATE" : "INSERT" ).append( CR )
    179                                .append( "  UPDATE=" ).append( sqls[0] ).append( CR )
    180                                .append( "  INSERT=" ).append( sqls[1] ).append( CR )
    181                                .append( "  ROW =["  ).append( (row+1) ).append( ']' ).append( CR )
    182                                .append( "  VALS=["  ).append( StringUtil.array2csv( vals )).append( ']' )      // 6.9.8.0 (2018/05/28)
    183                                .append( CR ) ;
    184
    185                        throw new HybsSystemException( errMsg.toString(),ex );          // 3.5.5.4 (2004/04/15) 引数の並び順変更
    186                }
    187                // 7.3.0.0 (2021/01/06) SpotBugs:チェック例外でストリームやリソースのクリーンアップに失敗するかもしれないメソッド
    188                finally {
    189                        updQuery.close();
    190                        insQuery.close();
    191                }
    192        }
    193
    194        /**
    195         * DBUpdater で処理するにあたり、
    196         * 処理自体は, #execute() と同様に、各サブクラスの実装に依存します。
    197         * これは、PreparedQuery で使用する引数を配列でセットするものです。
    198         * select * from emp where deptno = ? and job = ? などの PreparedQuery の
    199         * [カラム名] 部分の引数を、DBTableModelから順番にセットしていきます。
    200         *
    201         * @og.rev 7.2.9.1 (2020/10/23) TableUpdateParamTag のマージ(UPDATE,INSERT)対応
    202         * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
    203         */
    204        private static final class DBquery {
    205                private final DBTableModel      table;
    206                private final int[]                     clmNos;
    207                private final String            query ;
    208                private final int                       cnt   ;
    209                private final boolean[]         isTime;
    210                private final boolean           useTime;
    211                private final boolean           useSelect;              // 7.4.1.0 (2021/04/23)
    212
    213                private PreparedStatement       pstmt;                  // close用に持っておく
    214
    215                /**
    216                 * DBTableModelとQUERY文を指定した、コンストラクター
    217                 *
    218                 * @og.rev 7.2.9.1 (2020/10/23) TableUpdateParamTag のマージ(UPDATE,INSERT)対応
    219                 * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
    220                 *
    221                 * @param   table DBTableModelオブジェクト(登録する元データ)
    222                 * @param   sql 実行するQUERY文
    223                 */
    224                public DBquery( final DBTableModel table , final String sql ) {
    225                        useSelect =sql.startsWith( "SELECT" );          // 7.4.1.0 (2021/04/23)
    226
    227                        this.table = table;
    228                        final Formatter form = new Formatter( table,sql );
    229                        clmNos = form.getClmNos();                                      // 引数の個数分の配列。カラム番号を保存
    230                        query  = form.getQueryFormatString();
    231                        cnt    = clmNos.length;                                         // 引数の個数(カラムの個数ではありません。)
    232
    233                        isTime = new boolean[cnt];
    234
    235                        boolean useTimeStamp = false;
    236                        for( int j=0; j<cnt; j++ ) {
    237                                isTime[j] = table.getDBColumn( clmNos[j] ).isDateType();        // 6.4.6.0 (2016/05/27)
    238                                if( !useTimeStamp && isTime[j] ) { useTimeStamp = true; }       // isTime[j] == true 時に、一度だけ実行される。
    239                        }
    240                        useTime = useTimeStamp;
    241                }
    242
    243                /**
    244                 * Connection から、DBUpdater を作成して返します。
    245                 *
    246                 * @og.rev 7.3.0.0 (2021/01/06) PreparedStatement 関連の処理の見直し
    247                 * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
    248                 *
    249                 * @param   conn Connectionオブジェクト
    250                 * @param   usePMeta Meta情報を使用する場合は、true
    251                 * @return  DBUpdaterｵﾌﾞｼﾞｪｸﾄ
    252                 */
    253                public DBUpdater getDBUpdater( final Connection conn,final boolean usePMeta ) throws SQLException {
    254                        pstmt = conn.prepareStatement( query );
    255                        pstmt.setQueryTimeout( DB_MAX_QUERY_TIMEOUT );
    256
    257//                      return new DBUpdater( cnt,pstmt,usePMeta,useTime ? isTime : null );
    258                        return new DBUpdater( cnt,pstmt,usePMeta,useTime ? isTime : null,useSelect );   // 7.4.1.0 (2021/04/23)
    259                }
    260
    261                /**
    262                 * パラメータの個数を返します。
    263                 *
    264                 * @og.rev 7.3.0.0 (2021/01/06) PreparedStatement 関連の処理の見直し
    265                 *
    266                 * @return  パラメータの個数
    267                 */
    268                public void close() {
    269                        Closer.stmtClose( pstmt );
    270                }
    271
    272                /**
    273                 * 指定行のデータのうち、パラメータカラムの値の配列を返します。
    274                 *
    275                 * これは、[カラム]を? に置き換えた処理の、? の順番にDBTableModelの値を再設定します。
    276                 * 取り出した行データは、rTrim して、右側の空白文字を削除しています。
    277                 *
    278                 * @og.rev 7.2.9.1 (2020/10/23) TableUpdateParamTag のマージ(UPDATE,INSERT)対応
    279                 *
    280                 * @param   row 指定行
    281                 * @return  指定行のデータ配列
    282                 */
    283                public String[] getVals( final int row ) {
    284                        final String[] vals = new String[cnt];
    285                        final String[] data = table.getValues( row );
    286                        for( int j=0; j<cnt; j++ ) {
    287                                vals[j] = StringUtil.rTrim( data[ clmNos[j] ] );
    288                        }
    289                        return vals ;
    290                }
    291        }
    292}