JavaScript : コールバックがないならDOMの変更を監視する

月花です。

いろんなJavaScriptなりjQueryなりのライブラリを入れていると困るのが、ライブラリの動作にコールバックやフックがないことですよね。
なにか要素が追加されるとき、その追加された要素にCSSを適用したいがクラスもなければIDもない、さらにコールバックも無ければお手上げになってしまいます。

そこで、MutationObserver によるDOM変更の監視で解決しました。

たとえば、下記のように、DOMが追加や削除されるが、コールバックもなく追加されたものかどうか判断もできない場合です。
下記の場合ですと自作関数での追加なのでそこを編集してやればいいのですが、ライブラリを利用する場合、これに対応するコードはミニマム化されているので読んだりいじったりは困難です。

See the Pen dqywjm by gekka (@gekka9) on CodePen.

このような場合に追加や削除されたことを掴み、追加された要素やその親要素、兄弟要素に対してなにか処理をしてやりたい場合、MutationObserverによってDOMを監視することが可能です。

もくじ

  • MutationObserver
    • MutationObserver
    • MutationCallback
    • MutationRecord
    • MutationObserverInit
    • 簡単な実装例
  • MutationRecord
    • 追加されたノードに対して、さらに変更を加える
    • 何が起こったかで分岐してみる
    • 監視を停止してみる
  • MutationObserverInit
    • children と descendants の違い
  • 対応ブラウザと代替手段
    • MutationObserver の IE11対応 (※ 2018/08/22 追記)
      • Array.from()
    • Mutation events (非推奨)
    • なぜ Mutation events は非推奨なのか
    • できる限り Mutation events を使わないようにする
  • おわりに

MutationObserver

基本的には、下記のドキュメントにある。
developer.mozilla.org

が、あまり深く書いてくれていないので、下記も併読してもう少し深いところに突っ込んでいく。
DOM Standard


一旦、とりあえず最低限動くところまでを見ていこう。

MutationObserver

この子は、ある指定されたオブジェクトに対してそれ自身やその子孫の監視をする処理の本体で、監視開始や監視終了の関数を持っている。

関数名 引数 -
コンストラク function callback 変更を検知した際に呼び出すコールバック関数を渡す
observe() Node target, MutationObserverInit options 監視を開始する。監視対象と、どのような変更を検知するかの設定を渡す
disconnect() - 監視を停止する
takeRecords() - コールバック関数に渡される変更を返し、そのキューを空にする

MutationCallback

変更が検知されたときに呼び出される関数。

引数 -
MutationRecord 変更があったノードなど、検知した変更に関するデータオブジェクト
MutationObserver 変更を検知した MutationObserverインスタンス自身

MutationRecord

検知後、コールバック関数に渡される、検知した変更に関するデータオブジェクト。
変更された要素自身や変更箇所の隣人要素などが入っている。

MutationObserverInit

observe() にオプションとして渡す、監視対象の設定オブジェクト。
最低限、下記のうち少なくとも1つを明示的に設定する必要がある。

項目名 -
childList boolean trueにすると子ノードを監視対象とする
attributes boolean trueにすると、対象の属性値の変更を監視対象とする
characterData boolean trueにすると、対象の文字データの変更を監視対象とする

ここで、ノードとは簡単に言うとDOM構造の一部のことで、子ノードとは対象の要素の中に入れ子となっているDOM要素を指す。
今回の例でいくと、

<div>
  <ul id="target">
    <li>A</li>
    <li>B</li>
    <li>C</li>
  </ul>
</div>

ulタグを対象とした場合、childList をtrueにすると、ul・liの変更や、新たな子タグの追加を監視する。
一方で、characterData を true にした場合、もともと ul は li しか持っておらず直下にテキストは存在しないため、ノードは持っているがテキストデータは持っていないことになるので、テキストの追加を監視することになる(もちろん、追加した後に変更や削除があった場合、それらも検知される)。
もし childList と characterData を true にした場合は、DOM構造自体の変更だけでなく、ul 直下のテキストも検知することができる(そこにテキストを追加してはいけないが)。

簡単な実装例

上記を踏まえて、一度動かしてみよう。

See the Pen NLWVwR by gekka (@gekka9) on CodePen.

この例では、ulタグの childList を監視する。動作は li の追加と削除しかないため、その2パターンの変更を検知した際に、検知した回数をカウントするようにした。
また、コールバック関数に渡される MutationRecord は見ず、どのような変更であっても同じ動作をするし、disconnect() を呼んでいないため監視しっぱなしである。

次は、コールバック関数で込み入った処理を行うため、MutationRecord を詳しくみていく。

MutationRecord

項目名
type DOMString どの種類の検知に引っかかったかを保持している。例えば、childList の関しに引っかかったなら"childList"、attributesの検知なら"attributes"というように持つため、分岐処理をすることができる
target Node 変更があったノードそのもの。追加・削除されたノードや、変更されたノードを保持している
addedNodes NodeList 追加されたノードのリスト。今回は1つずつの追加のため、配列長は1となる
removedNodes NodeList 削除されたノードのリスト。今回は1つずつの削除のため、配列長は1となる
previousSibling Node 追加/削除があった要素の、1つ前のお隣さんノード。なければnullとなる
nextSibling Node 追加/削除があった要素の、1つ後のお隣さんノード。なければnullとなる
attributeName DOMString 変更があった要素の属性名を返す
attributeNamespace DOMString 変更があった要素の属性の名前空間を返す
oldValue DOMString attributes や characterData の変更の場合、変更前の値を保持している。childList の場合は null になる。使うには MutationObserverInit に設定が必要(後述)

追加されたノードに対して、さらに変更を加える

addedNodes を利用して、追加された要素に対して、追加後にさらに要素を変更してみる。

See the Pen KxKLGv by gekka (@gekka9) on CodePen.

addedNodes はノードのリストなので、配列長は1であるが、forEach を使って処理をしている。
極めて軽い処理なので、一瞬で動作が終わってしまうため、書き換え後のものが挿入されているように見えるが、実は挿入後に書き換えられている。

何が起こったかで分岐してみる

次に、attributes の監視に引っかかるような動作を追加し、分岐をしてみる。

See the Pen WgNqXJ by gekka (@gekka9) on CodePen.


type が childList の場合は、addedNodes や removedNodes の配列長を見ることで追加か削除が判別できる。
attributes では、古い値は oldValue に入っており、新しい値は target と attributeName を使って変更後に取得することができる。
characterData の場合も処理の基礎としては attributes と同様になる。

監視を停止してみる

何かの変更を検知して、一回だけ処理をすればいい場合は、コールバック関数の最後で監視を停止してやることができる。

See the Pen gdbYGO by gekka (@gekka9) on CodePen.


少し前の、変更するたびカウントを増やしていく例で、最初の一回のみで止めるようにした。
コールバック関数には、第二引数に MutationObserver のインスタンスそのものを受け取るため、これの disconnect() を呼んでやることで、コールバック関数内で自分自身を止めることができる。
もちろん、MutationObserver のインスタンスを持っている変数が使える場所である限り、任意の場所で止めることができる。

MutationObserverInit

前章までは、検知対象を絞り込むことに使っていたこのオプション配列だが、もう少し踏み込んだ設定もできる。

項目名
childList boolean 監視対象要素の子ノードを見るかどうか。以下、true で見る
attributes boolean 監視対象要素の属性値を見るかどうか
characterData boolean 監視対象要素のテキストを見るかどうか
subtree boolean 監視対象要素の子孫を見るかどうか。childList の場合は、あくまでも子への追加や削除を見ており、孫世代の要素が変わったとしても検知されない。また、attributes とcharacterData の場合は元々監視対象そのものの値の変更しか見ていない。これを true にしてやると、 childList は孫以降への変更も見るようになり、 attributes の場合は子以降の属性の変更も見るようになる。
attributeOldValue boolean 監視対象の変更前の属性値を MutationRecord に保持するかどうか。前章で使っている。
characterDataOldValue boolean 監視対象の変更前のテキストを MutationRecord に保持するかどうか
attributeFilter DOMStringの配列 attributes が true の場合有効で、どの属性の値の変更を検知するかのホワイトリスト

前章までで実際のコードを掲載していないのは characterData だが、処理の流れとしては attributes と同様なので、省略する。

children と descendants の違い

先程の例で、 subtree を true にせず、監視対象を元々の ul からその親の div に変えてみた。
subtree が孫を見るものかどうかの是非であるのが間違いないなら、これでは検知されないはずだ。

See the Pen zJYgyw by gekka (@gekka9) on CodePen.

一つ前の例のようなメッセージが出ていない。検知されなかったようだ。

ここで、
DOM Standard
には、

childList
Set to true if mutations to target’s children are to be observed.

subtree
Set to true if mutations to not just target, but also target’s descendants are to be observed.

とある。

children とあるのだから、 childList の場合は、subtree を true にしなくても検知するのでは、と考えていた。

children について、

An object that participates in a tree has a parent, which is either null or an object, and has children, which is an ordered set of objects. An object A whose parent is object B is a child of B.

descendants について、

An object A is called a descendant of an object B, if either A is a child of B or A is a child of an object C that is a descendant of B.

🤔?

ひとまず、 MutationObserver 自体の能力については一通り確認することができた。

対応ブラウザと代替手段

Can I use... Support tables for HTML5, CSS3, etc
上記を見ると、FireFoxChrome は早いこと対応しているが、IE は11以降からだった。

では、MutationObserver が使えない場合はどのようにすればよいだろうか。

MutationObserver の IE11対応 (※ 2018/08/22 追記)

IE11では、MutationObserver はサポートされているが、そこで扱うことになる NodeList の forEach() がサポートされていない。
つまり、これらの処理で渡される NodeList に対しては、IE11が扱えるループをしてやらねばならない。
そもそも、NodeList はあくまでも配列風 ( array-like ) オブジェクトであり、 Array を継承していない。
JavaScript における forEach は、制御構文ではなく Array.prototype に実装されている関数に過ぎず、これを継承していないオブジェクトでは呼び出すことができない。

Array.from()

様々なやり方があるが、ここでは単純な方法として、forEach の前で NodeList をArray に変換することにする。
そこで、Array.from() を使う。
単純な例として、第一引数のみを使い、ただ変換するだけの関数として扱う例を載せる。

第一引数では、array-likeオブジェクトもしくは iterableオブジェクトを受け取る。
ここで、NodeList は先述のように array-likeオブジェクトなので、単純に渡すだけで Arrayオブジェクトにしてくれる。
Array になってしまえば Array.prototype が実装されたオブジェクトとなるので、forEach を呼び出すことができる。

observer = new MutationObserver(function(mutations, observer){
    mutations.forEach( function(mutation){
        var arrayNodes = Array.from(mutation.addedNodes);
        arrayNodes.forEach( function(node){
            // 処理
        });
    });
});

MutationObserver の代替手段 Mutation events (非推奨)

Mutation events というものがある。

  • DOMAttrModified
  • DOMAttributeNameChanged
  • DOMCharacterDataModified
  • DOMElementNameChanged
  • DOMNodeInserted
  • DOMNodeInsertedIntoDocument
  • DOMNodeRemoved
  • DOMNodeRemovedFromDocument
  • DOMSubtreeModified

これらは、イベントリスナとして扱うことができ、 addEventListener の第一引数として渡すことで、各々のイベントに応じて発火する。

この中では、

  • DOMNodeInserted
  • DOMNodeRemoved
  • DOMAttrModified
  • DOMCharacterDataModified

これらが、それぞれ先程のコードに該当するだろう。

これらを使って、一番最初の単純な例を作ってみる。

See the Pen WgbebQ by gekka (@gekka9) on CodePen.


なぜ Mutation events は非推奨なのか

それは、これは同期的な処理だからである。
同期的にDOMを監視し、なにか処理を入れるということは他の動作に影響を及ぼしてモッサリしてしまう、ということである。
このため、非同期的な処理である MutationObserver を代わりに使うことが推奨されている。

できる限り Mutation events を使わないようにする

非推奨だからといって、絶対使わないというわけにもいかないのが現実。
でもできるだけ使いたくない。

であれば、 MutationObserver が使えればそれを、使えなければ Mutation events を使うようにしてやればよい。

See the Pen aazomY by gekka (@gekka9) on CodePen.


このように、 typeof MutationObserver が undefined かどうかで、そのブラウザが MutationObserver を実装しているかどうかを判別することができる。

おわりに

MutationObserver は非常に強力な手段であることがわかったが、実際にシステムに組み込んでみると思いの外発火する。
各種統計用の JavaScriptライブラリや reCAPTCHA 、lazyload など、様々な変更がひっきりなしに発火していることに気づく。
やむを得ず body の孫まで監視しているような状態では、とんでもない数になると思われる。
こうした変更すべてを検知していては、さすがに今の優秀なブラウザでも重くなりそうである。

コールバックが実装されていたり、追加要素にはクラスを振ってくれるライブラリをできるだけ使うことを前提に、最終手段として使っていきたい。
使う場合も、できれば subtree は使わず、終わったら disconnect() を呼び出すようにしたいところ。

JavaScript : 複雑なbindのリーディング

月花です。

今回は、複雑なJavaScriptを見つけて、リーディングに時間がかかったので、その仕組みを紹介したいと思います。
単純な修正なのに、2時間くらいかけて読んで30分くらいかけて修正することになったので、次修正するとき忘れないように書いておきます。

バインド時にイベントハンドラの動的生成をして、event や this をさらにその先へ引き回す、というコードです。
主に以下の内容についての解説です。

  • argumentsオブジェクト
  • apply()
  • .on()
$(function() {
    $(document).on("click", ".targetA", returnFunc(hoge));
    $(document).on("click", ".targetB", returnFunc(fuga));
});

var returnFunc = function () {
    var func = arguments[0];
    
    return function applyImpl () {
        return func.apply(this, new Array(2, 3));
    };
};

var hoge = function(foo, bar){
    result = foo + bar;
    alert(result);
}
var fuga = function(foo, bar){
    result = foo * bar;
    alert(result);
}

上記のコードはどのように動くだろうか。

See the Pen GBbNgN by gekka (@gekka9) on CodePen.

というように動作する。

今回はこのコードを読んでいく。
説明のため、各所に番号を振ったものを以下に掲載する

$(function() {
    $(document).on("click", ".targetA", returnFunc(hoge)); // [1] 関数を収めた変数を引数として渡す
    $(document).on("click", ".targetB", returnFunc(fuga));
});
var returnFunc = function () {
    var func = arguments[0]; // [2] argumentsによる引数の受け取り
    
    return function handler () {
        return func.apply(this, new Array(2, 3)); // [3] apply()による関数の呼び出し
    };
};

var hoge = function(foo, bar){
    result = foo + bar;
    alert(result);
}
var fuga = function(foo, bar){
    result = foo * bar;
    alert(result);
}

目的

今回のコードリーディングの目的は、このクリックイベントの処理内で新たに event と this を使った追加の処理を挿入することである。
ざっくり読んでもわからなかったので、深く読んでみることになった。

もくじ

  • [1] 関数を収めた変数を引数として渡す
  • [2] argumentsによる引数の受け取り
  • [3] apply()による関数の呼び出し

[1] 関数を収めた変数を引数として渡す

$(function() {
    $(document).on("click", ".targetA", returnFunc(hoge)); // [1] 関数を収めた変数を引数として渡す
});

.on() の引数は以下のようになる。

第1引数 イベント種別 今回はclickなのでクリックしたときに発火する。
第2引数 セレクタ(オプション) 今回は targetA というクラスを指定する。※今回はイベントデリゲートの話はしません。
第3引数 データ (オプション) イベントハンドラにデータを渡すことができる。今回は省略されている。
第4引数 ハンドラ イベントハンドラを設定する。

したがって、ここで行っているのは、targetA というクラスを持つ要素に処理をバインドする処理である。

コード中のイベントハンドラとして記述している部分に着目すると、何やら関数を呼び出しているようだ。
この関数の定義を読んでみよう。

var returnFunc = function () {
    var func = arguments[0]; // ← ここはあとで解説する
    
    return function handler () {
        return func.apply(this, new Array(2, 3)); // ← ここもあとで解説する
    };
};

着目してほしいのは4行目のreturn文で、この関数は関数を返すものであることがわかる。
この関数によって返却されてきた関数こそがハンドラとしてバインドされることになる。
最終的に、バインドされるのは handler() 。

つまり、このクリックイベントを取得したい場合や、クリックされた要素自体を $(this) として掴むには、ここを以下のように修正すればよいことになる。

$(function() {
    $(document).on("click", ".targetA", returnFunc(hoge));
});
var returnFunc = function () {
    var func = arguments[0];
    
    return function handler (event) { // ← イベントを引数として取得
        piyo = $(this); // ← クリックされた要素自体の取得
        return func.apply(this, new Array(2, 3)); 
    };
};

なんと、$(this) が関数を生成する関数の、しかもreturn文の中に入ってしまった
これは良くない。なにか他の書き方ができないか、もっと読んでみよう。

ちなみに、よくある簡素な書き方では、

$(function() {
    $(document).on("click", ".targetA",function(event){
        piyo = $(this);
    });
});

というように、ネストの中で記述することができる。

[2] argumentsによる引数の受け取り

$(function() {
    $(document).on("click", ".targetA", returnFunc(hoge));
});
var returnFunc = function () {
    var func = arguments[0]; // [2] argumentsによる引数の受け取り
    
    return function handler () {
        return func.apply(this, new Array(2, 3)); // ← ここはあとで解説する
    };
};

これは関数を定義して変数に収める記述だが、よく見ると関数定義に引数がないにも関わらず呼び出し時に変数を渡している。
そしてこれは、渡された変数をちゃんと使っている。

argumentsオブジェクト

JavaScriptの関数では、argumentsというローカル変数が提供される。
この中には様々なデータが入っているが、単純に連番の添字を見た場合には引数が入っている。
argumentsの中には定義した引数の数に関わらず(たとえゼロでも)呼びだれたときに渡されたものをそのまま保持している。
なのでたとえば、

for (  var i = 0;  i < arguments.length;  i++  ) {
    console.log ( arguments[ i ] );
}

というようにループすることができる。
おそらくこれが本来の使い方で、決して引数を定義する必要がないわけではない

arguments の中には他にも、呼び出し元関数を収めた arguments.caller や 呼び出し先関数(つまり arguments がローカル変数として所属する関数自身)を収めた arguments.callee などがあった
現在は caller は削除、 callee は非推奨となっている。
callee は無名関数での再帰処理に使うことができた。本来名前がないから再帰ができない無名関数だが、arguments.callee にはきちんと収まるため、arguments.callee を擬似的な識別子として扱えることを利用できた。

しかし、この arguments はスマートな処理追加の解法とは関係なさそうだ。

[3] apply()による関数の呼び出し

$(function() {
    $(document).on("click", ".targetA", returnFunc(hoge));
    $(document).on("click", ".targetB", returnFunc(fuga));
});
var returnFunc = function () {
    var func = arguments[0];
    
    return function handler () {
        return func.apply(this, new Array(2, 3)); // [3] apply()による関数の呼び出し
    };
};

var hoge = function(foo, bar){
    result = foo + bar;
    alert(result);
}
var fuga = function(foo, bar){
    result = foo * bar;
    alert(result);
}

JavaScript での関数の呼び出しは、 単純に関数名に引数を渡すように書くことで呼び出すが、それよりアドバンスな呼び出しをする関数が提供されている。

apply(thisArg, argsArray)

apply関数は、JavaScriptの関数オブジェクトが持っている関数なので、関数なら基本的に使用できる。

$(function() {
    $(document).on("click", ".targetA", returnFunc(hoge)); // hoge() を渡している
});
var returnFunc = function () {
    var func = arguments[0]; // hoge() を受け取っている
    
    return function handler () {
        return func.apply(this, new Array(2, 3)); // hoge() をapply() を使って呼び出す
    };
};

// このコードでの呼び出し先関数 hoge() の定義
var hoge = function(foo, bar){
    result = foo + bar;
    alert(result);
}
第1引数 this ここで渡したオブジェクトが呼び出し先の関数内で this を使って参照するオブジェクトとなる。
第2引数 引数の配列 呼び出し先関数の引数を収めた配列

これは、たとえばすでに配列になっているものを分解して引数として渡す手間を省いたり、既存の関数を非破壊的に上書きすることに使われる。

今回の記述を追いかけてみると、[1] の章で書いたとおり、handler() 関数はハンドラなので、 this にはクリックされた要素自体が入っていることが想定されるものである。
なので、handler() にとっての this をapply() を使って呼び出し先に渡してやることで、呼び出し先にとっての this が handler() にとっての this と同じものになる。
第2引数として渡した配列長2の配列はそのまま呼び出し先関数の第1引数・第2引数となり、キモとなる演算(ここでは加算や乗算)が行われる。

これらを踏まえると、

$(function() {
    $(document).on("click", ".targetA", returnFunc(hoge));
});
var returnFunc = function () {
    var func = arguments[0];
    
    return function handler (event) { 
        piyo = $(this); // ← これと
        return func.apply(this, new Array(2, 3)); 
    };
};
var hoge = function(foo, bar){
    piyo = $(this);// ← これ
    result = foo + bar;
    alert(result);
}

このコメントアウトした2行の $(this) は等価になる。
なので、[1] の章 で書いたように $(this) を handler() の中で使うように書くよりもむしろ handler() では書かずに、呼び出し先変数で書くべきだろう。

しかし、この修正では残念ながら event を呼び出し先変数に渡すことはできなかった。
正確には、 apply() の引数として追加してやればよいが、そのためには既存の関数に引数の定義を追加しなくてはならない。
解決したのは this を使う処理部分だけだが、 event を使った処理は大したことない上に仮に呼ばれすぎでも問題はない処理なので、これで良いこととしてしまった。

まとめ

結局このコードはなんだったのか

$(function() {
    // returnFunc() に hoge() や fuga() という関数を渡す
    // returnFunc() は渡された引数を使ってハンドラを動的に生成し、返す
    // 返ってきた関数はハンドラとしてバインドされ
    $(document).on("click", ".targetA", returnFunc(hoge));
    $(document).on("click", ".targetB", returnFunc(fuga));
});

// ハンドラを作成して返す関数の定義
var returnFunc = function () {
    // argumentsによる引数(中身は関数)の受け取り
    var func = arguments[0]; 
    
    // ハンドラ関数を生成して返却
    return function handler () {
        // apply() を使って呼び出すことで、呼び出し先に this を引き回す
        return func.apply(this, new Array(2, 3));
    };
};

// キモとなる演算を行う関数の定義
var hoge = function(foo, bar){
    result = foo + bar;
    alert(result);
}
var fuga = function(foo, bar){
    result = foo * bar;
    alert(result);
}

とこのように、バインド時にイベントハンドラの動的生成をして event や this をさらにその先へ引き回す、というコードであることがわかった。

結局私がしたい修正は、 event や this を使って追加の処理をしたかっただけだったので、[3] の章のように呼び出し先関数を修正することで対応した。

コードリーディングを終えて

確かに動的にハンドラを生成するためだったり重複コードをまとめるのにはこの手法は有効であると感じたが、読むのに時間がかかってしまった。

なかでも this の引き回しは修正の難易度が高かった。
最初に修正したのは handler() の中だったが、異なる呼び出し先をこれから呼ぼうという場所で書くは憚られ、結果として呼び出し先変数でも書けることがわかったので良かったが、気づいていなければかき混ぜてしまう修正になりかねなかった。

確かにスマートな書き方だがメンテナンスが楽じゃないという点では微妙で、私がPMならレビューで弾く、そんなコードだった。

PHP : エクスクラメーションマーク2つによる二重否定

月花です。
ちょっとコードリーディングをしていて、なんだこれって思ったのでまとめてみます。

<?php
$hoge = !!$foo;

というように、PHPでexclamation markによる二重否定( double not ) を行うコードが見つかった。
最初は double not だと思っていたのだが、実はどちらかというと double not ではなく twice not で、「二重否定」というよりは「否定の二重適用」というような挙動だった。

毎度のことながらここまでは検索用の文なので、以降は日本人に戻ってカタカナで書きます。

  • エクスクラメーションマーク2つによる二重否定
  • なぜ二重否定でboolean型へのキャストとなるのか
  • PHPでの比較演算
  • 他のboolean型への変換手法
  • 結論:どれ使ったら良いの

エクスクラメーションマーク2つによる二重否定

<?php

$hoge = !!$foo;

というようなコードを見つけた。
2文字の演算子なので、一見比較演算子か論理演算子かと思ったのだが、コードの文脈上違うようだ。
また、代入を行っているので、なんらかのキャストのようだと思ったのだが、結論から言うと

<?php

$hoge = (bool)$foo;

と同義のようだ。

なぜ二重否定でboolean型へのキャストとなるのか

二重否定は、このように置き換えられる。

<?php

$hoge = !( !$foo );

否定を行う論理演算子を一度適用し、さらにもう一度否定している流れである。
PHPではエクスクラメーションマークによる否定ではboolean型への暗黙的なキャストが行われ、boolean型にキャストされたものに対してもう一度否定をしてやることで、キャストしつつ元々の結果に反転させることで、遠回りにキャストを実現している。
冗長に書き直すと、

<?php

$hoge = !$foo;    // 暗黙的なキャストと論理否定
$hoge = !$hoge; // 2回目の論理否定

となる。

これは、

<?php

$hoge =1;
var_dump($hoge);              // int(1)
var_dump($hoge . '');         // string(1) "1"

というような変換と似た、演算での暗黙変換を利用するキャストの手法である。

PHPでの比較演算

では、2つの変数の比較結果のbooleanを変数に押し込めたい時、どうすればよいだろうか。
PHPでは比較演算の結果は必ず返却されるため、if文の条件式中で使うだけではなく変数に代入することもできる。
つまり、

<?php

if( $hoge == $piyo ){
    $result1 = true;
}else{
    $result1 = false;
}

$result2 = ($hoge == $piyo) ? true : false;

$result3 = ($hoge == $piyo);

これらはすべて等価である。

他のboolean型への変換手法

二重否定によるキャストは、単純に2つ書いただけでそんなことが可能であると知らなければ読めないので、いささか可読性に欠けると言える。
では、他にスマートな書き方はないだろうか。
以下は全て二重否定と等価の書き方である。

<?php

// 二重否定
$hoge = !!$foo;

// 明示的キャスト
$hoge = (bool)$foo;

// 変換関数
$hoge = boolval( $foo );

// ifの利用
if( $foo ){
    $hoge = true;
}else{
    $hoge = false;
}
$hoge =$foo ? true : false;

// trueとの比較 (falseとの比較もできる)
if( $foo == true ){
    $hoge = true;
}else{
    $hoge = false;
}
$hoge = ($foo == true) ? true : false;
$hoge = ($foo == true);

// trueとの論理積
$hoge = ($foo and true);
$hoge = ($foo && true);

もっとありそうだけど、思いつくのはこれくらい。
結局読みやすくてスマートなのは、boolval() 、 (bool)、trueとの比較、 三項演算子かな、と思う。
論理積はスマートだけど、他に良い方法あるし、これをあえて選ばなくても、というくらいかな。

リテラルへの適用

ちなみに、$fooの部分はリテラルであってもよい。

<?php

$hoge = !!'0';
$hoge = !!0;
$hoge = !!(1+2+3);

ここで、ちょっとガチャガチャとやってみよう。
PHPでは、0以外の数字、及び、'0'以外または''以外の文字列リテラルはtrueとなる。
これらを . 演算子で繋ぐことによって、整数リテラルを文字列リテラルに変換することができる。
では、これと二重否定を絡めてみよう。

<?php

$foo = 1;                     // 整数リテラル
$hoge = !!( $foo.'' );  // 整数リテラルを文字列リテラルにして、それを二重否定してboolean
var_dump( $hoge );  // bool(true)

この通りたった3行で型がゴシャゴシャになるので、絶対にやるなよ!!!!!!
そんなことしねえと思ったそこのお前!!!!!
長いこと煮詰まったコードではこれらが分散して、たとえば$fooの初期化が120行目、リテラル変換が140行目、二重否定が170行目にそれぞれあったりするんだからな!!!!その間に煩雑なコードが追加されていったりするから$fooについて追うまで気づかないんだ!!!!!
絶対にこの手の成長をする種を蒔くな!!!!!!!

結論:どれ使ったら良いの

これはチームが採用している規約などに応じて変えるべきだとは思うが、規定がないのであれば、ボトムの実力に合わせるべきである。
教科書的なif文しか理解されそうもない、という環境ではやはり

<?php

if( $foo == true ){
    $hoge = true;
}else{
    $hoge = false;
}

が強いと思う。
これは教科書みたいな技術書にも最初の方に書いてあるようなif文でしかないので。

それ以上の実力があるなら、キャストかboolval()の利用。

演算結果を変数に押し込めることを知っているレベルなら、三項演算子論理積が強いと思う。
見た目にもtrueとの演算であることがすぐわかり、そもそもtrueとfalseの文字列が出てくるのでgrepや目検にも強いという有用性がある。
もしくは、何らかの定数が導入されて比較対象を変える場合にも、明示的に比較演算子やtrueなどbooleanリテラルがコードに現れているとわかりやすい。
二重否定は、それがシンタックスエラーではなく成立するということを知らない限り使えない局所的な知識なので、推奨はできない。

PostgreSQL : 9.4より古いなら、マテビューをFROM句に入れるビューは良くない

月花です。

PostgreSQL の MATERIALIZED VIEW を REFRESH すると、 その MATERIALIZED VIEW を FROM句とする VIEW がどうなってしまうのか、
ということをよく知らずに、ばかなことをしてしまったなー、という記事です。
これは検索ワードのための文なので、ここから日本人になります。

  • なにをしたのか
  • 前提
    • マテビュー
    • ビュー
  • マテビューのリフレッシュとロック
    • リフレッシュ時のロック対策
    • あるビューのFROM句にいるマテビューをリネームするとどうなるのか
  • 実際に何が起こっていたか
  • 解決したかった
  • じゃあどうすればよかったのか

なにをしたのか

あるマテビューを作り、そのマテビューをFROM句とするビューを作った。
そこで、ロジックで分岐させてどのビューを読むのか、というようなことをしていた。

使っていたpostgresのバージョンが9.4より古かったので、マテビューのリフレッシュ時は ACCESS EXCLUSIVE LOCK という、SELECTすら許さない強いロックがかかる。
それを嫌って、同じマテビューを2つ用意して交互にリフレッシュとリネームをしながら、常にリフレッシュしていないマテビューが存在するようにしていた。

そう、マテビューをFROM句とするビューがあるとき、そのマテビューをリネームするとビューはどうなるのか。実装時の私は、全く気づいていなかったのである。

前提

ここではサンプルとして、下記のような構成とする。

マテビュー

商品テーブルで、名前とカテゴリ、値段を持っている。
この話で重要なのはカテゴリだけなので、他はてきとう。
sample_products

product_id name category_id price
1 apple 1 100
2 beef 3 110
3 carrot 2 120
4 durian 1 130
5 eggplant 2 140
6 fish 3 150
7 grape 1 160

ビュー

商品マテビューのカテゴリごとにビューが作ってある。
なんでだろうね?インデックス貼っとけば速いしviewじゃなくていいじゃんなんだけど、サンプルってことで許して欲しい。

CREATE VIEW sample_view_fluits AS
SELECT * FROM sample_products WHERE category_id=1;

CREATE VIEW sample_view_vegetables AS
SELECT * FROM sample_products WHERE category_id=2;

CREATE VIEW sample_view_meats AS
SELECT * FROM sample_products WHERE category_id=3;

マテビューのリフレッシュとロック

postgresのバージョンが9.4よりも古い場合、マテビューをリフレッシュする際には、そのマテビューにACCESS EXCLUSIVE LOCKという強い排他ロックがかかる。
つまり、リフレッシュ中は他からのSELECTを待たせることになる。
大した事ないマテビューならよいが、10秒もユーザは待ってくれないので、対策が必要となる。

リフレッシュ時のロック対策

いろいろやり方はあるのだが、私がとったのは、2つのマテビューを交互にリフレッシュしていく方式。
まず全く同じ構成のマテビューを2つ用意し、片方をリザーブとする。
その上で、リフレッシュの際にはリザーブだけ更新し、リザーブとそうでない方をリネームで入れ替える。
すると、ビジネスロジックから発行されるクエリは常にリザーブでない方をSELECTするため、ロックの対象にはならない、という方式である。

これは具体的には、下記のようなクエリになる。

BEGIN;

REFRESH MATERIALIZED VIEW sample_products_reserve;

ALTER MATERIALIZED VIEW sample_products RENAME TO sample_products_temp;
ALTER MATERIALIZED VIEW sample_products_reserve RENAME TO sample_products;
ALTER MATERIALIZED VIEW sample_products_temp RENAME TO sample_products_reserve;

COMMIT;

このときの流れは、元々のマテビューのバージョンが1で、リフレッシュすると1増えて2になると表現すると、下記のようになる

sample_products_reserve のリフレッシュ( 1 -> 2 )

sample_products( 1 ) -リネーム-> sample_products_temp( 1 )
sample_products_reserve( 2 ) -リネーム-> sample_products( 2 )
sample_products_temp( 1 ) -リネーム-> sample_products_reserve( 1 )

そして結果的に、バージョンが1つ上がった sample_products と元のバージョンのままの sample_products_reserve が残る。
tempは交代のためのただのプレースホルダなので、処理が終わればもう存在しない。

次に同じ処理が走ると、下記のようになる。

sample_products_reserve のリフレッシュ( 1 -> 3 )

sample_products( 2 ) -リネーム-> sample_products_temp( 2 )
sample_products_reserve( 3 ) -リネーム-> sample_products( 3 )
sample_products_temp( 2 ) -リネーム-> sample_products_reserve( 2 )

つまり、常に最新バージョンの sample_products と ひとつ古いバージョンの sample_products_reserve が存在するサイクルとなる。
これらをトランザクションに包み込んでしまえば、うまくいくはずだった。

あるビューのFROM句にいるマテビューをリネームするとどうなるのか

あたりまえの話だけど、ひも付けが変わる。

たとえば、前章で紹介したビューの、

CREATE VIEW sample_view_fluits AS
SELECT * FROM sample_products WHERE category_id=1;

をみると、先程のサイクル上にいるマテビューをFROM句としている。
このマテビューがあのサイクルにのるとどうなるのかを、このFROM句に対応するマテビューに☆印をつけることで追いかけてみる。

sample_products_reserve のリフレッシュ( 1 -> 2 )

☆sample_products( 1 ) -リネーム-> ☆sample_products_temp( 1 )
sample_products_reserve( 2 ) -リネーム-> sample_products( 2 )
☆sample_products_temp( 1 ) -リネーム-> ☆sample_products_reserve( 1 )

なんと、 sample_products_reserve( 1 ) になってしまったのである。
ちなみに、次のサイクルでは、

☆sample_products_reserve のリフレッシュ( 1 -> 3 )

sample_products( 2 ) -リネーム-> sample_products_temp( 2 )
☆sample_products_reserve( 3 ) -リネーム-> ☆sample_products( 3 )
sample_products_temp( 2 ) -リネーム-> sample_products_reserve( 2 )

どういうことかというと、マテビューがリネームされても、そんなマテビューないよってことにならないように、追跡してくれるのだ。
あったりまえじゃ〜ん。

他言語によるビジネスロジックからの読み込みはテーブル名による参照で、ビューからの読み込みは内部参照値によるものなのだ。
だから、ビジネスロジックからマテビューを検索するぶんにはこれでよい。だけど、ビューがこのマテビューを使う場合は話が違ったということだ。

そしてこの流れを繰り返すと、このビューは、奇数回ではバージョンが上がらず、偶数回で一気に2段階バージョンが上がる奇妙なビューとなった。
奇数回のリフレッシュは実質意味がない。
その上、これがやっかいなのだが、偶数回ではビューが見てしまっているリザーブに対してそのままリフレッシュをしてしまう。
つまり、あれだけ避けたくてローテーションまで組んだ ACCESS EXCLUSIVE LOCK が、実際には2回に1回発生していたことになる。
悲しい以外の言葉がない。

実際に何が起こっていたか

  • なんか反映遅いよね
  • 2回連続でリフレッシュすると変わる気がする
  • よくわかんないねえ
  • とりあえずこのマテビューに修正入るんで確認します
  • あ〜〜〜そういうことか!!!!!

解決したかった

とりあえず一旦の対策として、リフレッシュして3回リネーム後、改めてビューを再定義して、リザーブじゃないほうに紐付けるとしてみた。

deadlock detected

あ〜あ

たぶん、ビューの再定義でローテーションせず単純に CREATE OR REPLACE VIEW を使ったので、検索されてきたSELECT文が ACCESS SHARE LOCK を取っている間に、この再定義が ACCESS EXCLUSIVE LOCK を取り、 ハマってしまったのではないか、と予想している。
幸いにもすぐロックは解除され、無限にガッチリハマってしまったというほどの事故ではなかったのだが、それは混雑具合に左右されうる話。ちょっと許容できなかった。

じゃあどうすればよかったのか

どうすればいいんですか?

  • 9.4以降にバージョンアップする
  • そもそもマテビューをFROM句とするビューを作らない
    • DB設計を最初からちゃんとする

のどっちかが正解っていうか両方やるのが正解だと思う。
なかなかうまくはいかないけど。

Brackets拡張機能を作った話1 パネルを追加・表示するまで

月花です。
Brackets拡張機能を作ってみたので、その話をします。

この記事で扱う範囲

最終的に搦め手になり、いろんな場所をいじったので記事が長くなるため、3つくらいに分ける。
この記事では、Brackets拡張機能を追加し、機能が搭載されていないパネルを表示するまでとする。
次の記事で機能の紹介をし、最後の記事で実際に拡張機能として搭載して完成するまでの話をする。

環境

High Sierraにはアップデートしてないんですけど、多分動くと思う。
ていうかアップデートに1時間以上かかるオフィスから持ち帰れないPC、どうやってアップデートしたらいいんですか?

どんな拡張機能を作ったか

f:id:gekka9:20171128013312p:plain
// なんか動画にしようと思ったけど、よくわからなかったので静止画です。伝わってください。

BracketsからiTunesを操作することで、コーディングをしながら1画面で快適なミュージックライフをサポートする。
音楽を全部詰めてぐちゃぐちゃなシャッフルをするタイプなので、ウィンドウ切り替えたりスマホいじったりで中断するのが嫌だったので。

手法と問題点

JXA ( JavaScript for Automation ) を使う。
developer.apple.com

これまで、スクリプトAutomatorからアプリを操作する場合はAppleScriptのみが対応していたが、JavaScriptでも書けるようになった。
ちょっと使ってみようかな、という動機しか無く、最終的にJavaScriptである必要はなくなった。

当初は、BracketsJavaScriptでできていて、かつアプリなので、横断してそのまま呼べるんじゃないかと思っていたが、そうはいかなかった。
というのも、Brackets自体がブラウザであるようで、エディタなどの機能はその上で動くフレームワークに過ぎなかった。
であれば、ブラウザからローカルのアプリが動かせるというのはセキュリティ的に危険極まりないのでできるわけがなかった。
そのため、搦め手で実装することにした。

Brackets自体からでは無理だが、権限のあるユーザが、適切なスクリプトを動かせばよい。
具体的には、mac標準のapacheでローカルサーバを建て、その中のPHPからJXAで書いたjsファイルを実行する。
BracketsからこのPHPへは、ajaxによる非同期通信で行う。
この流れであれば、サーバ上のバッチが動くのと同義になるため、動かせるはずだ。というか、動かすことができた。

拡張機能の作り方

基本的にはBracketsAPIのページを参考に組み立てていく。
Brackets API

BracketsJavaScriptフレームワークなので、言語は全てJavaScriptとなり、画面上の全てはHTMLである。
デバッグ機能は、メニューの デバッグ > 開発者ツールを表示 で立ち上がる。
どうもChromiumで動いているようで、Chromeの開発者ツールとほぼ同じものが立ち上がった。
ということは console.log() などが呼べるので、変数ダンプもらくちんで非常に助かる。

さて、実際に作っていこう。
拡張機能の雛形のようなものを探してみたが見当たらなかったので、既存の拡張機能を拝借して、中身を書き換えることで実装するのが楽だと思う。
ドキュメントとにらめっこして、作りたい拡張機能に必要なモジュールを追加したり、逆に削除していけばいい。

ファイル構成はこんな感じになる。

extensions/
    user/
        html/
            assets/
                css/
                    iTunesController.css
            panel.html
        main.js
        package.json

このうち、必須なのは main.js と package.jsonで、これらはそれぞれ、拡張機能のメイン関数と情報定義を行う。
公開しないのであれば、package.jsonは適当でいいんじゃないかと思う。
公開するなら、説明文とかライセンスはキッチリ書こう。この内容が拡張機能検索画面に載ると思う。

main.js に書くべきこと

ここでは、紹介した画像のような、エディタの下にパネルを追加する形式の拡張機能を作る際に、どのような手順で何を書いていくかを説明する。
ざっくりと次のようなフェーズに分ける。

  • 依存モジュール読み込みフェーズ
  • 設定読み込み・登録フェーズ
  • コマンド設定・登録フェーズ
  • テンプレート読み込みフェーズ
  • 必須関数定義フェーズ
  • ユーザ関数定義フェーズ

これらはすべて、main.js 内の define() 内で行う。

define( function( require, exports, module ) {
    'use strict';
});

すなわち、このdefine() が、拡張機能のメイン関数となる。
use strict とあるが、厳格モードが必要かどうかはよくわかっていないが、よほど汚いコードを書かざるを得ない場合でもない限り、厳格モードでもさほど問題はないだろう。いつものようにキレイに書こうな!
なお、これ移行のサンプルコードはすべて define() 内に収まっていると思ってほしい。

依存モジュール読み込みフェーズ

まずは、これ移行の処理を行っていくために、Bracketsフレームワーク(以下このように呼称するが、正式な名称ではない)の必要なモジュールのインスタンスを確保する。

    var Menus = brackets.getModule( 'command/Menus' ),
        CommandManager = brackets.getModule( 'command/CommandManager' ),
        PreferencesManager = brackets.getModule( 'preferences/PreferencesManager' ),
        WorkspaceManager = brackets.getModule( 'view/WorkspaceManager' ),
        Resizer = brackets.getModule( 'utils/Resizer' ),
        AppInit = brackets.getModule( 'utils/AppInit' ),
        ExtensionUtils = brackets.getModule( 'utils/ExtensionUtils' );

Brackets API
こんな感じで、API辞書とにらめっこして、必要なものを読み込んでいこう。
大体名前から何をするものかはわかるので、必要そうな単語で検索して、たとえばパネルの追加なら、WorkspaceManager のページを開いて、 add とか create とかでさらに絞り込んでいけば、パネルの追加に必要な関数がわかる、という具合だ。
そのページには、そのモジュールが依存しているモジュールも記載されているので、それも読み込む必要があるが、なくても動いたりする。

テスト実行時に管理者ツールを起動しておくと、足りなかったときはエラーが出るので、それで潰していってもよい。

設定読み込み・登録フェーズ

たとえば、パネルを開きっぱなしにして閉じて、再度開いた時に、この設定に状態を書き出しておけば、以前の状態に復帰できる。

    var preferences = PreferencesManager.getExtensionPrefs( 'iTunesController' );
    preferences.definePreference( 'enabled', 'boolean', false );

必要な数だけ登録しておくとよい。
この拡張機能の場合は、開いているか閉じているか、だけとなった。
実際にパネルを開く際に、定義した設定に書き込んだり読み込んだりするので、今はどんな名前の設定を保持したいか定義するだけ。

コマンド設定・登録フェーズ

ユーザがメニューから何か選んで実行する際には、コマンドという形式で呼ばれてくる。
なので、今回はパネルを開いたり閉じたりするトグルコマンドを定義する。

    var COMMAND_ID = 'iTunesController.enable';
    CommandManager.register('iTunesController' , COMMAND_ID, togglePanel );

CommandManager.register() の第三引数は、メニューが押下された際に実行する関数で、無名関数としてもいいと思う。
関数として定義する場合は、define() の中で行おう。

次に、定義したコマンドをメニューに追加する。
このアプリでは、Macでいうところの上部にあるあのテキストメニューと、エディタ画面の右端にあるアイコンメニューに登録する。

    var menu = Menus.getMenu( Menus.AppMenuBar.VIEW_MENU ),
        contextMenu = Menus.getContextMenu(Menus.ContextMenuIds.PROJECT_MENU);
    if ( menu !== undefined ) {
        menu.addMenuDivider();
        menu.addMenuItem( COMMAND_ID, '' );
    }
    if ( contextMenu !== undefined ) {
        contextMenu.addMenuDivider();
        contextMenu.addMenuItem( COMMAND_ID );
    }

これで、メニュー上に iTunesController という名前のトグルボタンが追加された。

テンプレート読み込みフェーズ

パネルやアイコンのデザインを定義するテンプレートやCSSを読み込む。
htmlファイルにしてもいいし、DOMを作って叩き込んでもよい。

    var controllerPanelTemplate = require( 'text!html/panel.html' ),
        controllerPanel,
        projectUrl,
        controllerIcon = $( '<a href="#" title="' +EXTENSION_NAME + '" id="itunes-controller-icon"></a>' );
    // Load stylesheet.
    ExtensionUtils.loadStyleSheet( module, 'html/assets/css/iTunesController.css' );

これはあくまでもテンプレートのため、動的に動く部分はJavaScriptでDOM操作を行い、表示していく。
なので、ボタン類以外は空の、構造だけのテンプレートでよい。
もしくは、テンプレートエンジン Mustache を標準で扱えるようなので、大規模になる場合はそれで実装するのがいいだろう。

<div id="itunes-controller" class="itunes-controller bottom-panel vert-resizable top-resizer todo-file">
    <div class="toolbar simple-toolbar-layout">
        <div class="title">iTunes Controller</div>
        <div id="itunes-controller-playing">
            <i class="fa fa-folder-open-o" aria-hidden="true"></i> <span id="album"></span> / 
            <i class="fa fa-user" aria-hidden="true"></i> <span id="artist"></span> / 
            <i class="fa fa-music" aria-hidden="true"></i> <span id="name"></span>
        </div>
        <a href="#" class="close">&times;</a>
    </div>
    
    <div class="table-container resizable-content">
      <div id="itunes-controller-controller">
        <a id="itunes-controller-backward"><i class="fa fa-backward" aria-hidden="true"></i></a>
        <a id="itunes-controller-play"><i class="fa fa-play" aria-hidden="true"></i></a>
        <a id="itunes-controller-pause"><i class="fa fa-pause" aria-hidden="true"></i></a>
        <a id="itunes-controller-forward"><i class="fa fa-forward" aria-hidden="true"></i></a>
      </div>
    </div>
</div>

この拡張機能では、こんな感じのテンプレートになった。
画像を作るのがめんどくさいので、FontAwesome がしれっとでてきているけど、本筋ではないのでファイルツリーとかサンプルコードとしては記載しない。
fonts ディレクトリ作って、CSS追加で読み込むだけ。

必須関数定義フェーズ

前項までで、ようやく定義が終わり、ここからは実際にビジネスロジックを書いていく。
このフェーズでは、今回の拡張機能に必須な関数を定義していく。
今回は1つだけ、Brackets が ready になった際に呼ばれる関数を実装する。

    AppInit.appReady( function() {
        var panelHTML = Mustache.render( controllerPanelTemplate, {} );

        WorkspaceManager.createBottomPanel( 'itunesController.panel', $( panelHTML ), 50 );
        controllerPanel = $( '#itunes-controller' );
        controllerPanel
            .on( 'click', '.close', function() {
                enablePanel( false );
            } );

        setNowPlaying();
        setInterval(function(){
            setNowPlaying();
        },5000);
        $('#itunes-controller-backward').on('click',function(){
            controll('previous');
            return false;
        });
        $('#itunes-controller-play').on('click',function(){
            controll('play');
            return false;
        });
        $('#itunes-controller-pause').on('click',function(){
            controll('pause');
            return false;
        });
        $('#itunes-controller-forward').on('click',function(){
            controll('next');
            return false;
        });

        controllerIcon.click( function() {
            CommandManager.execute( COMMAND_ID );
        } ).appendTo( '#main-toolbar .buttons' );

        if ( preferences.get( 'enabled' ) ) {
            enablePanel( true );
        }
      
    } );

まずはパネルのレンダーと、パネルの create 、そして各種リスナーの定義を行っている。
setNowPlaying() や controll() はユーザ関数で、詳しくは次の記事で扱うが、ここでは基本的にリスナーの定義・登録に留めるとスッキリすると思う。
setNowPlaying() は現在再生中の曲を取得する関数で、初回実行による初期化と5秒ごとのタイマー実行の定義を行っており、定義の範疇からは抜け出していない・・・と思っている。
その後、アイコンメニューへのリスナー追加と、設定ファイルから前回の情報を読み出して、パネルを開いておく、ということをやっている。

ユーザ関数定義フェーズ

前項でいくつか呼び出していたような、独自関数の定義を行う。
ビジネスロジックは基本的にここで書くべきだと思う。
今回は下記のような独自関数を実装した。

  • enablePanel() : 実際のパネル表示の切り替え
  • togglePanel() : 現在の開閉状況に従い、 enablePanel() を呼び出す
  • setNowPlaying() : 現在再生中の曲を取得し、パネルへ記載する
  • controll(string) : 再生や一時停止、前後の曲への移動などの操作を行う

iTunes 操作関連は JXA が絡み、ややこしいので次の記事で詳しく話す。

まとめ

ここまでで、ビジネスロジックを抜きにして、ユーザ関数の定義やリスナー追加もふっ飛ばせば、とりあえずパネルが開くだけの拡張機能ができているはずだ。
あとはガリガリビジネスロジックを載せていくだけとなる。

PostgreSQL : 正規化されたテーブルをSQLだけで入れ子のJSONにする

月花です。

SQL芸人としての活動のメモです。

今回は、正規化された一対多のテーブルから、多の方を一行を連想配列JSONにしながら、複数の行をJSONにする、入れ子構造のJSONを抽出します。
結果的になんてことない単純なクエリになったのですが、割とパズル的に悩んでしまったので、残しておきます。

3人の生徒に、それぞれに2回の試験があり、その合計6つの結果を生徒ごとに分割して描画したい、というシナリオで書いていきます。
例では、PHPでHTMLを出力することにします。

通常であれば、普通にJOINして6レコードをループし、
生徒ごとにまとめてさらに3回のループを行うか、
ソートしてから、前のループと生徒IDが違えば閉じタグと開きタグを出力する、
みたいなことになりますが、条件式多くて読みづらいです。
なので、生徒ごとにまとめるところまでSQLにやらせてしまいましょう。レコードの集約はJSONにすれば、言語に依存しません。

目次です。

  • テーブル構造
    • 生徒テーブル students
    • 試験テーブル exams
    • 生徒-試験リレーションテーブル student_exams
  • この例での求めたいデータ
    • 普通にJOINしてみた場合
    • 理想の抽出結果
  • アプローチ
    • 横に広がっている試験データを連想配列に集約する
    • 生徒ごとに連想配列にした試験データを集約する
  • 取れたレコードをPHPで扱う

テーブル構造のサンプル

例では、生徒テーブルと試験テーブル、そしてそれらを繋ぐ生徒-試験リレーションテーブルの3つのテーブルが存在しているとする。
そこから、生徒テーブルを軸にして、生徒数分のレコードの中に、複数の試験をまとめて収めるJSONを書いていく。

生徒テーブル students

student_id name
1 田中
2 佐藤
3 木村

試験テーブル exams

exam_id name date math_point japanese_point english_point
1 中間試験 2017-09-10 10 20 30
2 中間試験 2017-09-10 40 50 60
3 中間試験 2017-09-10 70 80 90
4 期末試験 2017-09-30 15 25 35
5 期末試験 2017-09-30 45 55 65
6 期末試験 2017-09-30 75 85 95

なお、本来であれば試験名と日付も正規化するべきであるが、今回の話にはそれほど影響しないので、簡略化のために今回は正規化しない。

生徒-試験リレーションテーブル student_exams

student_id exam_id
1 1
2 2
3 3
1 4
2 5
3 6

この例での求めたいデータ

普通にJOINしてみた場合

SELECT
    s.*,
    e.*
FROM
    students s
    JOIN student_exams se ON s.student_id=se.student_id
    JOIN exams e ON se.exam_id = e.exam_id;

で、こうなる

student_id name exam_id name date math_point japanese_point english_point
1 田中 1 中間試験 2017-09-10 10 20 30
2 佐藤 2 中間試験 2017-09-10 40 50 60
3 木村 3 中間試験 2017-09-10 70 80 90
1 田中 4 期末試験 2017-09-30 15 25 35
2 佐藤 5 期末試験 2017-09-30 45 55 65
3 木村 6 期末試験 2017-09-30 75 85 95

しかし、これでは、たとえばPHPでこれを処理する場合、愚直にやれば6回ループになり、テーブルならtrタグが6つできたりする。
そういうテーブルを描画するならそれでいいが、生徒ごとのブロックとして閉じタグを挟みたい場合に厄介となる。
なので、3つにどうにか収めるように配列を操作するループを別途書いたり、ソートしてから生徒が変わったかを検知するif文を書いたりしないといけないので、端的にいってクソである。
なので、3レコードにしたい。

理想の抽出結果

student_id name exam_json
1 田中 {試験内容のJSON}
2 佐藤 {試験内容のJSON}
3 木村 {試験内容のJSON}

こうなればよい。
このJSONには、例でいえば、配列長が2で、その中には試験のデータをいれた1つの連想配列がはいっていればいい。
3回のループで生徒ごとに区切りつつ、内部のループで2つの試験のデータを出力すればいいのだ。
この条件に合う結果のJSONは、たとえばこうなる。

"[
    {"student_id":1,"student_name":"田中",
     "exam_data":[
        {"exam_id":1,"exam_name":"中間試験","date":"2017-09-10","math_point":10,"japanese_point":20,"english_point":30},
        {"exam_id":4,"exam_name":"中間試験","date":"2017-09-30","math_point":15,"japanese_point":25,"english_point":35}
      ]
    },
    {"student_id":2,"student_name":"佐藤",
     "exam_data":[
        {"exam_id":2,"exam_name":"中間試験","date":"2017-09-10","math_point":40,"japanese_point":50,"english_point":60},
        {"exam_id":5,"exam_name":"中間試験","date":"2017-09-30","math_point":45,"japanese_point":55,"english_point":65}
      ]
    }
]"

長くなったが、このJSONを作り、上のレコードを抽出することを目的とする。

アプローチ

まず、これを実現するためには、2回の集約が必要となる。

  1. 横に広がっている試験データを連想配列に集約する
  2. 生徒ごとに連想配列にした試験データを集約する

横に広がっている試験データを連想配列に集約する

まずは、試験データを1レコードずつ連想配列に変換していく。
次のステップで生徒IDを使うので、ここで生徒-試験リレーションテーブルをJOINして、生徒IDも含めてしまおう。

student_id exam_id json
1 1 {試験内容のJSON}
1 4 {試験内容のJSON}
2 2 {試験内容のJSON}

(省略)

このためには、json_agg関数を用いる。
生徒を軸に、試験テーブルの1レコードごとに集約するために、GROUP BYには生徒ID、試験IDを指定して、試験テーブルのデータを集約する。
そのためのクエリは、単純に上記を落とし込んで、こうなる。

SELECT
    student_id,
    e.exam_id,
    json_agg(e) AS json
FROM student_exams se
    JOIN exams e ON se.exam_id=e.exam_id
GROUP BY student_id,e.exam_id
ORDER BY student_id ASC

すると結果は、このようになる。

student_id exam_id json
1 1 {[{"exam_id":1,"name":"中間試験","date":"2017-09-10T00:00:00","math_point":10,"japanese_point":20,"english_point":30}]
2 4 [{"exam_id":4,"name":"期末試験","date":"2017-09-30T00:00:00","math_point":15,"japanese_point":25,"english_point":35}]
3 2 [{"exam_id":2,"name":"中間試験","date":"2017-09-10T00:00:00","math_point":40,"japanese_point":50,"english_point":60}]

(省略)

ちなみに、json_agg関数の引数をカラムではなくテーブルにした場合、 table_name.* と同義になる。
すべてのカラムを出すしかないので、カラムを絞りたい場合は、サブクエリにするかWITH句で追い出しておく必要がある。

ここで、当初の目論見とはことなり、連想配列が配列長1の配列に入ってしまっていることに留意しておく。

生徒ごとに連想配列にした試験データを集約する

次に、生徒ごとに集約するので、今度はGROUP BYに生徒IDのみを指定し、再度json_aggにかけてやればよい。
postgresのjson_aggは内部でjson型を扱えるため、
JSONの中に「JSONの形をしたstring」が入ってしまい、最終的にjson_decodeした結果をjson_decodeする必要がある、
なんてことにならないようなツリー状にまとめてくれる。
そのため、そこのところは考慮せず、単純にさきほどのクエリをサブクエリとして、json_aggをかけてやるだけでよい。

SELECT
    student_id,
    json_agg(jsons.json) AS exam_json
FROM
    (
        --ここから
        SELECT
            student_id,
            e.exam_id,
            json_agg(e) AS json
        FROM student_exams se
            JOIN exams e ON se.exam_id=e.exam_id
        GROUP BY student_id,e.exam_id
        ORDER BY student_id ASC
        --ここまで先程のクエリ
    ) jsons
GROUP BY student_id;

これだけでよい。
そして結果は下記となる。

student_id exam_json
1 {[[{"exam_id":1,"name":"中間試験","date":"2017-09-10T00:00:00","math_point":10,"japanese_point":20,"english_point":30}], [{"exam_id":4,"exam_name":"期末試験","date":"2017-09-30T00:00:00","math_point":15,"japanese_point":25,"english_point":35}]]
2 [[{"exam_id":2,"name":"中間試験","date":"2017-09-10T00:00:00","math_point":40,"japanese_point":50,"english_point":60}], [{"exam_id":5,"exam_name":"期末試験","date":"2017-09-30T00:00:00","math_point":45,"japanese_point":55,"english_point":65}]]
3 [[{"exam_id":3,"name":"中間試験","date":"2017-09-10T00:00:00","math_point":70,"japanese_point":80,"english_point":90}], [{"exam_id":6,"exam_name":"期末試験","date":"2017-09-30T00:00:00","math_point":75,"japanese_point":85,"english_point":95}]]

生徒3人と試験6回のデータが3レコードとなって収まった。

SQLはここまでで、ここからはビジネスロジックのお話になる。
ここでは、PHPで行うとするが、JSONなので言語には依存しない。

取れたレコードをPHPで扱う

想定仕様は、生徒ごとに開きタグと閉じタグを出力し、生徒ごとのブロックを描画するものである。
取れたレコードを $records に入れたとすると、下記のようなPHPで扱う事ができる。

<?php

(省略)

// 生徒ごとのループ
foreach( $records as $record){
    // 開きタグ描画処理
    
    // jsonから配列に変換(オブジェクトに変換するなら第二引数は省略かfalse)
    $exam_array = json_decode($record['exam_json'], true);

    // 試験ごとのループ
    foreach( $exam_array as $exam_data_array ){
        // ここで、この中身は配列長が1のデータだったことを思い出そう
        // 仕様上そうなってしまうだけなので、先頭要素を取るだけで良い
        $exam_data = current( $exam_data_array );

        // $exam_data['name'] や、
        // $exam_data['date'] で、試験のデータが取れるので処理や描画
    }

    //閉じタグ描画処理
}

以上で、postgresに仕事をさせ、PHPに楽をさせることができた。

例では単純なものだったのでJOINが少なかったが、本来はもっと正規化してあるはずなのでサブクエリがごちゃつきがち。
WITH句やviewに、いかにわかりやすく追い出すかが出来るSQL芸人になれるかどうかのカギである。たぶん。

Air Mac Extremeを買った

月花です。
Air Mac Extremeを買いました。

www.apple.com


かねてより、ほしいとは思っていたのだ

  • ルータの調子が悪かった
  • macのバックアップしないとそろそろヤバイ
  • ファイルぽんぽんおける場所が欲しかった
  • PS4とつながればハッピー
    • ( 結局これはだめだった)

というわけで、購入時期が早くなったのだ。

ひととおりイカとFF14をしてみたが、ものすごく快適だ。
当然ルータを変えただけで上流はそのままなので、上り下りが速くなるわけではない。
深夜3時に130Mbpsしか出ないようなベストエフォート回線がルータのカタログスペックに追いつくことはないからそこはどうでもよい。

速度ではなく安定性が段違いによいらしく、イカをやれば22時前というピークタイムにも関わらずひっかかりなくキレッキレの動作をし、FF14をやれば逃げ遅れなどの失敗は自覚できたもののみだった。

これはよい買い物をしたぞと思ったしもっと早く買うべきだった。
まあでも最後まで読めばわかるけど、本当にMacユーザにしか値段相応の恩恵がなさそうなので、全体的には割とピーキーな感じ。

ルータの調子が悪かった

これはもう散々で、イカをすれば自分も味方も不審死し、FF14をやればギミックは失敗してタンクは気づけばHPがゼロ。
FF14しかやってなかったら単にヘタクソすぎワロタってだけなのだが、イカも不審とくれば、この2つはゲームシステムが違うのでまず間違いない。
イカもヘタすぎワロタって感じだけど、そこは無視しよう。

思えば長い付き合いだった。
就職して大阪に越してきたのが3年前の4月で、そのときに、ついでにルータを買い替えた。
とはいっても、安いルータから安いルータに変えただけで、へたをすれば前より安いルータになっていたかもしれなかった。
そこへいきなり2万のルータが黒船来航、すでにゴミ袋の中である。

macのバックアップしないとそろそろヤバイ

このMac Book Airとも長い。
購入時期がEarly2014に間に合わず、Mid2013の製品なので、そろそろ4年が経とうとしている。
2回ほどクリーンインストールはしているが、ハード的にそろそろだろう。

いよいよTime Machineか、というところだが、私にTime Machineを使いこなせると思わない。
ノートPCであるMac Book Airに外付けHDDを定期的に差し込み、バックアップをさせるという動作、絶対にしないからだ。
このAir Mac Extremeに接続した外付けHDDをTime Machine用のディスクとして設定できて、LANでつながっている限り、常にTime Machineがバックアップを取ってくれる。
これくらいしてくれないと私はバックアップをしない人間なんだ。もっと甘やかしてほしい。

PS4とつながればハッピーだった

Air Mac Extremeの裏にはUSBポートがあり、外付けHDDをNASのように扱える。
てきとうに軽く調べたら、どうもPS4が認識しそうなことが書いてあったんだけど、そうはうまくいかなかった。
そもそも、NASに対応しているのは、私が共有したかったキャプチャーギャラリーではなく、メディアプレイヤーだったのだ。

当初の筋書きとしては、
PS4スクリーンショットUSBメモリを使ってPCへ運ぶのが不便だから解決のためにNASであったのだが、やはりそれはだめだった。

まあどうせ置き場所近いし、そこまで頻繁にスクショ移さないし、外付けHDDの繋ぎ先を都度変えればいいだろうってことでそうした。
PS4のUSB端子は、キーボードとコントローラでふさがっているので、USBメモリを挿しっぱなしなどできず、それよりは楽だ。

Air Mac ExtremeexFAT読めない問題。

PS4が認識するのはFAT32exFATなので、じゃあとexFATパーティションを切ってスクショをPS4からコピーしたら、Air Mac Extremeが「ディスクの修復が必要です」と言い出した。
よく調べてみると、何の事はない、Air Mac ExtremeexFATに対応していないだけで、別に修復は必要なかった。こいつは手のかかる二万円だ。

しかたがないので、FAT32は表向きは32GBまでが対応ということなので、ためしに128GBの領域をFAT32で切り出してみたが、そうするとPS4が対応していない。
まあそうだよね。

しょうがないから、32GBの領域をFAT32で切り出して、どちらからも読み書きできることを確認した。
結果的にはUSBメモリよりも容量が小さくなった。ちょっと涙をこらえた。