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 *        <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                        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}