2019年3月

Firebase Realtime Databaseで遊んでみた件

ヒロシです。
FirebaseとはGoogleが提供しているmBaasです。mBaasって?という方はググってください。
今回、その機能の中に、「Realtime Database」なるものが存在するということをお客様よりお聞きし、
さっそく遊んでみました。というお話です。

Realtime Databaseとはなんぞや?

https://firebase.google.com/

「アプリデータを瞬時に保存および同期」

うん、なんとなくわかった。
サーバとクライアント間で同期できるNoSQLを使った仕組みで、チャットみたいなのも簡単に?作れるんだよね、きっと。
(という、ふんわりとした理解)

そんなこんなで、mBaasだもんで、UnityやらAndroidやら、そんなとこで使うもんなのかな?と
Firebaseのドキュメントを読み進めてたところ、
C#やAndroid、Unityというワードに、ごくごく当たり前のように潜んでいる
『ウェブ』なんていう、何とも抽象的な「そこにシビれる!あこがれるゥ!」な、
技術ドキュメントあるあるワードを発見。

サンプルコードもあったので見てみると、シンプルなjavascriptですね、これ簡単そう。

ということで、最近モノ忘れの激しい社内メンバーのために、リアルタイム共有TODOツールを作ってみたYO!

今回作りたいもの

  • TODOを登録できる
  • リアルタイムに共有できる
  • 完了したら削除できる

至ってシンプルですが、まずはこんなところでしょうか。

まずは初期設定

https://qiita.com/H_Crane/items/adf88cc01eabce2b9a5f

この辺りを参考にさせてもらって、まずは登録して、プロジェクト作って、
ウェブ用のconfigをコピー(下で使ってます)、と。

そして、今回作成するのは「todo.html」1枚だけ!(たぶん)

登録するよ

まずはこんな感じなのかね?
(Firebaseのプロジェクトの設定からコピーできるやつです)

<script src="https://www.gstatic.com/firebasejs/5.9.1/firebase.js"></script>
<script> // Initialize Firebase var config = { apiKey: hoge, authDomain: hoge, databaseURL: hoge, projectId: hoge, storageBucket: hoge, messagingSenderId: hoge }; firebase.initializeApp(config); </script>

でもって、データベースを参照する場合はこんな感じらしい

const database = firebase.database();

そんで、これでノードを指定するのね

const ref = database.ref('todo');

とりあえず、ここまでで準備は完了!まずは登録を。
今回、登録したい項目として、

  • タイトル
  • 内容
  • 期限
  • 担当者

が入力できる適当なフォームを作りました。
こんな。

<div>
    <div>
        <input type="text" id="title" placeholder="タイトル(必須)">
    </div>
    <div>
        <textarea id="content" placeholder="内容(任意)"></textarea>
    </div>
    <div>
        <input type="text" id="end_date" placeholder="タスクの期限を設定">
    </div>
    <div>
        <select id="person">
            <option value="未指定">担当者を選んでください</option>
            <option value="おれ">おれ</option>
            <option value="あいつ">あいつ</option>
        </select>
    </div>
    <div>
        <button id="post">登録する</button>
    </div>
</div>

divdivしちゃってるのは、あとで何かスタイルつけようかと思ってるだけ。
ブラウザでアクセスしてみましょう。

で、登録アクションを作ります。
内部ツールなのでタイトルだけ処理側で必須にして、弾かれてもエラーメッセージもなんもなし!
そう、これを漢(をとこ)ツールと勝手に呼ぶ。

// 登録処理
const postAction = () => {
    const title = $("#title").val();
    const content = $("#content").val();
    const end_date = $("#end_date").val();
    const person = $("#person").val();
    if(title && title !== "") {
        ref.push({
            title: title,
            content: content,
            person: person,
            end_date: end_date,
            date: new Date().getTime()
        });
    }
    // とりあえず登録終わったら空にしとく
    $("#title").val("");
    $("#content").val("");
    $("#end_date").val("");
    $("#person").val("未指定");
};

// 登録時のアクション
$('#post').click(() => postAction());

そう、気づいた人もいるだろう。
漢ツールはエスケープなんてものもしないのです。

NOエスケープ NOバリデーション!

これぞ漢(をとこ)の浪漫!
(お仕事ではちゃんとやりますので安心してくださいね)

で、指定したノードにpushってやるだけで子ノードが追加できるんだと。
ほんとかね? 実際に登録してみる。

こんな感じで、適当に入力して「ポチっ」と。

Firebaseの管理画面で確認すると、、
おおおお!!いけてますわー。(ちょっと感動)
どんどんいきましょ、次へ。

一覧表示してみる

// 初期表示と登録後のコールバック
ref.on("child_added", (snapshot) => {
    dispTodo({
        id: snapshot.key,
        value: snapshot.val()
    });
});

onの第一引数にリスナーっての指定してあげると、
リスナーに対応したイベントがトリガーとなって、この部分がコールバックされて動くみたい。
何言ってるかわからんって?
とりあえず、公式を以下に引用しておくので各自読み解いてくださいな。

value 特定のデータベース パスにあるコンテンツの静的なスナップショットを、読み取りイベントのときに存在していたとおりに読み取るために使用します。これは、初期データで 1 回トリガーされます。さらに、データが変更されると、そのたびに再びトリガーされます。イベントのコールバックには、その場所にあるすべてのデータ(子のデータも含む)を含んでいるスナップショットが渡されます。上記のコード例で、value はアプリ内のすべてのブログ投稿を返しています。新しいブログ投稿が追加されるたびに、コールバック関数がすべての投稿を返します。
child_added 通常、データベースからアイテムのリストを取得するために使用します。その場所にあるコンテンツ全体を返す value とは異なり、child_added は既存の子ごとに 1 回トリガーされます。さらに、指定されたパスに新しい子が追加されると、そのたびに再びトリガーされます。イベント コールバックには、新しい子のデータを含んでいるスナップショットが渡されます。並べ替え目的のため、前の子のキーを含んでいる 2 番目の引数も渡されます。
child_changed 子ノードが修正されると、そのたびに child_changed イベントがトリガーされます。この修正には、子ノードの子孫に対する修正も含まれます。これはアイテムのリストに対する変更に応答するために通常、child_added や child_removed と組み合わせて使用されます。イベント コールバックに渡されるスナップショットには、子の更新済みデータが含まれています。
child_removed 直接の子が削除されるとトリガーされます。これは通常、child_added や child_changed と組み合わせて使用されます。イベントのコールバックに渡されるスナップショットには、削除された子のデータが含まれています。

ひとまず、今回はchild_addedとchild_removedを使えばよさそう。
ってことでまずは初期表示と登録を行うためにchild_addedをチョイス。
dispTodoという実際にTODO表示を行う関数(後述)を呼び出す想定でこんな感じに。
さて、snapshotはなんぞや?という話になりますが、
これは上のコールバックが呼ばれた際に受信する静的スナップショットなんだそう。
なんかわからんけど、key:valueで来るのね。

じゃ、実際の表示処理を作ってみよう。

// TODOを表示する
const dispTodo = (todo) => {
    let end_date = "";
    // 期日があれば「xxまでに」という文字列を作る
    if(todo.value.end_date) {
        end_date = todo.value.end_date + "までに";
    }
    // TODO内容をリストの一番上に挿入
    const todo_html = todo.value.title + "<br />" + todo.value.content + "<br />" + todo.value.person + "が" + end_date  + "やる";
    $("#todo_list").prepend(`<div id="${todo.id}">${todo_html}</div>`);
}

とりあえず、下のような空のdivを作っておいて、そこに渡されたノードの情報をどんどん出していく作戦。

<div id="todo_list"></div>

リロード。でた。(ちょっと感動)

削除する

片付いたタスクは消すことができないと困りますね。
ということで、削除機能を作ります。
まずはボタン。『削除』ってのもなんかアレなんで『DONE』というボタンを作ってみました。

// TODOを表示する
const dispTodo = (todo) => {
    let end_date = "";
    // 期日があれば「xxまでに」という文字列を作る
    if(todo.value.end_date) {
        end_date = todo.value.end_date + "までに";
    }
    // TODO内容をリストの一番上に挿入
    const todo_html = todo.value.title + "<br />" + todo.value.content + "<br />" + todo.value.person + "が" + end_date  + "やる";
    $("#todo_list").prepend(`<div id="${todo.id}">${todo_html}<button class="done">DONE</button></div>`);
}

こんな感じで、しょーもないボタンを。

次に削除処理も作りましょ。
ボタンもどんどん動的に追加されていくので、documentのonでセレクタを指定してやる。
イベントの犯人を捕まえて、その親divのid(※)を取ってきて、削除リクエストを投げる、というプレイ。
※各TODOを挟んでいる<div id="${todo.id}">

// 削除処理
$(document).on('click', '.done', (event) => {
    const id = $(event.target).closest('div').attr('id');
    firebase.database().ref('todo/' + id).remove();
});

/keyで目的のノードを操れるようだ。
※登録時にkeyは指定していないが、Firebase側でユニークIDが勝手に付与される、というのはどこかで知りました。という前提ですw

次に削除時のコールバック

/
// 削除
ref.on("child_removed", (snapshot) => {
    $("#"+snapshot.key).remove();
});

出ました、リスナー。
迷わず「child_removed」でコールバックされることを期待しつつ、受け取ったkeyに対応するTODOのdivごと葬る。

よし、削除してみましょ!
ポチっとな!

できた。(あっさり)

さてさて、ほんとに同期とれてるの?
同期されてなければただのしょーもないHelloWorldに毛の生えたWebシステムもどきだし。

これ、複数ブラウザ開いて試してみてください。
どっちかで登録すると、もう一方に出現。
削除すると、もう一方からも消滅。
たいしたことしてないからこその、大感動がここにあるっ!

あとはちょいちょいとスタイルなんかつけちゃって、こんな感じの仕上がり♪

それにしてもHTML一枚、サーバレスでこんなことできる時代になったんですね。
おじさん、困っちゃう。

じゃ、またね!

2016/09/19Database

PythonエンジニアがPHPフレームワークSymfonyをざっくり紹介する

普段私はPythonDjangoで書くことが多いのですが、先日PHPSymfonyを触る機会がありました。

そこで今回は、普段Djangoを使っている目線でのSymfonyの構成などをざっくりご紹介したいと思います。

  1. まずSymfonyとは?

Symphonyではありません。Google先生に頼っていて思うような検索結果が出ていないときは、結構な確率でタイポしていたりします。

Symfonyとは、PHPのフレームワークの一つです。

PHPのフレームワークというと、日本ではLaravelCakePHPがメジャーですが、海外ではSymfonyも幅広く使われています。

Symfonyの設計思想はMVCModel View Controller)です。

DjangoMTVModel Template View)ですね。

役割的には

  • Model データ
  • ViewSymfony), Template 画面に表示されるもの
  • Controller, ViewDjangoModelView(Symfony)やTemplateに渡す部分

ざっくり上記になっています。名前が違ったり被っていたりするので分かりにくいですが、大まかな考え方は近しい部分があります。

  1. Symfonyの構造

ディレクトリ構成は大まかに以下のような形になっています

紹介解説用に番号を振っています

0. sample

├── 1. app                    

  ├── 1-1. Resources

    └── 1-1-1. views     

  └── 1-2. config             

├── 2. bin                    

├── 3. src                    

  └── 3-1. SampleBundle      

    ├── 3-1-1. Controller          

    ├── 3-1-2. Entity     

    ├── 3-1-3. Form            

    ├── 3-1-4. Resources

        └── 3-1-4-1. views   

    ├── 3-1-5. Service

    └── 3-1-6. Validator

├── 4. var                    

└── 5. vendor         

└── 6. web    

  • 0 sample

プロジェクトディレクトリになります

  • 1 app

ここに、プロジェクト全体で使用する設定ファイル(yamlなど)や、初期表示のindex.htmlを入れたりします。

細かい表示制御やデータの加工など各ページで使用するソースは基本的にこのディレクトリには置きません

  • 2 bin

コンソールで使用するコマンドなどのファイルを格納します。

  • 3 src

BundleごとにソースコードやHTMLファイルを格納します。主に開発をする際触るのはここになるかと思います。

  • 3-1 SampleBundle

そもそもBundleって何?と思う方も多いかと思われます。

簡潔にまとめると、Bundleとは、データベースと関連付いたエンティティや、ソースコードやテスト、画面に表示するHTMLなどのファイルをひとところにまとめた機能の塊です。

Djangoで言うところのアプリケーションに近いものだと思うと、想像しやすいかもしれません。

Bundle単位で機能拡張ソースコードも多く公開されているので、取捨選択することで自分でコードを書かずとも色々な機能を組み込めるようになっています。

  • 3-1-1 Controller

Djangoで言うところのView部分です。画面表示するデータを制御したり、URLのルーティング制御を行います。

  • 3-1-2 Entity

データベースのテーブルに応じたEntityを格納する場所です。getter, setterを設定したり、更新時のシーケンスなど、データベースの接続の際の付加処理を設定します。Djangoの方がこの部分に記載する処理の幅は広くなりがちですが、各アプリのmodels.pyに記載するような、データベースに応じたオブジェクトを定義する場所です。

  • 3-1-3 Form

画面で使用するFormを設定する部分です。ここで、各項目のバリデーションも設定するのですが、細かいバリデーション処理はまた別途記載したります。Djangoでいう各アプリのforms.pyに近い役割を担っています。

  • 3-1-4 Resources

viewsディレクトリに、HTMLファイルを格納します。

また、Bundle固有で使う設定ファイルなどある場合は、ここに置いたりも。

  • 3-1-5 Service

Serviceだったりhelperだったり、Bundle内の位置付けで名称は多々あるかと。

Controllerでやるには複雑な処理や、細かいビジネスロジックを記載していく部分になります。

  • 3-1-6 Validator

文字通り、バリデーションを記述する部分です。ここでバリデーションクラスを作成し、formの方でインポートして各項目へのバリデーションを設定します。

  • 4 var

キャッシュファイルや、ログファイルを格納しておくディレクトリになります。

  • 5 vendor

ライブラリ一般を格納するディレクトリになります。

  • 6 web

Jscss、画像ファイルなど、画面上から見えるファイルを格納する場所です。Djangoでいうstaticのような役割です。

  1. Symfonyのメリット・特徴

個人的に使っていてSymfonyってここが面白い!と感じた部分を取り上げてみます。

  • 分業がしやすい

画面表示はここ、バリデーションはここ、formの生成内容はここ、と細かく記述ファイルが分かれているので、多人数で分業をする際はコンフリクトを減らしやすいかと思います。また、Bundleごとに切り分けて開発を進める事も出来るため、開発方法の幅が広がります。

  • テンプレート言語のTwigが使いやすい

Djangotemplate記法と近しい、Twigが使えます。画面上でオブジェクトをループさせてテーブルに値を入れ込んだり、if分岐で表示するものを変えたり、生のHTMLでは表現しにくい処理が簡潔に行えます。

  • Formを使う機能を補助する仕組みが協力

検索であったりデータの登録・更新、ログインと、formを使わないシステムはほぼ無いかと思われます。Symfonyでは、Validationクラスで細かく設定してのバリデーションや、entityをまるっとフォームに入れてそれを使用してデータの更新を行う事、また画面にレンダリングする際も、細かくlabelform本体の配置は行えます。使い方のルールがある程度定まっている事で、多人数での開発の際にソースの揺れを減らせる事もメリットだと思います。

今までSymfonyの名前も知らなかったのですが、使ってみると意外と馴染んだ部分もありました。

PHPのメリットでありデメリットでもある自由にコードが書ける(≒フォーマットの統一が難しい)という部分を、Symfonyの記法である程度統一規格を持たせ一貫性のあるソースに仕上げやすいという風にうまくカバーしていました。

日本でのシェアが広くない事もあり、ドキュメントは少ないのでそこはお気をつけください。

もしPHPで開発を行う事がありましたら、フレームワークの選択肢の一つに検討してみてはいかがでしょうか?

2016/09/19PHP

PostgreSQLで階層マスタを扱う

とある案件で階層マスタを扱うことがありました。

DBがOracleの場合だと、「START WITH 〜 CONNECT BY PRIOR 〜」という、階層問い合わせの構文があるのですが、PostgreSQLだと階層問い合わせが出来るのか知らなかったので調べてみました。
(なお、MySQLでは使えません)

1. データを準備する

以下の様な階層構造を考えます。

レベル0 レベル1 レベル2
勘定科目
資産科目
現金
預金
負債科目
借入金
預り金
資本科目
資本金
利益剰余金


テーブル定義を以下の通りとします。

列名 内容
id int 主キー
code varchar(20) 科目コード
name varchar(100) 科目名
parent_id int 親科目の主キー (外部キー)


DDLは以下の通りです。

CREATE TABLE kamoku (
  id int primary key,
  code varchar(20),
  name varchar(100),
  parent_id int references kamoku(id)
);

INSERT文は以下になります。

INSERT INTO kamoku (id, code, name, parent_id) VALUES (1, 'A', '勘定科目', NULL);
INSERT INTO kamoku (id, code, name, parent_id) VALUES (2, 'A01', '資産科目', 1);
INSERT INTO kamoku (id, code, name, parent_id) VALUES (3, 'A02', '負債科目', 1);
INSERT INTO kamoku (id, code, name, parent_id) VALUES (4, 'A03', '資本科目', 1);
INSERT INTO kamoku (id, code, name, parent_id) VALUES (5, 'A0101', '現金', 2);
INSERT INTO kamoku (id, code, name, parent_id) VALUES (6, 'A0102', '預金', 2);
INSERT INTO kamoku (id, code, name, parent_id) VALUES (7, 'A0201', '借入金', 3);
INSERT INTO kamoku (id, code, name, parent_id) VALUES (8, 'A0202', '預り金', 3);
INSERT INTO kamoku (id, code, name, parent_id) VALUES (9, 'A0301', '資本金', 4);
INSERT INTO kamoku (id, code, name, parent_id) VALUES (10, 'A0302', '利益剰余金', 4);

こうして作ったデータをSELECTすると、以下の結果になります。

hoge=# select * from kamoku;

id code name parent_id
1 A 勘定科目
2 A01 資産科目 1
3 A02 負債科目 1
4 A03 資本科目 1
5 A0101 現金 2
6 A0102 預金 2
7 A0201 借入金 3
8 A0202 預り金 3
9 A0301 資本金 4
10 A0302 利益剰余金 4

(10 rows)

2. WITH RECURSIVE句による階層問い合わせ

PostgreSQLにはWITH句というサブクエリを定義する句が使えます。
ここに、RECURSIVEを付けると、再帰的なサブクエリを書くことが出来ます。

具体的には、以下の様に書きます。

WITH RECURSIVE child (level, id, code, name, parent_id) AS (
  SELECT 0, kamoku.id, kamoku.code, kamoku.name, kamoku.parent_id FROM kamoku WHERE kamoku.id = 1
UNION ALL
  SELECT
    child.level + 1,
    kamoku.id,
    kamoku.code,
    kamoku.name,
    kamoku.parent_id
  FROM
    kamoku, child
  WHERE
    kamoku.parent_id = child.id)
SELECT level, id, code, name, parent_id FROM child ORDER BY code;

結果は以下の通りです。

level id code name parent_id
0 1 A 勘定科目
1 2 A01 資産科目 1
2 5 A0101 現金 2
2 6 A0102 預金 2
1 3 A02    負債科目 1
2 7 A0201 借入金 3
2 8 A0202 預り金 3
1 4 A03    資本科目 1
2 9 A0301 資本金 4
2 10 A0302 利益剰余金 4

(10 rows)

2行目のSELECT文のWHERE句の条件が、開始のidになります。
開始のidを指定すれば、途中から取得することも出来ます。

WITH RECURSIVE child (level, id, code, name, parent_id) AS (
  SELECT 0, kamoku.id, kamoku.code, kamoku.name, kamoku.parent_id FROM kamoku WHERE kamoku.id = 2
UNION ALL
  SELECT
    child.level + 1,
    kamoku.id,
    kamoku.code,
    kamoku.name,
    kamoku.parent_id
  FROM
    kamoku, child
  WHERE
    kamoku.parent_id = child.id)
SELECT level, id, code, name, parent_id FROM child ORDER BY code;

level id code name parent_id
0 2 A01 資産科目 1
1 5 A0101 現金 2
1 6 A0102 預金 2

(3 rows)

逆順(子→親)も出来ます。

WITH RECURSIVE child (level, id, code, name, parent_id) AS (
  SELECT 0, kamoku.id, kamoku.code, kamoku.name, kamoku.parent_id FROM kamoku WHERE kamoku.id = 10
UNION ALL
  SELECT
    child.level + 1,
    kamoku.id,
    kamoku.code,
    kamoku.name,
    kamoku.parent_id
  FROM
    kamoku, child
  WHERE
    child.parent_id = kamoku.id)
SELECT level, id, code, name, parent_id FROM child ORDER BY code DESC;

level id code name parent_id
0 10 A0302 利益剰余金 4
1 4 A03 資本科目 1
2 1 A 勘定科目

(3 rows)

3. まとめと落とし穴

今回の階層問い合わせを再帰的に行うクエリだと、1クエリで出来て速度も速いので良い方法だと思います。

ですが、各階層(level)毎に取得している為、最後に並び替えをしないと、ツリー状に並ぶ結果が返ってきません。
今回は簡単にするために、科目コード(code)にルールを作って並ぶようにしています。

実際に使う時は、もう一工夫したいところです。

2016/09/19Database
1