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.fukurou.db; 017 018import java.util.List; 019import java.util.ArrayList; 020import java.util.Locale ; 021import java.util.Arrays ; 022import java.util.Set ; 023import java.util.HashSet ; 024import java.util.LinkedHashSet ; 025import java.util.StringJoiner ; 026 027import org.opengion.fukurou.util.StringUtil; 028import org.opengion.fukurou.system.OgBuilder ; 029import org.opengion.fukurou.system.OgRuntimeException ; 030import static org.opengion.fukurou.system.HybsConst.CR; 031import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE; 032 033/** 034 * QueryMaker は、カラム名などから、SELECT,INSERT,UPDATE,DALETE 文字列を作成するクラスです。 035 * 036 * 基本的には、カラム名と、それに対応する値のセットで、QUERY文を作成します。 037 * 値には、[カラム名] が使用でき、出力される値として、? が使われます。 038 * これは、PreparedStatement に対する引数で、処理を行うためです。 039 * この[カラム名]のカラム名は、検索された側のカラム名で、INSERT/UPDATE/DELETE等が実行される 040 * データベース(テーブル)のカラム名ではありません。(偶然、一致しているかどうかは別として) 041 * 042 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 043 * 044 * @version 6.8.6.0 (2018/01/19) 045 * @author Kazuhiko Hasegawa 046 * @since JDK6.0, 047 */ 048public class QueryMaker { 049 private static final String QUERY_TYPE = "SELECT,INSERT,UPDATE,DELETE,MERGE" ; 050 051 private final List<String> whrList = new ArrayList<>() ; // where条件に含まれる [カラム名] のリスト(パラメータ一覧) 052 053 private String queryType ; // QUERYタイプ(SELECT,INSERT,UPDATE,DELETE,MERGE) を指定します。 054 private String table ; 055 private String names ; 056 private String omitNames ; 057 private String where ; 058 private String whrNames ; 059 private String orderBy ; 060 private String cnstKeys ; 061 private String cnstVals ; 062 063 private int clmLen; // names カラムの "?" に置き換えられる個数 064 private boolean isSetup ; // セットアップ済みを管理しておきます。 065 private String[] nameAry; 066 067 /** 068 * デフォルトコンストラクター 069 * 070 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 071 */ 072 public QueryMaker() { super(); } // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。 073 074 /** 075 * 処理の前に、入力データの整合性チェックや、初期設定を行います。 076 * 077 * あまり、何度も実行したくないので、フラグ管理しておきます。 078 * 079 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 080 * @og.rev 6.9.0.2 (2018/02/13) omitNamesの対応 081 * @og.rev 6.9.8.0 (2018/05/28) setup() は、内部処理からのみ呼ばれるので、private化します。 082 */ 083// public void setup() { 084 private void setup() { 085 if( isSetup ) { return; } // セットアップ済み 086 087 if( StringUtil.isNull( table ) ) { 088 final String errMsg = "指定の table に、null、ゼロ文字列は指定できません。" 089 + " table=" + table ; 090 throw new OgRuntimeException( errMsg ); 091 } 092 093 if( StringUtil.isNull( names ) ) { 094 final String errMsg = "指定の names に、null、ゼロ文字列は指定できません。" 095 + " names=" + names ; 096 throw new OgRuntimeException( errMsg ); 097 } 098 099 // 6.9.0.2 (2018/02/13) omitNamesの対応 100 final String[] nmAry = StringUtil.csv2Array( names ); 101 final Set<String> nmSet = new LinkedHashSet<>( Arrays.asList( nmAry ) ); // names の順番は、キープします。 102 final String[] omtAry = StringUtil.csv2Array( omitNames ); 103 final Set<String> omtSet = new HashSet<>( Arrays.asList( omtAry ) ); // 除外する順番は、問いません。 104 nmSet.removeAll( omtSet ); 105 106 // 初期設定 107 clmLen = nmSet.size(); 108// nameAry = nmSet.toArray( new String[clmLen] ); 109 nameAry = nmSet.toArray( new String[0] ); // 8.5.4.2 (2024/01/12) PMD 7.0.0 OptimizableToArrayCall 対応 110 111// // 初期設定 112// nameAry = StringUtil.csv2Array( names ); 113// clmLen = nameAry.length; 114 115 // [カラム名] List は、whereNames + where の順番です。(whrListの登録順を守る必要がある) 116 // where条件も、この順番に連結しなければなりません。 117 where = StringUtil.join( " AND " , whrNames , formatSplit( where ) ); // formatSplit で、whrListの登録を行っている。 118 119 isSetup = true; 120 } 121 122 /** 123 * データを検索する場合に使用するSQL文を作成します。 124 * 125 * SELECT names FROM table WHERE where ORDER BY orderBy ; 126 * 127 * cnstKeys,cnstVals は、使いません。 128 * where,orderBy は、それぞれ、値が存在しない場合は、設定されません。 129 * 130 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 131 * @og.rev 6.9.0.2 (2018/02/13) omitNamesの対応 132 * 133 * @return 検索SQL 134 * @og.rtnNotNull 135 */ 136 public String getSelectSQL() { 137 if( !"SELECT".equals( queryType ) ) { 138 final String errMsg = "指定のQUERYタイプと異なるSQL文を要求しています。" + CR 139 + " 要求SQL=SELECT queryType=" + queryType ; 140 throw new OgRuntimeException( errMsg ); 141 } 142 143 setup(); 144 145 return new OgBuilder() 146// .append( "SELECT " , names ) 147 .append( "SELECT " ) 148 .join( "," , nameAry ) // 6.9.0.2 (2018/02/13) names ではなく、omitNames後のカラム配列を使用します。 149 .append( " FROM " , table ) 150 .appendNN( " WHERE " , where ) // nullなら、追加しない。where + whereNames 151 .appendNN( " ORDER BY " , orderBy ) // nullなら、追加しない。 152 .toString(); 153 } 154 155 /** 156 * データを追加する場合に使用するSQL文を作成します。 157 * 158 * INSERT INTO table ( names,cnstKeys ) VALUES ( values,cnstVals ) ; 159 * 160 * cnstKeys,cnstVals は、INSERTカラムとして使います。 161 * where,orderBy は、使いません。 162 * 163 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 164 * @og.rev 6.9.0.2 (2018/02/13) omitNamesの対応 165 * 166 * @return 追加SQL 167 * @og.rtnNotNull 168 */ 169 public String getInsertSQL() { 170 if( !"INSERT".equals( queryType ) && !"MERGE".equals( queryType ) ) { 171 final String errMsg = "指定のQUERYタイプと異なるSQL文を要求しています。" + CR 172 + " 要求SQL=INSERT queryType=" + queryType ; 173 throw new OgRuntimeException( errMsg ); 174 } 175 176 setup(); 177 178 return new OgBuilder() 179 .append( "INSERT INTO " ).append( table ) 180// .append( " ( " ).append( names ) 181 .append( " ( " ) 182 .join( "," , nameAry ) // 6.9.0.2 (2018/02/13) names ではなく、omitNames後のカラム配列を使用します。 183 .appendNN( "," , cnstKeys ) 184 .append( " ) VALUES ( " ) 185 .appendRoop( 0,clmLen,",",i -> "?" ) 186 .appendNN( "," , cnstVals ) 187 .append( " )" ) 188 .toString(); 189 } 190 191 /** 192 * データを更新する場合に使用するSQL文を作成します。 193 * 194 * UPDATE table SET names[i]=values[i], ・・・cnstKeys[i]=cnstVals[i], ・・・ WHERE where; 195 * 196 * cnstKeys,cnstVals は、UPDATEカラムとして使います。 197 * orderBy は、使いません。 198 * 199 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 200 * @og.rev 6.9.9.1 (2018/08/27) cnstKeys,cnstValsは、個数違いの場合のみ、エラーです。 201 * 202 * @return 更新SQL 203 * @og.rtnNotNull 204 */ 205 public String getUpdateSQL() { 206 if( !"UPDATE".equals( queryType ) && !"MERGE".equals( queryType ) ) { 207 final String errMsg = "指定のQUERYタイプと異なるSQL文を要求しています。" + CR 208 + " 要求SQL=UPDATE queryType=" + queryType ; 209 throw new OgRuntimeException( errMsg ); 210 } 211 212 setup(); 213 214 final String[] cnKey = StringUtil.csv2Array( cnstKeys ); // @og.rtnNotNull 215 final String[] cnVal = StringUtil.csv2Array( cnstVals ); // @og.rtnNotNull 216 217 // 整合性チェック 218 // 6.9.8.0 (2018/05/28) FindBugs:null でないことがわかっている値の冗長な null チェック 219// if( cnKey != null && cnVal == null || 220// cnKey == null && cnVal != null || 221// cnKey != null && cnVal != null && cnKey.length != cnVal.length ) { 222 // 6.9.9.1 (2018/08/27) cnstKeys,cnstValsは、個数違いの場合のみ、エラーです。 223// if( cnKey.length == 0 || cnVal.length == 0 || cnKey.length != cnVal.length ) { 224// final String errMsg = "指定の keys,vals には、null、ゼロ件配列、または、個数違いの配列は指定できません。" 225 if( cnKey.length != cnVal.length ) { 226 final String errMsg = "指定の keys,vals の個数が違ます。" 227 + " keys=" + cnstKeys 228 + " vals=" + cnstVals ; 229 throw new OgRuntimeException( errMsg ); 230 } 231 232 // 6.9.8.0 (2018/05/28) FindBugs:コンストラクタで初期化されていないフィールドを null チェックなしで null 値を利用している 233 // queryType と、nameAry は、setup() メソッドで設定されるため、FindBugs の指摘は、対応済みとなります。 234 // とりあえず、条件判定を入れておいて、FindBugs の警告が出ないようにしておきます。 235 if( nameAry == null ) { 236 // nameAry は、setup() メソッドで設定されるため、このエラーは出ません。 237 final String errMsg = "何らかの不測の事態が発生しました。本来、このエラーは出ません。"; 238 throw new OgRuntimeException( errMsg ); 239 } 240 241 return new OgBuilder() 242 .append( "UPDATE " ).append( table ) 243 .append( " SET " ) 244 .appendRoop( 0,clmLen ,",",i -> nameAry[i] + "=?" ) 245 .appendRoop( 0,cnVal.length,",",i -> cnKey[i] + "=" + cnVal[i] ) 246 .appendNN( " WHERE " , where ) // nullなら、追加しない。where + whereNames 247 .toString(); 248 } 249 250 /** 251 * データを削除する場合に使用するSQL文を作成します。 252 * 253 * DELETE FROM table WHERE where; 254 * 255 * cnstKeys,cnstVal,orderBys は、使いません。 256 * where は、値が存在しない場合は、設定されません。 257 * orderBy は、使いません。 258 * 259 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 260 * 261 * @return 削除SQL 262 * @og.rtnNotNull 263 */ 264 public String getDeleteSQL() { 265 if( !"DELETE".equals( queryType ) ) { 266 final String errMsg = "指定のQUERYタイプと異なるSQL文を要求しています。" + CR 267 + " 要求SQL=DELETE queryType=" + queryType ; 268 throw new OgRuntimeException( errMsg ); 269 } 270 271 setup(); 272 273 return new OgBuilder() 274 .append( "DELETE FROM " ).append( table ) 275 .appendNN( " WHERE " , where ) // nullなら、追加しない。where + whereNames 276 .toString(); 277 } 278 279 /** 280 * [カラム名]を含む文字列を分解し、Map に登録します。 281 * 282 * これは、[カラム名]を含む文字列を分解し、カラム名 を取り出し、whrList に 283 * 追加していきます。 284 * 戻り値は、[XXXX] を、? に置換済みの文字列になります。 285 * 286 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 287 * 288 * @param fmt [カラム名]を含む文字列 289 * @return PreparedStatementに対応した変換後の文字列 290 */ 291 private String formatSplit( final String fmt ) { 292 if( StringUtil.isNull( fmt ) ) { return fmt; } // null,ゼロ文字列チェック 293 294 final StringBuilder rtnStr = new StringBuilder( BUFFER_MIDDLE ); 295 296 int start = 0; 297 int index = fmt.indexOf( '[' ); 298 while( index >= 0 ) { 299 final int end = fmt.indexOf( ']',index ); 300 if( end < 0 ) { 301 final String errMsg = "[ と ] との対応関係がずれています。" 302 + "format=[" + fmt + "] : index=" + index ; 303 throw new OgRuntimeException( errMsg ); 304 } 305 306 // [ より前方の文字列は、rtnStr へ追加する。 307 if( index > 0 ) { rtnStr.append( fmt.substring( start,index ) ); } 308 // index == 0 は、][ と連続しているケース 309 310 // [XXXX] の XXXX部分と、位置(?の位置になる)を、Listに登録 311 whrList.add( fmt.substring( index+1,end ) ); 312 313 rtnStr.append( '?' ); // [XXXX] を、? に置換する。 314 315 start = end+1 ; 316 index = fmt.indexOf( '[',start ); 317 } 318 // ] の後方部分は、rtnStr へ追加する。 319 rtnStr.append( fmt.substring( start ) ); // '[' が見つからなかった場合は、この処理で、すべての fmt データが、append される。 320 321 return rtnStr.toString(); 322 } 323 324 /** 325 * QUERYタイプ(SELECT,INSERT,UPDATE,DELETE,MERGE) を指定します。 326 * 327 * 引数が nullか、ゼロ文字列の場合は、登録しません。 328 * 329 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 330 * 331 * @param queryType QUERYタイプ 332 */ 333 public void setQueryType( final String queryType ) { 334 if( !StringUtil.isNull( queryType ) ) { 335 if( QUERY_TYPE.contains( queryType ) ) { 336 this.queryType = queryType; 337 } 338 else { 339 final String errMsg = "queryType は、" + QUERY_TYPE + " から、指定してください。"; 340 throw new OgRuntimeException( errMsg ); 341 } 342 } 343 } 344 345 /** 346 * テーブル名をセットします。 347 * 348 * 引数が nullか、ゼロ文字列の場合は、登録しません。 349 * 350 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 351 * 352 * @param table テーブル名 353 */ 354 public void setTable( final String table ) { 355 if( !StringUtil.isNull( table ) ) { 356 this.table = table; 357 } 358 } 359 360 /** 361 * テーブル名を取得します。 362 * 363 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 364 * 365 * @return テーブル名 366 */ 367 public String getTable() { 368 return table; 369 } 370 371 /** 372 * カラム名をセットします。 373 * 374 * カラム名は、登録時に、大文字に変換しておきます。 375 * カラム名は、CSV形式でもかまいません。 376 * 引数が nullか、ゼロ文字列の場合は、登録しません。 377 * 378 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 379 * 380 * @param names キー(大文字のみ。内部で変換しておきます。) 381 */ 382 public void setNames( final String names ) { 383 if( !StringUtil.isNull( names ) ) { 384 this.names = names.toUpperCase(Locale.JAPAN); 385 } 386 } 387 388 /** 389 * カラム名を取得します。 390 * 391 * 登録時に、すでに、大文字に変換していますので、 392 * ここで取得するカラム名も、大文字に変換されています。 393 * 394 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 395 * 396 * @return カラム名(大文字に変換済み) 397 */ 398 public String getNames() { 399 return names; 400 } 401 402 /** 403 * 除外するカラム名をセットします。 404 * 405 * カラム名は、登録時に、大文字に変換しておきます。 406 * カラム名は、CSV形式でもかまいません。 407 * 引数が nullか、ゼロ文字列の場合は、登録しません。 408 * 409 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 410 * 411 * @param omitNames キー(大文字のみ。内部で変換しておきます。) 412 */ 413 public void setOmitNames( final String omitNames ) { 414 if( !StringUtil.isNull( omitNames ) ) { 415 this.omitNames = omitNames.toUpperCase(Locale.JAPAN); 416 } 417 } 418 419 /** 420 * WHERE条件をセットします。 421 * 422 * whereNames属性と同時に使用する場合は、"AND" で、処理します。 423 * 引数が nullか、ゼロ文字列の場合は、登録しません。 424 * 425 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 426 * 427 * @param where WHERE条件 428 */ 429 public void setWhere( final String where ) { 430 if( !StringUtil.isNull( where ) ) { 431 this.where = where; 432 } 433 } 434 435 /** 436 * WHERE条件となるカラム名をCSV形式でセットします。 437 * 438 * カラム名配列より、WHERE条件を、KEY=[KEY] 文字列で作成します。 439 * where属性と同時に使用する場合は、"AND" で、処理します。 440 * 引数が nullか、ゼロ件配列の場合は、登録しません。 441 * 442 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 443 * 444 * @param whNames WHERE句作成のためのカラム名 445 */ 446 public void setWhereNames( final String whNames ) { 447 if( !StringUtil.isNull( whNames ) ) { 448 final String[] whAry = StringUtil.csv2Array( whNames ); 449 450 final StringJoiner sj = new StringJoiner( " AND " ); // 区切り文字 451 for( final String whName : whAry ) { 452 whrList.add( whName ); 453 sj.add( whName + "=?" ); 454 } 455 whrNames = sj.toString(); 456 } 457 } 458 459 /** 460 * orderBy条件をセットします。 461 * 462 * 引数が nullか、ゼロ文字列の場合は、登録しません。 463 * 464 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 465 * 466 * @param orderBy orderBy条件 467 */ 468 public void setOrderBy( final String orderBy ) { 469 if( !StringUtil.isNull( orderBy ) ) { 470 this.orderBy = orderBy; 471 } 472 } 473 474 /** 475 * 固定値のカラム名をセットします。 476 * 477 * nullでなく、ゼロ文字列でない場合のみセットします。 478 * カラム名は、CSV形式でもかまいません。 479 * 引数が nullか、ゼロ文字列の場合は、登録しません。 480 * 481 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 482 * 483 * @param keys 固定値のカラム名 484 */ 485 public void setConstKeys( final String keys ) { 486 if( !StringUtil.isNull( keys ) ) { 487 this.cnstKeys = keys; 488 } 489 } 490 491 /** 492 * 固定値のカラム名に対応した、固定値文字列をセットします。 493 * 494 * nullでなく、ゼロ文字列でない場合のみセットします。 495 * 固定値は、CSV形式でもかまいません。 496 * 引数が nullか、ゼロ文字列の場合は、登録しません。 497 * 498 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 499 * 500 * @param vals 固定値 501 */ 502 public void setConstVals( final String vals ) { 503 if( !StringUtil.isNull( vals ) ) { 504 this.cnstVals = vals; 505 } 506 } 507 508 /** 509 * PreparedStatement で、パラメータとなるカラム名の配列を返します。 510 * 511 * これは、QUERYの変数部分 "[カラム名]" を、"?" に置き換えており、 512 * この、カラム名の現れた順番に、配列として返します。 513 * データベース処理では、パラメータを設定する場合に、このカラム名を取得し、 514 * オリジナル(SELECT)のカラム番号から、その値を取得しなければなりません。 515 * 516 * カラム名配列は、QUERYタイプ(queryType)に応じて作成されます。 517 * SELECT : パラメータ は使わないので、長さゼロの配列 518 * INSERT : where条件は使わず、names部分のみなので、0 ~ clmLen までの配列 519 * UPDATE : names も、where条件も使うため、すべての配列 520 * DELETE : names条件は使わず、where部分のみなので、clmLen ~ clmLen+whrLen までの配列(clmLen以降の配列) 521 * 522 * @og.rev 6.8.6.0 (2018/01/19) 新規作成 523 * @og.rev 6.9.8.0 (2018/05/28) セットアップチェックが漏れていた。 524 * @og.rev 8.5.5.1 (2024/02/29) switch式の使用 525 * 526 * @param useInsert queryType="MERGE" の場合に、false:UPDATE , true:INSERT のパラメータのカラム名配列を返します。 527 * @return パラメータとなるカラム名の配列 528 * @og.rtnNotNull 529 */ 530 public String[] getParamNames( final boolean useInsert ) { 531 // 6.9.8.0 (2018/05/28) FindBugs:コンストラクタで初期化されていないフィールドを null チェックなしで null 値を利用している 532 // queryType と、nameAry は、setup() メソッドで設定されるため、FindBugs の指摘は、対応済みとなります。 533 // とりあえず、条件判定を入れておいて、FindBugs の警告が出ないようにしておきます。 534 535 // 6.9.8.0 (2018/05/28) セットアップチェックが漏れていた。 536 // 8.5.5.1 (2024/02/29) spotbugs UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR 537 // ※ StringUtil.isNull( queryType ) で、queryType の nullチェックは出来ているが、spotbugs で判断できないので、あえて入れておきます。 538// if( !isSetup || StringUtil.isNull( queryType ) || nameAry == null ) { 539 if( queryType == null || !isSetup || StringUtil.isNull( queryType ) || nameAry == null ) { 540 final String errMsg = "getParamNames(boolean) は、SQL文を取得してから、行ってください。"; 541 throw new OgRuntimeException( errMsg ); 542 } 543 544// final String[] whrAry = whrList.toArray( new String[whrList.size()] ); 545 final String[] whrAry = whrList.toArray( new String[0] ); // 8.5.4.2 (2024/01/12) PMD 7.0.0 OptimizableToArrayCall 対応 546 final String[] allAry = Arrays.copyOf( nameAry , nameAry.length + whrList.size() ); 547 System.arraycopy( whrAry , 0 , allAry , nameAry.length , whrAry.length ); // allAry = nameAry + whrAry の作成 548 549 // 8.5.5.1 (2024/02/29) switch式の使用 550// String[] rtnClms = null; 551// switch( queryType ) { 552// case "SELECT" : rtnClms = new String[0]; break; // パラメータはない。 553// case "INSERT" : rtnClms = nameAry; break; // names指定の分だけ、パラメータセット 554// case "UPDATE" : rtnClms = allAry; break; // names+whereの分だけ、パラメータセット 555// case "DELETE" : rtnClms = whrAry; break; // whereの分だけ、パラメータセット 556// case "MERGE" : rtnClms = allAry; break; // useInsert=false は、UPDATEと同じ 557// default : break; 558// } 559 String[] rtnClms = switch( queryType ) { 560 case "SELECT" -> new String[0]; // パラメータはない。 561 case "INSERT" -> nameAry; // names指定の分だけ、パラメータセット 562 case "UPDATE" -> allAry; // names+whereの分だけ、パラメータセット 563 case "DELETE" -> whrAry; // whereの分だけ、パラメータセット 564 case "MERGE" -> allAry; // useInsert=false は、UPDATEと同じ 565 default -> null; // 上記以外は、null 566 }; 567 568 if( useInsert && "MERGE".equals( queryType ) ) { 569 rtnClms = nameAry; // MERGEで、useInsert=true は、INSERTと同じ 570 } 571 572 return rtnClms; 573 } 574}