LoginSignup
0
0

【PHP】クロージャで配列を操作し、連番を効率よく生成する

Last updated at Posted at 2022-04-28

システム作成において、DBテーブルの処理が必要な案件にはよく連番やグループ番号を作成していくということがあります。そして、その度、各テーブルに問い合わせて最大値を取得するというような面倒なことをしていたのですが、クロージャを用いることで、その手間を省略し、パフォーマンスも大幅に向上させることができます。

そして、だいたい番号作成には以下の種類がパターン化しています。

  • 初めて登場するグループ番号 → 初期値は1
  • インポートされたデータで2回め以降に登場するグループ番号 → システム内でインクリメントされた値
  • 既にDBで登録されているグループ → DBテーブル内の最大値+1

そして、これらの処理を一つの関数で処理して、作業を効率化しようというのがこの記事の狙いです。

バルクインサートでグループ番号を一括生成する

システムを構築する際には、データを一括登録したいという要望を受けることがよくあります。そのシステム構築にはcsvデータやExcelデータなどを読み込み、DBテーブルにインポートしていくという流れで実装していきます。

そして、DBテーブルに登録していく場合、自分はよくバルクインサートを用います。バルクインサートとはinsert文一つで複数のデータ行を登録できるというもので

insert into trn_records values(1,1,"hoge"),(2,1,"fuga"),(1,2,"hoge")…………;

こんな感じで記述していきます。なぜ、このバルクインサートを活用するかといえば、もし登録データにエラーなどが生じた場合に処理を全部中断してくれるからです。これがプログラム内でinsert文を行数分繰り返したりすると、エラーが生じた場合に、その時点で中断されます。なので、次からどのデータを入れていけばいいのかわからない、また一括登録なので、インポートファイルに適宜行削除などの再調整が必要という面倒な事態に陥ることがあります。

ですが、このバルクインサートにおいて大きな問題点があります。それはグループごとのシーケンスを取得するのが困難になるからで、データの中には上述の3パターンが混在し、それらを逐一振り分けてデータを自動生成していく必要があります。なお、今回はPHPを用いていますが、原理さえわかれば、他の言語でも可能じゃないかと思います(array_key_existsと同様の挙動をする関数があれば、より簡単です)。

プログラム作成

それで作ってみたプログラムの根幹が以下の部分です。原理は単純でarray_key_exists関数を用いることによって、この3種類のパターンを簡単に処理できるようになります。array_key_exists関数は検索値のキーが存在するかを確認するための関数で、bool判定を返します。

新規か加算か?

新規に入ってくるグループか、インクリメントが必要なグループかを判別したい場合、対象となるグループ名でキーを作成し、それをarray_key_exist関数で判別すれば、簡単に振り分けが可能です。

php
    //既に登録されたグループ名かを判定
	if(array_key_exists($row_value,$ar_count)){
		$ar_count[$row_value]++; //登録済ならグループ番号をインクリメントする
	}else{
		$ar_count[$row_value] = 1; //未登録なら、グループ名をキーにして初期値を代入する。
	}

これを応用して、今回のインポートファイルに登録済でないデータのうち、もう一つif文を用いて、DBテーブル内に格納されているグループの最大値+1を取得するようにすれば、3パターンのシーケンス取得に対応できます。

php
    //既に登録されたグループ名かを判定
	if(array_key_exists($row_value,$ar_count)){
		$ar_count[$row_value]++;
	}else{
        /*中略。ここでDBテーブルの最大値を取得する*/
        if(is_countable($rows)){
            $ar_count[$row_value] = $rows['max'] + 1; //DBテーブル上の最大値+1
        }else{
		     $ar_count[$row_value] = 1;  //DB未登録なら、グループ名をキーにして初期値を代入する。
        }
	}

カウント処理用の関数を作成

しかしこのままでは使いづらいので、これをクロージャで呼び出しできるように改良します。PHPでクロージャを用いるのはけっこう簡単で、だいたい、こんな式になります。

php
  function countvalue(){
      $cnt = 1; //初期値
      //ここがクロージャ、`&$cnt`は参照渡し。
      return function() use(&$cnt){
          return $cnt++;
      }
  }
  echo countvalue(); //1
  echo countvalue(); //2

クロージャのメリットは、変数の値を継承してくれることで、継続的に同じ変数を処理できることです。そして、実例では単一の変数に対して用いることが多いのですが、このようにクロージャは配列にも応用できます(しかも、キーごとに格納された値も保持してくれている)。

この特長を活かして、クラス化した上で使用します。このオブジェクト指向でクロージャを用いる方法が、なかなか情報が少なく困っていましたが、以下の記事を参考にしました。クロージャを用いることで、継続的にグループごとにシーケンスのインクリメントが実現できます。

注意点は、インスタンスによって生成されたメソッドにシーケンスを作成したいデータを代入しないことです。また、インスタンスとメソッドによるクロージャ作成はループ文の外側で定義してください(ループ内で定義すると、逐一値が初期化されてしまうので、クロージャの意味がありません)。

php
    //一括インポートによって得られたデータ
	$ar_values = [
		["さいたま","川口","さいたま","越谷","さいたま"],
		["さいたま","川越","所沢","越谷","川口"],
		["さいたま","川口","川越","春日部","上尾"],
		["熊谷","所沢","上尾","越谷","春日部"],
		["上尾","川口","さいたま","草加","草加"],
	];
	$Calc = new Calc(); //インスタンス作成
	$Cntval = $Calc -> count(); //メンバからクロージャを作成。ここに変数を代入しないこと!
	foreach($ar_values as $row_value){
		$datas = $Cntval($row_value); //このメソッドがクロージャ、ここに引数を代入する。
		echo var_export($datas),"<hr>";
	}
	
    //シーケンス計算のためのクラス
	class Calc
	{
		private $ar_count = []; //シーケンスの格納用
		
        //シーケンス計算のためのメソッド
		public function count(){
			$ar_count = $this -> ar_count; //初期値はクロージャの外側で
            //クロージャ制御
			return function($ar_values) use(&$ar_count){ 
                $values = []; //シーケンスを生成したデータの格納用
				foreach($ar_values as $row_value){
                    //既に登録されたグループ名かを判定
					if(array_key_exists($row_value,$ar_count)){
						$ar_count[$row_value]++;
					}else{
                        /*ここでDBテーブルの最大値を取得する*/
                        //作業は省略
                        /*最大値は$rows['max']に格納、存在しない場合はnullが返される*/
                        if(is_countable($rows)){
                            $ar_count[$row_value] = $rows['max'] + 1; //DBテーブル上の最大値+1
                        }else{
		                    $ar_count[$row_value] = 1; //新規グループの初期値
                        }
					}
					$values[] = [$row_value,$ar_count[$row_value]];
				}
				return $values;
			};
		}
	}

データはこのように作成されます。配列は[グループ名,グループのシーケンス]となっており、各グループごとに番号がインクリメント(加算)されているのがわかると思います。たとえば、さいたまは6件登録するので最後のシーケンスは6、川口は4……となっています。

score.jpg

後はこれをinsert文に代入していくだけです(DBテーブルのバルクインサート処理制御プログラムは割愛)。

次からはインポートデータに存在しない値の場合は、DBテーブルに存在する値との比較になるので、さいたまの初期値は7、川口の初期値は5が取得されます。また、狭山、入間、新座といった値が登場した場合の初期値は1となります(現時点ではインポートファイルにもDBテーブル上にも存在しないため)。もう一度さいたまが取得された場合は、カウント用の変数、$ar_count['さいたま']は7となっているので値を継承して8となり、二度目に狭山が登場した場合は2となり、このようにして常時、インポートされたファイルのタイミングでシーケンス取得を継続できます。

※ここではグループ名がユニークであるという前提で、そのまま配列のキーとしています。2バイト文字のキー名が不安ならば、json_encode、bintohexなどを用いて、任意の文字列化(同じ引数操作なので、乱数発生はしません)するといいでしょう。また、本来ならば後々の運用を考えてグループidを用いるのがベターです。

また、単一の数値ではなく、任意コードの連番を作成したい場合はsprintf関数を活用すればいいでしょう。

応用

今回は静的トランザクションのグループ名でシーケンスを作成していますが、この方法を活用すれば、記録テーブルや履歴テーブルを一括登録する際に利用回数を付与したい場合、ファイルのエクスポート時に自動的にグループ番号を採番していくとき、また集計作業などにも応用できます。

そして、オブジェクト指向で作成しているのでインスタンスを複製していけば、システム内のあらゆる連番作成処理を一つの関数で処理することができます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0