PHPの配列で空文字を1行で無くす

こんな配列があった時。

$arr = array('aaa', '', 'ccc');

foreach使わずに簡単に空文字を無くしたいなーと思って調べたらこんなの>発見。

$arr = array('aaa', '', 'ccc');
$secArr = array_filter($arr, "strlen");

「2番目の引数の関数をコールして、FALSE(0)であればその要素を削除」
なので、これで出来る!素敵!

Mock使った時のMocked method does not exist.

PHPUnitでハマりにハマった内容残しときます。

テスト対象こんな感じ。

    public function modifyBlogEntriesProducts($articleId, $productIds) {
        $columnDao = $this->_getDao('blog/column');
        $products = $columnDao->selectBlogEntriesProductsList(array('blog_entry_id' => $articleId));
        if(!$products) {
            $this->entryBlogEntriesProductsByArray($articleId, $productIds);
        } else {
            $products = $products[0];
            //deleteする配列とinsertする配列を作る。
            $oldProductIds = $this->_getValuesArrayByKeyName($products, 'product_id');

            //insertList = 入力値にあってDBにない商品ID(=新たに追加する商品ID)
            $insertList = array_diff($productIds, $oldProductIds);
            //deleteList = DBにあって入力値にない商品ID(=削除しなければならない商品ID)
            $deleteList = array_diff($oldProductIds, $productIds);
            $this->deleteBlogEntriesProductsByArray($articleId, $deleteList);
            $this->entryBlogEntriesProductsByArray($articleId, $insertList);
        }
    }

誤ったテストコードこんな感じ。

    function testModifyBlogEntriesProducts2() {
        $methods = array('selectBlogEntriesProductsList'
                        , 'deleteBlogEntriesProducts'
                        , 'insertBlogEntriesProducts');
        $columnDao = $this->getMock('Dao_Blog_Sample', $methods);
        $returnObj = array(
                        array(
                            (object)array('product_id' => '789')
                            , (object)array('product_id' => '753')
                         )
                     );
        $columnDao->expects($this->once())
                    ->method('selectBlogEntriesProductsList')
                    ->with($this->equalTo(array('blog_entry_id' => '123')))
                    ->will($this->returnValue($returnObj));
        $columnDao->expects($this->once())
                    ->method('deleteBlogEntriesProducts')
                    ->with($this->equalTo('123'), $this->equalTo('753'));
        $columnDao->expects($this->exactly(2))
                    ->method('insertBlogEntriesProducts');
        $columnDao->expects($this->at(1))
                    ->method('insertBlogEntriesProducts')
                    ->with($this->equalTo('123'), $this->equalTo('456'));
        $columnDao->expects($this->at(2))
                    ->method('insertBlogEntriesProducts')
                    ->with($this->equalTo('123'), $this->equalTo('012'));

        $mocks = array('blog/column' => $columnDao);
        $daoManager = new MockDaoManager();
        $daoManager->setMockInstance($mocks);

        $columnService = $this->_getService('blog/column', $daoManager);
        $columnService->modifyBlogEntriesProducts('123', array('456', '789', '012'));
    }

テスト結果こんな感じ。

phpunit service/Blog/SampleServiceTest.php 
PHPUnit 3.3.17 by Sebastian Bergmann.

....F

Time: 0 seconds

There was 1 failure:

1) testModifyBlogEntriesProducts2(SampleServiceTest)
Expectation failed for method name is equal to <string:insertBlogEntriesProducts> when invoked at sequence index 0.
Mocked method does not exist.
..../PHPUnit/Framework/MockObject/Mock.php(228) : eval()'d code:24

FAILURES!
Tests: 5, Assertions: 5, Failures: 1.

モックが作れない?とかなんとかってエラーメッセージだったのでなんで?とずーーーーっと悩んでた。
google先生に聞いて↓の回答みて気付いた。。
http://stackoverflow.com/questions/3367513/phpunit-mocked-method-does-not-exist-when-using-mock-expectsthis-at

The issue ended up being with how I understood the "at" matcher to work. Also, my example was not verbatim how it is in my unit test. I thought the "at" matcher counter worked on a per query basis, where it really works on a per object instance basis.

Example:

class MyClass {

    public function exists($foo) {
        return false;
    }

    public function find($foo) {
        return $foo;
    }
}
Incorrect:

class MyTest extends PHPUnit_Framework_TestCase
{

    public function testThis()
    {
        $mock = $this->getMock('MyClass');
        $mock->expects($this->at(0))
             ->method('exists')
             ->with($this->equalTo('foo'))
             ->will($this->returnValue(true));

        $mock->expects($this->at(0))
             ->method('find')
             ->with($this->equalTo('foo'))
             ->will($this->returnValue('foo'));

        $mock->expects($this->at(1))
             ->method('exists')
             ->with($this->equalTo('bar'))
             ->will($this->returnValue(false));

        $this->assertTrue($mock->exists("foo"));
        $this->assertEquals('foo', $mock->find('foo'));
        $this->assertFalse($mock->exists("bar"));
    }

}
Correct:

class MyTest extends PHPUnit_Framework_TestCase
{

    public function testThis()
    {
        $mock = $this->getMock('MyClass');
        $mock->expects($this->at(0))
             ->method('exists')
             ->with($this->equalTo('foo'))
             ->will($this->returnValue(true));

        $mock->expects($this->at(1))
             ->method('find')
             ->with($this->equalTo('foo'))
             ->will($this->returnValue('foo'));

        $mock->expects($this->at(2))
             ->method('exists')
             ->with($this->equalTo('bar'))
             ->will($this->returnValue(false));

        $this->assertTrue($mock->exists("foo"));
        $this->assertEquals('foo', $mock->find('foo'));
        $this->assertFalse($mock->exists("bar"));
    }

}

何が違うって
$mock->expects($this->at(1))
の数値が違う。
何?と思ってリファレンス↓
http://www.phpunit.de/manual/3.3/ja/test-doubles.html#test-doubles.mock-objects.tables.matchers
のatのトコ見てみて「あれ?」と。
評価対象全体で何回目に実行されたか、ということなので正解は

    function testModifyBlogEntriesProducts2() {
        $methods = array('selectBlogEntriesProductsList'
                        , 'deleteBlogEntriesProducts'
                        , 'insertBlogEntriesProducts');
        $columnDao = $this->getMock('Dao_Blog_Sample', $methods);
        $returnObj = array(
                        array(
                            (object)array('product_id' => '789')
                            , (object)array('product_id' => '753')
                         )
                     );
        $columnDao->expects($this->once())
                    ->method('selectBlogEntriesProductsList')
                    ->with($this->equalTo(array('blog_entry_id' => '123')))
                    ->will($this->returnValue($returnObj));
        $columnDao->expects($this->once())
                    ->method('deleteBlogEntriesProducts')
                    ->with($this->equalTo('123'), $this->equalTo('753'));
        $columnDao->expects($this->exactly(2))
                    ->method('insertBlogEntriesProducts');
        $columnDao->expects($this->at(2))   //← この前にselectとdeleteがはしってるので評価対象全体では2回目。(0から数えるので。)
                    ->method('insertBlogEntriesProducts')
                    ->with($this->equalTo('123'), $this->equalTo('456'));
        $columnDao->expects($this->at(3))   //← この前にselectとdeleteとinsertがはしってるので評価対象全体では3回目。(0から数えるので。)
                    ->method('insertBlogEntriesProducts')
                    ->with($this->equalTo('123'), $this->equalTo('012'));

        $mocks = array('blog/column' => $columnDao);
        $daoManager = new MockDaoManager();
        $daoManager->setMockInstance($mocks);

        $columnService = $this->_getService('blog/column', $daoManager);
        $columnService->modifyBlogEntriesProducts('123', array('456', '789', '012'));
    }

になります。

たしかにモックの実行順番からしたら1番目はdeleteBlogEntriesProductsだからinsertBlogEntriesProductsのモックは入れられないよーってメッセージは合ってる。
でもここらへんって日本語の詳細な使い方とか全然なくてリファレンスの内容結構スルーしてた…
ちゃんと読まなきゃだめだやっぱ。

preg_replace_callbackとpreg_match_allで文字列置換

初めて使ったんでメモ。
PHPでこんなことが出来るとのこと。

private function _applaySsiInclude($htmlText) { 
    preg_match_all($this->_getIncludePattern(), $htmlText, $includes); 
    $values = $includes[1]; 
    $callback = array($this, '_replaceSsiInclude'); 
    foreach ($values as $value) { 
        $pattern = $this->_getIncludePattern($value); 
        $htmlText = preg_replace_callback($pattern, $callback, $htmlText); 
    } 
    return $htmlText; 
} 

private function _replaceSsiInclude($matches) {
    //「value=」でもらってきた値に応じて置換内容変えたり、とか…そういう感じの処理
    //実際やったことはメソッドの引数にした、みたいな感じ。
    $str = "";
    if($matches[1] === 'なんとか') {
        $str = '何か';
    } else {
        $str = 'なんだろ…';
    }
    return $str;
}

private function _getIncludePattern($value = '[_-\\w]*') {
    return "/<!--\\s*#include\\s+virtual=\"\/sample\.php\?value=({$value})\\s*\"\s*-->/";
}

概要はhtmlの文字列にある「」の部分をphpで書き換えろ!みたいな感じ。

なんでそんなことしてるのかとかそうスルーして…
preg_replace_callbackとpreg_match_allを使ったらステップ数が減った。
最初はforeachでまわして〜とか色々やってコレ+15ステップ位あった。
そしてこっちの方がコードすっきり。

なにより(・∀・)イイ!!と思ったのが正規表現で「()」でくくると、も「()」でくくったトコを個別に配列にして渡してくれるところ。
これに助けられた。

でも相変わらずPHPって気持ち悪い。

ファイル全文検索スクリプト作ってみた

暇だったのでいつも使うコマンドをシェル化してみた。
といってもたいしたことしてないですが。

#!/bin/sh
#パターンだか拡張子だか取得
while getopts "p:" OPT; do
    case $OPT in
        "p" )
            pattern="$OPTARG"
            if [ "${pattern}" = "" ]; then
                echo "option args error!"
                exit 1
            fi;;
        "\?" ) exit 1;;
    esac
done
shift $(($OPTIND - 1))

echo "after get options::"
echo $pattern

# option error
if [ $OPT_ERROR ]; then
    echo '不正なオプションです。確認して下さい。'
    exit 1;
fi

if [ $1 = "" ]; then
    echo "検索文字列が有りません"
    exit 0
fi

if [ "${pattern}" = "" ]; then
    pattern="*.*"
else
    pattern="*.${pattern}"
fi

echo $pattern
echo $1

find . -type f -name "$pattern" | xargs grep -i "$1"

exit 0

改良の余地めっちゃあるな。
まぁいいや。後で修正しよう。

あとif文で二つの条件書くやり方でちょいとハマった。

if [ "${pattern}" = "" ] && [ "${2}" != "" ]; then

基礎が出来てないってことですな。

Viewの基礎

今更ですが、色々調べた時におさらいしたのでついでに。

●Viewとは
クエリによって内容が定義される仮想テーブル。
実データを持たず、定義のみが保持されているのでViewが参照されると定義に従い動的にデータが取得される。
Viewには以下3種類ある。
・標準View
基本的な説明そのまんま。
・インデックス付きView
一意クラスタ化インデックスを作成したView
・分散パーティションView
1台以上のサーバ上でパーティション分散されたテーブルを結合して作成されたView
以下2種類がある。
1)ローカルパーティションView
1サーバ内のSQLServerの同じインスタンス内のテーブルを結合して作られるView
2)分散パーティションView
複数サーバに分散されたテーブルを結合して作成されるView

どこからともなくView使うと速いとか聞いたことがあるようなないような、ですが、嘘だと思います。
正確には一般的にインデックス付きViewを使うと速くなる。(状況によりですが)
一度確認してみましたが、テーブル結合して実行した場合とView参照したクエリを実行した場合とではスピードに差はほとんど見られず。
ついでに実行プランも比較してみましたが同じ実行プランを選択してました。
とんだ勘違いだ。

SQLServerがクエリを処理する時の挙動について

色々調べたのでメモ。

【そもそも的な部分】
SQLServerはクエリ投げられると自分でクエリの実行方法を選択して実行してくれるらしい。
この実行方法を実行プラン、選択してくれる機能をクエリオプティマイザという。
ここら辺の詳細以下。
RDBの心臓部
以下2つに大別される。
●ストレージエンジン
・ディスク間のデータ読み書き
・レコード管理
・同時実行の制御
・ログファイルの保守

●クエリプロセッサ
SQL構文解釈
SQL構文実行方法の決定
・選択プランの実行
・ユーザー、プログラム、ストレージエンジンそれぞれとの対話
・ユーザーからの要求に対する結果取得方法の決定。

【クエリオプティマイザについて】
一般的に主要なクエリオプティマイザは以下2つに大別される。
●構文ベースのクエリオプティマイザ
クエリの厳密な構文及びクエリ内の句の順序によって手続き的なプランを作成する。
クエリが同じなら毎回同じプランを選択するので、例えば以下のような事はやらない。
・レコード数や構成によるプランの変動(クエリが同じなら毎回同じプランをたてる)
・DBの統計調査や保守
●コストベースのクエリオプティマイザ
DBの統計情報やレコード数、構成を元に複数たてた実行プランの各コストを予測。
その中から最適なプランを選択する。
SQLServer6.5、7で実装されている。
例えば以下のようなことはやらない。
・クエリの厳密な構文や句の順序によるプランの選択。
(クエリが同じでもDBの状況によりプランが変動する)
※SQLServer2008にて、スキーマの変更は統計の保守時に参照しない様子。

以下から本題。
SQLServerはクエリ処理時は以下二つのフェーズに大別される。
1.クエリ最適化
クエリオプティマイザが最速の実行プランを選択するフェーズ。
主に以下を選択。
・使用するインデックス(あれば)
・結合の実行順序
・制約(where句等)の適用順序
・最高のパフォーマンスを達成できる可能性の高いアルゴリズム(統計から得られるコスト情報に基づいて選択)

2.クエリ実行
クエリ最適化で選択したプランの実行。

※クエリプロセッサとクエリオプティマイザの違いについて
クエリオプティマイザはSQLステートメントを実行するのに最適な方法の選択を提供するコンポーネント(機能)。
クエリプロセッサはクエリオプティマイザを使用してクエリ最適化を行い、実行する。

SQL Serverオプティマイザと選択する実行プラン の触り。

SQL Server2008 SP1にてクエリが重くてサイトダウン寸前な状況に。
開発・STGででなくて本番でいきなりなったのでそんな状態になって調査が飛んできた。
今回ばかりはManagementStudioの実行プランとMicrosoftTechNetの二つにすがりました。
つかSQL Server自体初めてなので一から勉強。
とにかく実行プランを全部読みといてみた。

【調査1日目】
まず開発・STG環境で実行したプランと本番で実行したプランでクエリが対象にしてたデータ量がめっちゃ違う。
view参照クエリだったんですが、本番では約800万と約60万のMergeJoinが開発・STGでは約150と約30。
そもそもSTGのデータ量が本番と違うのなんでって事を突っ込みたい。
なのでまず検証環境を用意。

【調査2日目】
本番とほぼ同じデータ量の環境で再度実行。
でも再現せず。
むしろ開発・STGで実行されてる実行プランと同じプラン選択してる。
おかしいなと思って再度本番でクエリ実行したらこちらも再現しない。
開発・STGで実行されてる実行プランと同じプラン選択してる。
キャッシュか何か?と思ったけど、前日本番で何度実行しても遅い実行プラン選択してたからそんなわけない。
昨日から何か変わったのか?と思い返してはた、と。
そういえばインフラの人からメールでSQL Server2008 SP2にするってメールが来てた。

●以下予想。。
ワケがわからなくて詳しい人に助けを求めてたてた予想。
再起動して速い実行プランが選択されたって事は、そもそものデータ量で実行プランの選択が左右されていたわけではなく、別の要因で実行プランが選択されていた、と思う。
その別の要因がサーバ再起動の影響を受けるモノであるとすると、クエリオプティマイザが参照していた統計情報では?
(SP2に上げた事で影響を受けたモノかも?そうなるとSP1からの変更点はなんだろう?)
(そもそも統計情報はサーバー再起動のタイミングでクリアされるものなのだろうか?)
統計情報がクリアされてオプティマイザが新しく実行プランを組み立てて速いプランを選択したのであれば、本番で遅い実行プランを選択していたのはデータ量が違うからではなくて古い統計情報を参照して選択したプランだったから?
結構頻繁にカラム追加等がテーブルにされているのだが、スキーマまでは統計情報の保守に含まれていないため、古い統計情報と新しい統計情報の違いが生まれてしまったのではないだろうか?
そうするとオプティマイザが維持する統計情報はいつ何をとってきているのだろうか?

興味深い内容だったので流れ残しときます。
予想少しでも裏付けられるように引き続き調査中。