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 java.sql.Connection;
019import java.sql.PreparedStatement;
020// import java.sql.ParameterMetaData;
021import java.sql.SQLException;
022
023import org.opengion.hayabusa.db.AbstractQuery;
024import org.opengion.hayabusa.db.DBTableModel;
025import org.opengion.hayabusa.common.HybsSystemException;
026import org.opengion.fukurou.util.ErrorMessage;
027import org.opengion.fukurou.util.StringUtil;
028// import org.opengion.fukurou.util.HybsDateUtil;               // 5.5.8.5 (2012/11/27)
029import org.opengion.fukurou.model.Formatter;
030import org.opengion.fukurou.db.DBUpdater;                               // 6.9.3.0 (2018/03/26)
031import org.opengion.fukurou.system.Closer;                              // 7.3.0.0 (2021/01/06)
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 *        <og:tableUpdate
058 *            command   = "{@command}"
059 *            queryType = "JDBCTableUpdate"
060 *            <og:tableUpdateParam
061 *                sqlType     = "MERGE"                // INSERT or UPDATE
062 *                table       = "{@TABLE_NAME}"    // 処理対象のテーブル名
063 *                names       = "{@names}"         // 処理対象のカラム名
064 *                omitNames   = "{@omitNames}"     // 処理対象外のカラム名
065 *                where       = "{@where}"         // 処理対象を特定するキー(INSERT時には使われず、UPDAET時に使われる。)
066 *                constKeys   = "{@constKeys}"     // 処理カラム名の中の固定情報カラム名
067 *                constVals   = "{@constVals}"     // 処理カラム名の中の固定情報設定値
068 *            />
069 *        </og:tableUpdate>
070 *
071 *    ・JDBCTableMerge を直接的に指定する場合。
072 *    【entry.jsp】
073 *        <og:tableUpdate
074 *            command   = "{@command}"
075 *            queryType = "JDBCTableMerge"
076 *            <og:tableUpdateParam
077 *                sqlType     = "INSERT"                // INSERT or UPDATE
078 *                table       = "{@TABLE_NAME}"    // 処理対象のテーブル名
079 *                names       = "{@names}"         // 処理対象のカラム名
080 *                omitNames   = "{@omitNames}"     // 処理対象外のカラム名
081 *                constKeys   = "{@constKeys}"     // 処理カラム名の中の固定情報カラム名
082 *                constVals   = "{@constVals}"     // 処理カラム名の中の固定情報設定値
083 *            />
084 *            <og:tableUpdateParam
085 *                sqlType     = "UPDATE"                // INSERT or UPDATE
086 *                table       = "{@TABLE_NAME}"    // 処理対象のテーブル名
087 *                names       = "{@names}"         // 処理対象のカラム名
088 *                omitNames   = "{@omitNames}"     // 処理対象外のカラム名
089 *                where       = "{@where}"         // 処理対象を特定するキー
090 *                constKeys   = "{@constKeys}"     // 処理カラム名の中の固定情報カラム名
091 *                constVals   = "{@constVals}"     // 処理カラム名の中の固定情報設定値
092 *            />
093 *        </og:tableUpdate>
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                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 ForLoopCanBeForeach
158//                      for( int i=0; i<rowNo.length; i++ ) {
159//                              row = rowNo[i];
160                        for( final int tmp : rowNo ) {
161                                row = tmp;
162                                vals = updQuery.getVals( row );
163
164                                isUpdate = true;
165                                int cnt = updDB.update( vals ) ;
166                                if( cnt == 0 ) {                                                // UPDATE を実行して、結果が 0 件なら
167                                        vals = insQuery.getVals( row );
168                                        isUpdate = false;
169                                        cnt = insDB.update( vals );                     // INSERT 処理を行う。
170                                }
171                                executeCount += cnt ;
172                        }
173
174                        setExecuteCount( executeCount );
175                        setErrorCode( ErrorMessage.OK );
176                }
177                catch( final SQLException ex) {         // catch は、close() されてから呼ばれます。
178                        setErrorCode( ErrorMessage.EXCEPTION );
179                        final StringBuilder errMsg = new StringBuilder( BUFFER_MIDDLE )
180                                .append( ex.getMessage() ).append( ':' ).append( ex.getSQLState() ).append( CR )
181                                .append( isUpdate ? "UPDATE" : "INSERT" ).append( CR )
182                                .append( "  UPDATE=" ).append( sqls[0] ).append( CR )
183                                .append( "  INSERT=" ).append( sqls[1] ).append( CR )
184                                .append( "  ROW =["  ).append( row+1 ).append( ']' ).append( CR )
185                                .append( "  VALS=["  ).append( StringUtil.array2csv( vals )).append( ']' )      // 6.9.8.0 (2018/05/28)
186                                .append( CR ) ;
187
188                        throw new HybsSystemException( errMsg.toString(),ex );          // 3.5.5.4 (2004/04/15) 引数の並び順変更
189                }
190                // 7.3.0.0 (2021/01/06) SpotBugs:チェック例外でストリームやリソースのクリーンアップに失敗するかもしれないメソッド
191                finally {
192                        updQuery.close();
193                        insQuery.close();
194                }
195        }
196
197        /**
198         * DBUpdater で処理するにあたり、
199         * 処理自体は、#execute() と同様に、各サブクラスの実装に依存します。
200         * これは、PreparedQuery で使用する引数を配列でセットするものです。
201         * select * from emp where deptno = ? and job = ? などの PreparedQuery の
202         * [カラム名] 部分の引数を、DBTableModelから順番にセットしていきます。
203         *
204         * @og.rev 7.2.9.1 (2020/10/23) TableUpdateParamTag のマージ(UPDATE,INSERT)対応
205         * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
206         */
207        private static final class DBquery {
208                private final DBTableModel      table;
209                private final int[]                     clmNos;
210                private final String            query ;
211                private final int                       cnt   ;
212                // 8.5.4.2 (2024/01/12) PMD 7.0.0 LinguisticNaming
213//              private final boolean[]         isTime;
214                private final boolean[]         timeClms;
215                private final boolean           useTime;
216                private final boolean           useSelect;              // 7.4.1.0 (2021/04/23)
217
218                private PreparedStatement       pstmt;                  // close用に持っておく
219
220                /**
221                 * DBTableModelとQUERY文を指定した、コンストラクター
222                 *
223                 * @og.rev 7.2.9.1 (2020/10/23) TableUpdateParamTag のマージ(UPDATE,INSERT)対応
224                 * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
225                 *
226                 * @param   table DBTableModelオブジェクト(登録する元データ)
227                 * @param   sql 実行するQUERY文
228                 */
229                public DBquery( final DBTableModel table , final String sql ) {
230                        useSelect =sql.startsWith( "SELECT" );          // 7.4.1.0 (2021/04/23)
231
232                        this.table = table;
233                        final Formatter form = new Formatter( table,sql );
234                        clmNos = form.getClmNos();                                      // 引数の個数分の配列。カラム番号を保存
235                        query  = form.getQueryFormatString();
236                        cnt    = clmNos.length;                                         // 引数の個数(カラムの個数ではありません。)
237
238                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 LinguisticNaming
239//                      isTime = new boolean[cnt];
240                        timeClms = new boolean[cnt];
241
242                        boolean useTimeStamp = false;
243                        for( int j=0; j<cnt; j++ ) {
244                                // 8.5.4.2 (2024/01/12) PMD 7.0.0 LinguisticNaming
245//                              isTime[j] = table.getDBColumn( clmNos[j] ).isDateType();        // 6.4.6.0 (2016/05/27)
246//                              if( !useTimeStamp && isTime[j] ) { useTimeStamp = true; }       // isTime[j] == true 時に、一度だけ実行される。
247                                timeClms[j] = table.getDBColumn( clmNos[j] ).isDateType();      // 6.4.6.0 (2016/05/27)
248                                if( !useTimeStamp && timeClms[j] ) { useTimeStamp = true; }     // timeClms[j] == true 時に、一度だけ実行される。
249                        }
250                        useTime = useTimeStamp;
251                }
252
253                /**
254                 * Connection から、DBUpdater を作成して返します。
255                 *
256                 * @og.rev 7.3.0.0 (2021/01/06) PreparedStatement 関連の処理の見直し
257                 * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
258                 *
259                 * @param   conn Connectionオブジェクト
260                 * @param   usePMeta Meta情報を使用する場合は、true
261                 * @return  DBUpdaterオブジェクト
262                 * @throws  SQLException データベース実行エラー
263                 */
264                public DBUpdater getDBUpdater( final Connection conn,final boolean usePMeta ) throws SQLException {
265                        pstmt = conn.prepareStatement( query );
266                        pstmt.setQueryTimeout( DB_MAX_QUERY_TIMEOUT );
267
268//                      return new DBUpdater( cnt,pstmt,usePMeta,useTime ? isTime : null );
269                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 LinguisticNaming
270//                      return new DBUpdater( cnt,pstmt,usePMeta,useTime ? isTime : null,useSelect );   // 7.4.1.0 (2021/04/23)
271                        return new DBUpdater( cnt,pstmt,usePMeta,useTime ? timeClms : null,useSelect ); // 7.4.1.0 (2021/04/23)
272                }
273
274                /**
275                 * パラメータの個数を返します。
276                 *
277                 * @og.rev 7.3.0.0 (2021/01/06) PreparedStatement 関連の処理の見直し
278                 */
279                public void close() {
280                        Closer.stmtClose( pstmt );
281                }
282
283                /**
284                 * 指定行のデータのうち、パラメータカラムの値の配列を返します。
285                 *
286                 * これは、[カラム]を? に置き換えた処理の、? の順番にDBTableModelの値を再設定します。
287                 * 取り出した行データは、rTrim して、右側の空白文字を削除しています。
288                 *
289                 * @og.rev 7.2.9.1 (2020/10/23) TableUpdateParamTag のマージ(UPDATE,INSERT)対応
290                 *
291                 * @param   row 指定行
292                 * @return  指定行のデータ配列
293                 */
294                public String[] getVals( final int row ) {
295                        final String[] vals = new String[cnt];
296                        final String[] data = table.getValues( row );
297                        for( int j=0; j<cnt; j++ ) {
298                                vals[j] = StringUtil.rTrim( data[ clmNos[j] ] );
299                        }
300                        return vals ;
301                }
302        }
303}