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