Symfoware

Symfowareについての考察blog

CodeIgniter 3 自分で作成したモデルを継承する

CodeIgniter 3でCI_Modelを継承しモデルを定義。

・Base_model


  1. <?php
  2. // Base_modelを継承したモデル
  3. // Controllerからはこれを使用したい
  4. class Ext_model extends Base_model {
  5.     
  6.     public function say() {
  7.         echo 'Ext_model'.PHP_EOL;
  8.     }
  9.     
  10. }




このモデルを再度継承して拡張モデルを定義したい。

・Ext_model


  1. <?php
  2. // CI_Modelを継承したモデル
  3. // これを継承元としたい
  4. class Base_model extends CI_Model {
  5.     
  6.     public function say() {
  7.         echo 'Base_model'.PHP_EOL;
  8.     }
  9.     
  10. }




この状態で使用するとエラーになります。

・Test.php


  1. <?php
  2. class Test extends CI_Controller {
  3.     
  4.     // 作成した継承モデルの呼び出し
  5.     public function index() {
  6.         
  7.         $this->load->model('ext_model');
  8.         $this->ext_model->say();
  9.         
  10.     }
  11.     
  12. }





# php index.php test
PHP Fatal error: Class 'Base_model' not found in /var/dev/php/ci3/application/models/Ext_model.php on line 5

Fatal error: Class 'Base_model' not found in /var/dev/php/ci3/application/models/Ext_model.php on line 5

A PHP Error was encountered

Severity:    Error
Message:     Class 'Base_model' not found
Filename:    /var/dev/php/ci3/application/models/Ext_model.php
Line Number: 5

Backtrace:




まあ、Base_modelがロードされていないので当然なのですが。





解決策1



Base_modelがロードされていないことが原因なので、事前にロードしてやります。


  1. <?php
  2. class Test extends CI_Controller {
  3.     
  4.     // 作成した継承モデルの呼び出し
  5.     public function index() {
  6.         
  7.         // 事前にbase_modelをロード
  8.         $this->load->model('base_model');
  9.         $this->load->model('ext_model');
  10.         $this->ext_model->say();
  11.         
  12.     }
  13.     
  14. }




# php index.php test
Ext_model




動くには動きますし、modelのautoloadに指定してしまえば良いのですが、
あんまりしっくりきません。





解決策2



解決策1と似たような発想で、Ext_modelにBase_modelがロード済みか
チェックするロジックを入れてしまいます。

・Ext_model


  1. <?php
  2. // Base_modelを継承したモデル
  3. // Controllerからはこれを使用したい
  4. // 事前にBase_modelがロード済みかチェック
  5. if (!class_exists('Base_model')) {
  6.     // 未ロードならロード
  7.     get_instance()->load->model('base_model');
  8. }
  9. class Ext_model extends Base_model {
  10.     
  11.     public function say() {
  12.         echo 'Ext_model'.PHP_EOL;
  13.     }
  14.     
  15. }




これで呼び出せるようになりました。


  1. <?php
  2. class Test extends CI_Controller {
  3.     
  4.     // 作成した継承モデルの呼び出し
  5.     public function index() {
  6.         
  7.         $this->load->model('ext_model');
  8.         $this->ext_model->say();
  9.         
  10.     }
  11.     
  12. }




# php index.php test
Ext_model







解決策3



Hookとspl_autoload_registerの組み合わせを試してみます。

CodeIgniter 3 処理のフック(General Topics - Hooks - Extending the Framework Core)
PHP spl_autoload_registerでautoloadを自前で実装


※以下、例外処理のない適当な実装です。

まずHookを有効化。
application/config/config.phpのenable_hooksをtrueにします。


  1. /*
  2. |--------------------------------------------------------------------------
  3. | Enable/Disable System Hooks
  4. |--------------------------------------------------------------------------
  5. |
  6. | If you would like to use the 'hooks' feature you must enable it by
  7. | setting this variable to TRUE (boolean). See the user guide for details.
  8. |
  9. */
  10. $config['enable_hooks'] = TRUE;




application/config/hooks.phpを編集
pre_systemでspl_autoload_registerを登録します。


  1. <?php
  2. defined('BASEPATH') OR exit('No direct script access allowed');
  3. /*
  4. | -------------------------------------------------------------------------
  5. | Hooks
  6. | -------------------------------------------------------------------------
  7. | This file lets you define "hooks" to extend CI without hacking the core
  8. | files. Please see the user guide for info:
  9. |
  10. |    https://codeigniter.com/user_guide/general/hooks.html
  11. |
  12. */
  13. $hook['pre_system'][] = spl_autoload_register(function($className) {
  14.     // Base_modelの解決
  15.     get_instance()->load->model($className);
  16. });




これで動いてくれました。



わかりやすさから、解決策2のモデルクラスの先頭でチェックするのが良さそうでした。



テーマ:プログラミング - ジャンル:コンピュータ

  1. 2017/02/14(火) 00:02:13|
  2. PHP
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集

PHP 「@」でエラー抑止していても処理が途中で終了する

例えばこんなコード。


  1. <?php
  2. // とりあえずファイルを開こうとしてみる
  3. // 存在しない場合のエラーは「@」で抑止
  4. $file = @file_get_contents('dummy.txt');
  5. if ($file === false) {
  6.     echo 'ファイルが存在しません'.PHP_EOL;
  7.     
  8. } else {
  9.     echo $file.PHP_EOL;
  10. }



「@」でエラーを抑止し、とりあえずファイルを開いてみる。
戻り値がfalseだったらファイルが存在しないと判断。


$ php sample.php
ファイルが存在しません




このような処理が書いてあるプログラムで、ある環境ではうまく動くのですが、
別の環境ではファイルが存在しない時に処理が止まってしまい悩んでいました。

よくよく調べてみると、動かない環境では事前に読み込んでいるファイルで
「set_error_handler」が設定されており、ここでexitしています。


  1. <?php
  2. // -------------------------------------------
  3. // 事前に読み込まれるファイルで実行されている処理
  4. function _hook_exception_handler($severity, $message, $filepath, $line) {
  5.     echo $message.PHP_EOL;
  6.     exit();
  7. }
  8. set_error_handler('_hook_exception_handler');
  9. // -------------------------------------------
  10. // とりあえずファイルを開こうとしてみる
  11. // 存在しない場合のエラーは「@」で抑止
  12. $file = @file_get_contents('dummy.txt');
  13. if ($file === false) {
  14.     echo 'ファイルが存在しません'.PHP_EOL;
  15.     
  16. } else {
  17.     echo $file.PHP_EOL;
  18. }





$ php sample.php
file_get_contents(dummy.txt): failed to open stream: No such file or directory




なるほど、エラーを抑止しててもエラー発生時の処理でexitされたら
プログラムの実行は止まりますよね。


該当処理の直前で「set_error_handler(null)」とすることで問題を回避しました。


  1. <?php
  2. // -------------------------------------------
  3. // 事前に読み込まれるファイルで実行されている処理
  4. function _hook_exception_handler($severity, $message, $filepath, $line) {
  5.     echo $message.PHP_EOL;
  6.     exit();
  7. }
  8. set_error_handler('_hook_exception_handler');
  9. // -------------------------------------------
  10. // デフォルトのエラー設定を解除
  11. // 戻り値は変更前の設定無いようなので対比しておく
  12. $error_handlers = set_error_handler(null);
  13. // とりあえずファイルを開こうとしてみる
  14. // 存在しない場合のエラーは「@」で抑止
  15. $file = @file_get_contents('dummy.txt');
  16. if ($file === false) {
  17.     echo 'ファイルが存在しません'.PHP_EOL;
  18.     
  19. } else {
  20.     echo $file.PHP_EOL;
  21. }
  22. echo '初期値に復元'.PHP_EOL;
  23. // エラー設定を元に戻す
  24. set_error_handler($error_handlers);
  25. $file = @file_get_contents('dummy.txt');
  26. if ($file === false) {
  27.     echo 'ファイルが存在しません'.PHP_EOL;
  28.     
  29. } else {
  30.     echo $file.PHP_EOL;
  31. }




狙い通り、最初の処理だけ_hook_exception_handlerが実行されないようになりました。


$ php sample.php
ファイルが存在しません
初期値に復元
file_get_contents(dummy.txt): failed to open stream: No such file or directory




これ、半日ぐらいはまりましたよ...



【参考URL】

set_error_handler


テーマ:プログラミング - ジャンル:コンピュータ

  1. 2016/12/04(日) 16:55:56|
  2. PHP
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集

CodeIgniter 大量データの更新を高速に行う方法

CodeIgniterで大量データを一括登録する方法について調べました。
CodeIgniterで大量データを高速に登録する方法の検討

今回は一括更新の方法について調べてみます。


下準備



CodeIgniter 3.1.0、MariaDB 10.0.26でテストしてみます。

データは前回同様郵便番号の情報を使用します。
http://www.post.japanpost.jp/zipcode/dl/kogaki-zip.html

更新するにあたり、郵便番号をプライマリキーとして使用したかったので、
こんなPythonスクリプトで重複のない住所データを作成しました。


  1. # -*- coding:utf-8 *-
  2. import codecs
  3. def sunit(s, zip_codes):
  4.     
  5.     s = s.strip()
  6.     data = [s.strip('"') for s in line.split(u',')]
  7.     zip_code = data[2]
  8.     if (zip_code in zip_codes):
  9.         return None
  10.     
  11.     zip_codes.add(zip_code)
  12.     address = data[6] + data[7] + data[8]
  13.     
  14.     return '%s,%s\n' % (zip_code, address)
  15. kenall = codecs.open('KEN_ALL.CSV', 'r', 'ms932')
  16. out = codecs.open('all.csv', 'w', 'utf-8')
  17. zip_codes = set()
  18. with kenall, out:
  19.     for line in kenall:
  20.         data = sunit(line, zip_codes)
  21.         if not data:
  22.             continue
  23.         out.write(data)




119,777件のテストデータとなりました。
以下のテーブルを作成し、事前にデータの登録を行っておきます。


  1. create table zip (
  2.     zip_code char(7) not null primary key,
  3.     address varchar(100) not null
  4. );







通常の更新



1件ずつ郵便番号をキーに住所を更新するプログラムはこうなりました。

・application/controllers/Test.php


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // csvファイル読み込み準備
  8.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  9.         $csv->setFlags(SplFileObject::READ_CSV);
  10.         
  11.         
  12.         // 時間計測開始
  13.         $start = microtime(true);
  14.         
  15.         $rows = [];
  16.         
  17.         foreach ($csv as $row) {
  18.             if ($row === [null]) {
  19.                 break;
  20.             }
  21.             
  22.             $this->db->where('zip_code', $row[0]);
  23.             $this->db->update(
  24.                 'zip',
  25.                 ['address' => '変更1'.$row[1]]
  26.             );
  27.         }
  28.         
  29.         $stop = microtime(true);
  30.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  31.         
  32.     }
  33. }



実行してみると、3,947秒でした。


$ php index.php test
処理時間:3947.239 秒



テストは仮想環境で行っているため、ディスクIOが多く発生すると
性能が低下するようです。





一括コミット



コードはほぼそのままに、ループ全体にトランザクションをかけて
一括更新するようにしてみます。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // csvファイル読み込み準備
  8.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  9.         $csv->setFlags(SplFileObject::READ_CSV);
  10.         
  11.         
  12.         // 時間計測開始
  13.         $start = microtime(true);
  14.         
  15.         // トランザクションをかけて一括更新
  16.         $this->db->trans_start();
  17.         foreach ($csv as $row) {
  18.             if ($row === [null]) {
  19.                 break;
  20.             }
  21.             
  22.             $this->db->where('zip_code', $row[0]);
  23.             $this->db->update(
  24.                 'zip',
  25.                 ['address' => '変更2'.$row[1]]
  26.             );
  27.         }
  28.         
  29.         $this->db->trans_complete();
  30.         
  31.         $stop = microtime(true);
  32.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  33.         
  34.     }
  35. }



実行してみると20秒程度。
劇的に性能が向上しました。


$ php index.php test
処理時間:20.908 秒







update_batch



CodeIgniterのドキュメントを見てみると、update_batchという
メソッドが用意されていました。
Query Builder Class

どうやってupdate文をバッチ処理するんだろう?と思い、
実行するクエリを調べてみると、このようにcaseで設定する値を
変更していました。なるほど。


  1. UPDATE `zip` SET `address` = CASE
  2. WHEN `zip_code` = '0600000' THEN '変更3北海道札幌市中央区以下に掲載がない場合'
  3. WHEN `zip_code` = '0640941' THEN '変更3北海道札幌市中央区旭ケ丘'
  4. WHEN `zip_code` = '0600041' THEN '変更3北海道札幌市中央区大通東'
  5. ELSE `address` END
  6. WHERE `zip_code` IN('0600000','0640941','0600041')




update_batchを使用した処理に変更してみます。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // csvファイル読み込み準備
  8.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  9.         $csv->setFlags(SplFileObject::READ_CSV);
  10.         
  11.         
  12.         // 時間計測開始
  13.         $start = microtime(true);
  14.         
  15.         $rows = [];
  16.         
  17.         foreach ($csv as $row) {
  18.             if ($row === [null]) {
  19.                 break;
  20.             }
  21.             
  22.             $rows[] = [
  23.                 'zip_code' => $row[0],
  24.                 'address' => '変更3'.$row[1],
  25.             ];
  26.             
  27.         }
  28.         
  29.         // update_batchで一括処理
  30.         // 第三引数に更新のキーとなる値を指定する
  31.         $this->db->update_batch('zip', $rows, 'zip_code');
  32.         
  33.         $stop = microtime(true);
  34.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  35.         
  36.     }
  37. }




8秒で処理が終了しました。


$ php index.php test
処理時間:8.339 秒




update_batchは第四引数で一括処理するレコード数を指定できます。(デフォルト100)
1,000件、10,000件と一括処理する件数を変更して試してみます。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // csvファイル読み込み準備
  8.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  9.         $csv->setFlags(SplFileObject::READ_CSV);
  10.         
  11.         
  12.         // 時間計測開始
  13.         $start = microtime(true);
  14.         
  15.         $rows = [];
  16.         
  17.         foreach ($csv as $row) {
  18.             if ($row === [null]) {
  19.                 break;
  20.             }
  21.             
  22.             $rows[] = [
  23.                 'zip_code' => $row[0],
  24.                 'address' => '変更3'.$row[1],
  25.             ];
  26.             
  27.         }
  28.         
  29.         // update_batchで一括処理
  30.         // 第三引数に更新のキーとなる値を指定する
  31.         // 第四引数で一括処理するレコード数を指定
  32.         $this->db->update_batch('zip', $rows, 'zip_code', 1000);
  33.         
  34.         $stop = microtime(true);
  35.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  36.         
  37.     }
  38. }




1,000件一括更新

$ php index.php test
処理時間:9.253 秒



10,000件一括更新

$ php index.php test
処理時間:49.161 秒




クエリーの生成やデータベースサーバー側での解析に時間がかかるようで、
性能はデフォルトの100件の時と変わりませんでした。





update_batch + トランザクション



トランザクションをかけた状態で、update_batchを実行してみます。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // csvファイル読み込み準備
  8.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  9.         $csv->setFlags(SplFileObject::READ_CSV);
  10.         
  11.         
  12.         // 時間計測開始
  13.         $start = microtime(true);
  14.         
  15.         $rows = [];
  16.         
  17.         foreach ($csv as $row) {
  18.             if ($row === [null]) {
  19.                 break;
  20.             }
  21.             
  22.             $rows[] = [
  23.                 'zip_code' => $row[0],
  24.                 'address' => '変更5'.$row[1],
  25.             ];
  26.             
  27.         }
  28.         
  29.         // トランザクションをかけて一括更新
  30.         $this->db->trans_start();
  31.         
  32.         // update_batchで一括処理
  33.         // 第三引数に更新のキーとなる値を指定する
  34.         // 第四引数で一括処理するレコード数を指定
  35.         $this->db->update_batch('zip', $rows, 'zip_code', 100);
  36.         
  37.         $this->db->trans_complete();
  38.         
  39.         $stop = microtime(true);
  40.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  41.         
  42.     }
  43. }




100件ずつ一括更新

$ php index.php test
処理時間:9.680 秒



1,000件ずつ一括更新

$ php index.php test
処理時間:9.636 秒



10,000件ずつ一括更新

$ php index.php test
処理時間:51.278 秒




case文の解釈に時間がかかるのでしょうか、トランザクションの有無による
実行時間の差はほとんどありませんでした。




まとめ



各パターン5回実行した時の結果と平均をまとめています。数字はすべて秒です。
通常パターンはあまりに時間がかかるので1回しか測定していません。

内容1回目2回目3回目4回目5回目平均
単件update3947.239----3947.239
一括update20.90819.88719.65220.3619.31520.024
update_batch(100)8.3398.3688.2088.5998.8248.468
update_batch(1,000)9.2538.6118.5368.9688.4518.764
update_batch(10,00)49.16152.61449.04149.27548.85849.79
update_batch(100) + tran9.688.4468.2368.688.3978.688
update_batch(1,000) + tran9.6368.8099.6919.2498.4639.17
update_batch(10,00) + tran51.27850.61448.50550.18549.26449.969


update_batchデフォルトの100件ずつ、トランザクションなしで実行するのが一番速そうです。




テーマ:プログラミング - ジャンル:コンピュータ

  1. 2016/08/06(土) 17:07:19|
  2. PHP
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集

CodeIgniterで大量データを高速に登録する方法の検討

PHPのwebフレームワークCodeIgniter
http://www.codeigniter.com/

このフレームワークを使用しての大量データ投入を
高速に行う方法を考えてみます。

使用したCodeIgniterのバージョンは3.1.0
PHPのバージョンは5.6.24です。


サンプルデータ



郵便番号データ123,929件を登録してみます。
http://www.post.japanpost.jp/zipcode/dl/kogaki-zip.html

こちらの全国一括版を使用しました。
Pythonで文字コードをutf-8に変換し、郵便番号と住所だのcsvファイルに加工しておきます。


  1. # -*- coding:utf-8 *-
  2. def sunit(s):
  3.     
  4.     s = s.strip()
  5.     data = [s.strip('"') for s in line.split(u',')]
  6.     zip_code = data[2]
  7.     address = data[6] + data[7] + data[8]
  8.     
  9.     return '%s,%s\n' % (zip_code, address)
  10. kenall = codecs.open('KEN_ALL.CSV', 'r', 'ms932')
  11. out = codecs.open('all.csv', 'w', 'utf-8')
  12. with kenall, out:
  13.     for line in kenall:
  14.         out.write(sunit(line))




登録するデータベースにはMariaDB 10.0.26を使用します。
Debian 8.5にインストールしました。


郵便番号を格納するテーブルの定義は以下のとおりです。


  1. create table zip (
  2.     zip_code char(7) not null,
  3.     address varchar(100) not null
  4. );






csvファイルの読み込み



csvファイルを読み込むだけの処理はこんな感じになります。

・application/controllers/Test.php


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // 全件削除
  8.         $this->db->empty_table('zip');
  9.         
  10.         // csvファイル読み込み準備
  11.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  12.         $csv->setFlags(SplFileObject::READ_CSV);
  13.         
  14.         
  15.         // 時間計測開始
  16.         $start = microtime(true);
  17.         
  18.         foreach ($csv as $row) {
  19.             if ($row === [null]) {
  20.                 break;
  21.             }
  22.         }
  23.         
  24.         $stop = microtime(true);
  25.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  26.         
  27.     }
  28. }




コマンドでプログラムを実行します。


$ php index.php test




時間を計測すると、0.3226秒でした。





単件インサート



1件づつ登録するサンプルです。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // 全件削除
  8.         $this->db->empty_table('zip');
  9.         
  10.         // csvファイル読み込み準備
  11.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  12.         $csv->setFlags(SplFileObject::READ_CSV);
  13.         $csv->setFlags($csv->getFlags() | SplFileObject::SKIP_EMPTY);
  14.         $csv->setFlags($csv->getFlags() | SplFileObject::DROP_NEW_LINE);
  15.         
  16.         
  17.         // 時間計測開始
  18.         $start = microtime(true);
  19.         
  20.         foreach ($csv as $row) {
  21.             $this->db->insert('zip', [
  22.                 'zip_code' => $row[0],
  23.                 'address' => $row[1],
  24.             ]);
  25.         }
  26.         
  27.         $stop = microtime(true);
  28.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  29.         
  30.     }
  31. }




結果は4074秒となりました。
なんだか怪しい測定結果ですが、かなり待たされたのは間違いないです。





insert_batch



insert_batchを使用して100件毎に一括登録してみます。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // 全件削除
  8.         $this->db->empty_table('zip');
  9.         
  10.         // csvファイル読み込み準備
  11.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  12.         $csv->setFlags(SplFileObject::READ_CSV);
  13.         
  14.         
  15.         // 時間計測開始
  16.         $start = microtime(true);
  17.         
  18.         $rows = [];
  19.         
  20.         foreach ($csv as $row) {
  21.             if ($row === [null]) {
  22.                 break;
  23.             }
  24.             
  25.             $rows[] = [
  26.                 'zip_code' => $row[0],
  27.                 'address' => $row[1],
  28.             ];
  29.             
  30.             if (count($rows) >= 100) {
  31.                 $this->db->insert_batch('zip', $rows);
  32.                 $rows = [];
  33.             }
  34.         }
  35.         
  36.         if (!empty($rows)) {
  37.             $this->db->insert_batch('zip', $rows);
  38.         }
  39.         
  40.         $stop = microtime(true);
  41.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  42.         
  43.     }
  44. }




結果は40秒程度。
格段に性能が向上しました。


合わせて、一括登録する件数を1,000件、10,000件と増やしてみます。
insert_batchはデフォルトで100件毎に分割してinsertするので、
一括登録する件数を引数で明示してやります。
若干処理を変更しました。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // 全件削除
  8.         $this->db->empty_table('zip');
  9.         
  10.         // csvファイル読み込み準備
  11.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  12.         $csv->setFlags(SplFileObject::READ_CSV);
  13.         
  14.         
  15.         // 時間計測開始
  16.         $start = microtime(true);
  17.         
  18.         $rows = [];
  19.         
  20.         foreach ($csv as $row) {
  21.             if ($row === [null]) {
  22.                 break;
  23.             }
  24.             
  25.             $rows[] = [
  26.                 'zip_code' => $row[0],
  27.                 'address' => $row[1],
  28.             ];
  29.         }
  30.         
  31.         // 1,000件区切りでインサート
  32.         $this->db->insert_batch('zip', $rows, null, 1000);
  33.         
  34.         $stop = microtime(true);
  35.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  36.         
  37.     }
  38. }




1,000件毎インサートで6.7秒
10,000件毎インサートで2.8秒程度でした。
格段に性能が向上しています。





トランザクション



トランザクションをかけて、自動コミットを抑止してみます。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // 全件削除
  8.         $this->db->empty_table('zip');
  9.         
  10.         // csvファイル読み込み準備
  11.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  12.         $csv->setFlags(SplFileObject::READ_CSV);
  13.         
  14.         
  15.         // 時間計測開始
  16.         $start = microtime(true);
  17.         
  18.         $rows = [];
  19.         
  20.         foreach ($csv as $row) {
  21.             if ($row === [null]) {
  22.                 break;
  23.             }
  24.             
  25.             $rows[] = [
  26.                 'zip_code' => $row[0],
  27.                 'address' => $row[1],
  28.             ];
  29.         }
  30.         
  31.         // トランザクションをかけて一括更新
  32.         $this->db->trans_start();
  33.         $this->db->insert_batch('zip', $rows, null, 100);
  34.         $this->db->trans_complete();
  35.         
  36.         $stop = microtime(true);
  37.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  38.         
  39.     }
  40. }




100件毎が4.9秒
1,000件毎が2.4秒
10,000件毎が2.2秒
という結果になりました。





まとめ



結果をまとめると以下のようになります。

5回試行して平均を求めています。(単件登録はあまりに時間がかかるので測定は1回でやめました)
単位はすべて秒です。

内容1回目2回目3回目4回目5回目平均
ループのみ0.3260.3230.3210.3240.3190.323
単件insert4074.385----4074.385
insert_batch(100)41.09640.55241.67940.43940.33640.82
insert_batch(1,000)6.5716.5716.7566.6497.0346.716
insert_batch(10,000)2.6982.9252.8312.6982.7062.772
insert_batch(100)+tran4.7945.0064.8375.0495.0294.943
insert_batch(1,000)+tran2.1432.3632.5342.4342.6032.415
insert_batch(10,000)+tran2.1182.2282.3062.2222.2192.219


・可能な限り1回のbatch_size(登録件数)を増やす。
・ロールバックする気がなくてもトランザクションをかける。

この2つでかなりの高速化が可能です。

テーマ:プログラミング - ジャンル:コンピュータ

  1. 2016/08/05(金) 01:12:39|
  2. PHP
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集

PHP pharに同梱したプログラムにautoloadを適用する

PHPのソースファイルをpharにまとめる方法を調べました。
PHP pharで複数のソースファイルを1つのファイルにまとめる

また、autoloadを自前で実装する方法を調べました。
PHP spl_autoload_registerでautoloadを自前で実装


これらを組み合わせて、pharで固めたソースファイルに対し、
autoloadが適用されるようにしてみます。



サンプルプログラム



pharにするサンプルプログラムは、
PHP spl_autoload_registerでautoloadを自前で実装
こちらで作成したソースにしました。

702_02.png


・classes/Fuga.php


  1. <?php
  2. // classes/Fuga.php
  3. class Fuga {
  4.     
  5.     public function call() {
  6.         echo 'Fuga!'.PHP_EOL;
  7.     }
  8.     
  9. }




・classes/Hoge.php


  1. <?php
  2. // classes/Hoge.php
  3. class Hoge {
  4.     
  5.     public function call() {
  6.         echo 'Hoge!'.PHP_EOL;
  7.     }
  8.     
  9. }




・classes/SomeOne.php


  1. <?php
  2. // classes/SomeOne.php
  3. class SomeBody {
  4.     
  5.     public function call() {
  6.         echo 'SomeBody!'.PHP_EOL;
  7.     }
  8.     
  9. }




・classes/SpecialPiyo.php


  1. <?php
  2. // classes/SpecialPiyo.php
  3. class Piyo {
  4.     
  5.     public function call() {
  6.         echo 'Piyo!'.PHP_EOL;
  7.     }
  8.     
  9. }





目標は
・これらのソースを「my.phar」というファイルにまとめる。
・require_once('phar://my.phar')するだけで、すべての同梱されているクラスがnewできるようにする





pharの作成



こちらを見ながらpharの作成を思い出してみます。
PHP pharで複数のソースファイルを1つのファイルにまとめる


単純にpharにするだけなら、このようなプログラムを用意すればOKです。

・makePhar.php


  1. <?php
  2. // my.pharという名前でpharを作成
  3. $phar = new Phar('my.phar');
  4. // ファイルを追加
  5. $phar->addFile('classes/Fuga.php');
  6. $phar->addFile('classes/Hoge.php');
  7. $phar->addFile('classes/SomeOne.php');
  8. $phar->addFile('classes/SpecialPiyo.php');




実行時に指定するiniファイル
・phar_make_php.ini


[Phar]

phar.readonly = Off
phar.require_hash = Off




以下のコマンドを実行して、pharを作成します。


$ php -c phar_make_php.ini makePear.php




出来上がったmy.pharが使えるかテスト

・sample.php


  1. <?php
  2. require_once('phar://my.phar/classes/Hoge.php');
  3. $hoge = new Hoge();
  4. $hoge->call();




ここまではうまく行きました。


$ php sample.php
Hoge!








pharをrequireした時に呼び出される関数



こちらが参考になりました。
Pharに自作スタブをセットする

・何も指定しないとデフォルトのスタブが設定される
・setStubで独自の内容に書き換えられる

動作を確認してみます。
先ほどのpharを作成するプログラムを変更。


・makePhar.php


  1. <?php
  2. // my.pharという名前でpharを作成
  3. $phar = new Phar('my.phar');
  4. // ファイルを追加
  5. $phar->addFile('classes/Fuga.php');
  6. $phar->addFile('classes/Hoge.php');
  7. $phar->addFile('classes/SomeOne.php');
  8. $phar->addFile('classes/SpecialPiyo.php');
  9. // 独自のstubを設定
  10. $phar->setStub('<?php
  11. echo "stubが呼び出されました。\n";
  12. __HALT_COMPILER();
  13. ');




pharを再作成


$ php -c phar_make_php.ini makePear.php




pharファイルをrequireしてみます。

・sample.php


  1. <?php
  2. require_once('phar://my.phar');





実行すると、setStubしたソースが実行されます。


$ php sample.php
stubが呼び出されました。




setStubにautoloadの設定プログラムを記載すれば、

・require_once('phar://my.phar')するだけで、すべての同梱されているクラスがnewできるようにする

という目標が達成できますね。


ここで作成したローダーをstubに指定します。
PHP spl_autoload_registerでautoloadを自前で実装


・makePear.php


  1. <?php
  2. // my.pharという名前でpharを作成
  3. $phar = new Phar('my.phar');
  4. // ファイルを追加
  5. $phar->addFile('classes/Fuga.php');
  6. $phar->addFile('classes/Hoge.php');
  7. $phar->addFile('classes/SomeOne.php');
  8. $phar->addFile('classes/SpecialPiyo.php');
  9. // 独自のstubを設定
  10. $phar->setStub('<?php
  11. class MyClassLoader {
  12.     private static $_classMap = null;
  13.     
  14.     public static function initialize() {
  15.         // コストの高い対応表の作成処理
  16.         self::$_classMap = array(
  17.             "Hoge" => "Hoge.php",
  18.             "Fuga" => "Fuga.php",
  19.             "Piyo" => "SpecialPiyo.php",
  20.             "SomeBody" => "SomeOne.php",
  21.         );
  22.     }
  23.     
  24.     // spl_autoload_registerに登録する関数
  25.     public static function loadClass($className) {
  26.         echo "load:".$className.PHP_EOL;
  27.         
  28.         $fileName = self::$_classMap[$className];
  29.         
  30.         // ファイルの存在チェック
  31.         $path = "phar://" . __FILE__ . "/classes/".$fileName;
  32.         if (!is_file($path)) {
  33.             echo "load:".$className." error".PHP_EOL;
  34.             return;
  35.         }
  36.         require_once($path);
  37.     }
  38.     
  39. }
  40. // 事前に対応表を作成
  41. MyClassLoader::initialize();
  42. spl_autoload_register(array("MyClassLoader", "loadClass"));
  43. __HALT_COMPILER();
  44. ');




注意点として、ファイルの存在チェックはpharからのパスで行うことになります。
pharのファイル名は「__FILE__」で取得できるので、


  1. "phar://" . __FILE__ . "/classes/".$fileName;



のようにしてis_fileすればファイルの存在チェックが行えます。
pharを再作成してサンプルプログラムを実行。


・sample.php


  1. <?php
  2. require_once('phar://my.phar');
  3. $hoge = new Hoge();
  4. $hoge->call();




狙いの動作になりました。


$ php sample.php
load:Hoge
Hoge!







stubを別ファイルで準備する



setStubでプログラムのソースを文字列で指定するのは、
コードのハイライトも行われず作成が面倒です。

この部分だけ別ファイルに切り出しましょう。


・stub.php


  1. <?php
  2. class MyClassLoader {
  3.     private static $_classMap = null;
  4.     
  5.     public static function initialize() {
  6.         // コストの高い対応表の作成処理
  7.         self::$_classMap = array(
  8.             "Hoge" => "Hoge.php",
  9.             "Fuga" => "Fuga.php",
  10.             "Piyo" => "SpecialPiyo.php",
  11.             "SomeBody" => "SomeOne.php",
  12.         );
  13.     }
  14.     
  15.     // spl_autoload_registerに登録する関数
  16.     public static function loadClass($className) {
  17.         echo "load:".$className.PHP_EOL;
  18.         
  19.         $fileName = self::$_classMap[$className];
  20.         
  21.         // ファイルの存在チェック
  22.         $path = "phar://" . __FILE__ . "/classes/".$fileName;
  23.         if (!is_file($path)) {
  24.             echo "load:".$className." error".PHP_EOL;
  25.             return;
  26.         }
  27.         require_once($path);
  28.     }
  29.     
  30. }
  31. // 事前に対応表を作成
  32. MyClassLoader::initialize();
  33. spl_autoload_register(array("MyClassLoader", "loadClass"));
  34. __HALT_COMPILER();




pharを作る際は、file_get_contentsで取得してやるとOKです。

・makePear.php


  1. <?php
  2. // my.pharという名前でpharを作成
  3. $phar = new Phar('my.phar');
  4. // ファイルを追加
  5. $phar->addFile('classes/Fuga.php');
  6. $phar->addFile('classes/Hoge.php');
  7. $phar->addFile('classes/SomeOne.php');
  8. $phar->addFile('classes/SpecialPiyo.php');
  9. // 独自のstubを設定
  10. $phar->setStub(file_get_contents('stub.php'));



テーマ:プログラミング - ジャンル:コンピュータ

  1. 2016/08/03(水) 22:19:10|
  2. PHP
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集
前のページ 次のページ