アナログCPU:5108843109

ゲームと音楽とプログラミング(酒と女とロックンロールのノリで)

('ω') < イザユケエンジニャー

2度目の転職から1年8か月経ちました

前回のあらすじ(?)

SESなWebエンジニアとして3つ目の現場で働いているところですが、
早くも3度目の転職を考えることになりました。
といっても今の会社にはさほど不満はなく、今の現場で引き抜きのお誘いを受けたのです。

給与は今より上がることが前提ですよとは伝えたし、勤務時間は変わらないし、家から近いし、実力主義裁量労働感あって価値観近そうだし、雰囲気も悪くないし、有休とかも普通に取りやすそうだし。今までの勤務先・出向先全部合わせても自分の中ではかなり上位。
技術的にも悪くない。色々試せる風土はありそうだし、DBもSQLServerPostgreSQL使っててまだまだ勉強になりそう。
デメリットはSESでなくなること(いろんな現場を渡り歩いて武者修行できなくなること)かなあ。あと、すごい雑な言い方すると、SESだと人間関係割り切って開発業務だけしてればいいから好きではあるし。今の会社来るまでSES敬遠してたけど意外と楽しかった。電話対応とか雑務とかしたくねえ~って感じ。
あと自社は自社でいいとこなので申し訳なさもある…。いい人ばっかりなんだよなあ…

つーか引き抜きってマジであるんだな…仲介業者との関係とかの大人の都合は大丈夫なのかな…と思ってたら、どうもその業者経由で来てた人を既に何人か引き抜いてるらしい。いいんだ…。それで普通に関係継続してるんだ…。
(自分みたいに、その業者の直接の所属じゃない人ばかりだったのかもしれませんが)

年収次第なところはあるのでまだ保留ですけどね。
フリーランスになって直接契約」を提案するのが一番効率良さそうな気がしたけどフリーランスこわい。

CodeIgniterでSPA実装してみる

SPA is 何

参考:SPA(Single Page Application)の学習、そこに高度なスキルなどいらない - Qiita

Single Page Applicationの略。
一枚のHTMLの中でJS使ってがちゃがちゃ書き換えようぜっていう思想です。

FacebookとかTwitterみたいなやつね。

CodeIgniterで実装してみた

参考:ui-routerとcodeigniterを組み合わせてSPAをつくるときのメモ - よっこらせとプログラム

JSのプラグインとかもそのへんに落ちてるみたいですが、今回はCodeIgniter+jQuery環境で自分で作ってみました。
(別にjQueryである必要はないです)

とりあえずサンプル完成品から貼る。
適当に抜粋してるので適当に解釈してください。

あと、できる限りシンプルにしているので

仕様

「/sample/test_a/」「/sample/test_b/」の二つのページを行き来するだけのもの。
それぞれに互いへのリンクが張ってあります。
で、もちろんSPAなので中身のコンテンツ部分だけを書き換える感じ。
もちろん、各URLに直接アクセスしたときもそれぞれのページが表示されるようにするし、ブラウザのURLや履歴もちゃんと更新する。

作ったのは

  • core/MY_Controller.php
  • controllers/Sample.php
  • views/top.html
  • views/test_a.html
  • views/test_b.html

core/MY_Controller.php

やってること

  • viewに渡すパラメータ変数はここで定義している
  • __construct():spaっていうパラメータに「on」が渡されたらspaフラグを立てる
  • view():spaフラグが立ってたらコンテンツ部分だけを返し、立ってなければ普通にview()する
  • spa_view():viewをちょっと拡張して、必ず結果文字列を返すようにしたやつ

controllerをシンプルにしたいあまりここがごちゃごちゃに。
実際に使うとなったらもうちょっと切り分けなきゃいけないケースが出てくるかもしれない。

<?php 
class MY_Controller extends CI_Controller {

	var $spa = FALSE;

	var $data_list = array('content' => NULL);

	public function __construct()
	{
		parent::__construct();
		if ($this->input->get("spa") == "on")
		{
			$this->spa = TRUE;
		}
	}

	public function view($template_name = "top")
	{
		if ($this->spa == TRUE)
		{
			echo $this->data_list['content'];
			return;
		}
		$this->load->view($template_name . ".html", $this->data_list);
	}

	public function spa_view($template_name)
	{
		return $this->load->view($template_name . ".html", $this->data_list, TRUE);
	}
}

controllers/Sample.php

やってること

  • test_a():test_aというviewの中身を取ってview()するだけのやつ
  • test_b():test_aというviewの中身を取ってview()するだけのやつ
<?php
class Sample extends MY_Controller {

	public function test_a()
	{
		$this->data_list['content'] = $this->spa_view("test_a");
		$this->view();
	}

	public function test_b()
	{
		$this->data_list['content'] = $this->spa_view("test_b");
		$this->view();
	}
}

views/top.html

やってること

  • id="spa_content" なdivタグを用意して、この中身を書き換えるようにする
  • JSで、aタグがクリックされたときに以下処理を行うようにしている
    • aタグからhref要素を取得
    • 「#」が含まれていれば以降の処理は無視、1文字目が「/」だったらサイト内リンク、そうでなければ外部リンクと判断(自分の場合はこれで事足りるので雑な実装)
    • 外部リンクだったら別タブで開いて処理終了
    • サイト内リンクだったら「?spa=on」とパラメータをくっつけてgetする(自分の場合は他でクエリストリング使わないので雑な実装)
    • getに成功したらdivタグの中身を書き換えて、ブラウザの履歴も書き換えて、終了
    • getに失敗したらエラーメッセージをポップアップして終了
<!DOCTYPE html>
<html>
<head><title>test</title></head>
<body>

<div>test-header</div>
<div id="spa_content"><?= $content ?></div>
<div><a href="http://google.com">test</a></div>
<div>test-footer</div>

<script src="/jquery.min.js"></script>
<script>
$(document).on('click', 'a', function(){
  href = $(this).attr('href');
  if (href.indexOf('#') != -1){
      return;
  }
  if (href.substr(0, 1) != '/'){
    $(this).attr('target', '_blank');
    return;
  }
  $.ajax(href+'?spa=on',{ type: 'get', }
  )
  .done(function(data) {
    $('#spa_content').html(data);
    history.pushState(null, null, href);
  })
  .fail(function() {
    alert('データの取得に失敗しました。時間をおいて再度お試しください。');
  });
  return false;
});
</script>
</body>
</html>

test_a.html / test_b.html

ただリンク張ってるだけのやつ

<a href="/top/test_b/">test a</a>
<a href="/top/test_a/">test b</a>

リダイレクトするだけのプログラム

ダウンロード版同人作品の頒布をするとき用のプログラムをざっくり作ってあったので残しておく。

やってることは「特定のURLにアクセスされたら別のURLにリダイレクトする」だけです。
弊サークルでは、「サークル公式ドメインで用意した作品別URL」にアクセスされたら「ダウンロードコンテンツを設置したdropboxの公開ディレクトリ」にリダイレクトするようにしています。

そもそもそういう設置場所URLをダイレクトにバラまいて問題ないならこのプログラムを使う必要はないのですが、
使うと以下のようなメリットがあります。

  • 仮に設置場所を変えても配布URLを変える必要がない(設定を変更するだけでOK)
    • 単に設置場所の整理をしたい場合や、リダイレクト先のURLの方が外部に漏れちゃったので置き場所変えたい…という場合も配布した方のURLは気にしなくて済む
    • 逆に、設置場所は動かないけど自サーバのURLはいつ変わるか分からない…というときは向かないです
  • アクセスログを記録できる
    • 購入してもないのにランダムアタックしてくる奴がいたら分かるようになっています
    • 1つの設置場所に対して複数のURLを作ることもできるので、頒布するイベントや相手ごとにURLを変えれば簡単なマーケティングも可能かも
  • 特定IPからのアクセスを弾くことができる
  • 配布URLを好きな短いものにできる
    • 弊サークルでは「ttp://******.com/dlc/hoge」みたいな感じです。hogeの部分が作品ごとに変わる秘密のコードなやつ。
    • あからさまにdropboxとかのURLをバラまくのもカッコ悪いと感じる場合はそこも解決します

今回必要なかったので実装してませんが、ちょっと改造すれば「有効期限を過ぎたコードは無視する」みたいなこともできますね。

設置・設定方法は末尾に記載。

.htaccess

これは
「このファイルを置いたディレクトリ以下へのアクセスは全部↓の「index.php」にぶっこむよ!」
という設定のファイルです。

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [L]

ただ、.htaccessはレンサバによっては設置できなかったり無視されたりするケースもあります…厄介…
わたしが個人で使っているサーバ(さくら)は大丈夫なんですが、サークルのサーバ(エックス)はだめだったので結局別途対応しています。
サークル主に任せたので方法は把握してませんが、サーバによっては代替の手段があるようです。

index.php

<?php
// config ////////////////////////////////////////////////////////////////////////////

// このファイルを設置しているディレクトリパス(ドメインより後ろ)
$dirpath = "/download/";

// ログファイルパス
$ok_logpath = __DIR__ . "/access_ok.log";
$ng_logpath = __DIR__ . "/access_ng.log";

// IPブラックリスト
$ip_black_list = array(
	"xxx.xxx.xxx.xxx", // ダミー
);

// コードとリダイレクト先の対応
$codelist = array(
	'code_a' => "https://google.com/", // ダミー
	'code_b' => "https://yahoo.co.jp/", // ダミー
);

//////////////////////////////////////////////////////////////////////////////////////

// アクセス元IPを判別(ログ記録・ブラックリスト照合の用途)
// HTTP_X_FORWARDED_FOR と REMOTE_ADDR は前者を優先し、さらにカンマ区切りの場合は最後のものを採用
// 環境によっては上手く取れない可能性もなくはないです。様子見て要調整
$ip = (isset($_SERVER["HTTP_X_FORWARDED_FOR"]) && !empty($_SERVER["HTTP_X_FORWARDED_FOR"])) ? $_SERVER["HTTP_X_FORWARDED_FOR"] : $_SERVER["REMOTE_ADDR"];
if (strpos($ip, ",") !== FALSE)
{
	$ip_list = explode(",", $ip);
	$ip = trim(array_pop($ip_list));
}

// 引数に渡されたコードを判別
preg_match("/^" . preg_quote($dirpath, "/") . "(.*)/", rtrim($_SERVER['REQUEST_URI'], "/"), $match);
$code = empty($match[1]) ? "" : $match[1];

// 【OKパターン】コードが定義されていれば対応するURLにリダイレクト
if (!empty($code) && isset($codelist[$code]) && !in_array($ip, $ip_black_list))
{
	// ログに記録
	if (!empty($ok_logpath))
	{
		file_put_contents($ok_logpath, "[" . $ip . "] " . $code . "\n", FILE_APPEND);
	}

	// リダイレクト
	header("Location: " . $codelist[$code]);
	exit();
}

// 【NGパターン】ログに記録
if (!empty($ng_logpath))
{
	file_put_contents($ng_logpath, "[" . $ip . "] " . $code . "\n", FILE_APPEND);
}

// --------------------------------------------------------------//
// PHP処理はここまで:NGパターンだった場合はそのまま下記HTMLを出力して終了 //
// --------------------------------------------------------------//
?>

<!DOCTYPE html>
<html>
<head>
  <title>ダウンロード</title>
  <meta charset="utf-8">
</head>
<body>
  <div style="text-align:center;">
    <Error!><br>URLが誤っています。<br>お手数ですがもう一度お確かめください。
  </div>
</body>
</html>

設置・設定手順

  • ダウンロードコンテンツdropboxの公開ディレクトリなどに設置する
    • もちろん自分のサーバ内に置くなどでもOK。とにかくアクセスすればダウンロードできるようなURLを作る
  • 上記の.htaccessとindex.phpを任意の公開ディレクトリに設置
  • 必要に応じてindex.phpを調整
    • $dirpath
      • 例えば設置場所が [domain]/download/ ならデフォルトのままでOK
    • $ok_logpath / $ng_logpath
      • アクセスログを取る場合のログファイルパス
      • okが成功時、ngが失敗時
      • ログを取らないなら空文字でOK
    • $ip_black_list
      • IPでブロックする場合のブラックリスト
      • 不審なアクセスがあった場合など、随時ここに追加するとそのIPからのアクセスを無視する
        • エラー等を出すのではなく、正しいコードだった場合も誤っている場合と同じ表示になります
    • $codelist
      • DL用のコードと実際のURLの対応表
      • 例えば「'hoge' => "ttps://google.com/"」という設定であれば、「[domain]/download/hoge」にアクセスすると「ttps://google.com/」にリダイレクトするというわけです
      • コードはパスワードみたいなものなので、「ランダムな長い文字列」にするほど不正なダウンロードを防ぐことができます。多少は構わないと考える場合やそもそも無料配布の場合などは簡単なものにしておいた方がアクセスしてもらいやすくなります
      • コードに使える文字はURL
    • NGパターンのHTML部分のデザインなど

一通り設定・設置してみて、例えば
「/download/」ならアクセスできるのに「/download/test」は404エラーになる!
というような場合は.htaccessが効いていない可能性が高いです。

CodeIgniter入門 #9:webAPIを作ってみる

CodeIgniter入門シリーズ カテゴリーの記事一覧 - アナログCPU:5108843109

今回は、アプリケーションとwebAPIを分離して開発することを想定してサンプルを作ってみます。

今回やること

  • アプリケーションとwebAPIを分離したディレクトリ構造にする
  • webAPIっぽいものを作ってみる
  • アプリケーションのコントローラからwebAPIをコールする
  • アプリケーションの画面フォームからコントローラを呼び出す(間接的にwebAPIをコールする)

ディレクトリ構造

こんな感じにしてみました。
もちろん、/www/index.php と /www/api/v1/index.php の内容も適宜調整します。

  • application
    • web(CodeIgniterのapplication以下そのまま)
    • api
      • v1(CodeIgniterのapplication以下そのまま)
  • system(CodeIgniterのsystem以下そのまま)
  • www

作ってみるAPI

RESTAPIでいってみよう。
参考: REST APIとは? - API設計のポイント

ユーザーテーブル(user)を用意して、それに対して操作するAPIにしようと思います。

  • GET /api/v1/user/ 一覧取得
  • GET /api/v1/user/1/ ID:1を取得
  • POST /api/v1/user/ 登録
  • PUT /api/v1/user/1/ ID:1を変更
  • DELETE /api/v1/user/1/ 削除

userではなくusersにする方が一般的らしいんだけど、今回はこれで。

userテーブルは以下の感じ。(サンプルなのですごい最低限だしパスワードも暗号化しない)

  • id(int / PKでauto_increment)
  • login_id(varchar)
  • password(varchar)

まずはAPIを実装してみる

このURLになるなら、userコントローラを作り、indexの中で処理を分岐させればよいだろうと思う。
ただし、処理の分岐というのは「methodを見分けて適切な関数をコールし分けるだけ」のはずであり、
他のコントローラでも使いまわすはずなので、コアクラスに書くことを考える。

コアクラス

CI_Controllerを継承したMY_Controllerを継承したWebApiControllerを作り、これをコントローラで継承することにする。

<?php
class MY_Controller extends CI_Controller
{
    (省略)
}

class WebApiController extends MY_Controller
{
    public function __construct() { (省略) }

    public function common()
    {
        // methodの判別
        $method = $this->input->method(FALSE);

        // 引数(get,post,streamをマージ)
        $stream = json_decode($this->input->raw_input_stream, true);
        $param_list = array_merge(
            $this->input->get(),
            $this->input->post(),
            !is_array($stream) ? array() : $stream
        );

        // 対応する関数をコール
        if (is_callable(array($this, $method)) && in_array($method, array("get", "post", "put", "delete")))
        {
            echo json_encode($this->$method($param_list));
        }
        else
        {
            echo json_encode(array('msg' => "Undefined API"));
        }
    }
}

特に意味もなく全文貼ったが、このcommonというのは要するに

  • methodを判別して
  • 引数は雑にgetとpostとstreamを全部マージして
  • methodと同名の関数があり、かつそれがget,post,put,deleteのいずれかであればそこに引数を渡す
  • 呼ぶべき関数がなければメッセージを返す

というだけである。

真面目に実装するなら場合によっていろいろ調整が必要かもしれないけど今回はこれで。

ルーティング

config/routes.php

$route['user/(:num)'] = 'user/index/$1';

を追加。
これで、/user/123/ にアクセスすると
userコントローラのindex関数の引数に渡されます。

Userコントローラ
<?php
class User extends WebApiController {

    var $url_param_list = array();

    // コンストラクタ
    public function __construct() { (省略) }

    // API受け口(コアクラスで共通処理を行う)
    public function index($id = null)
    {
        // クエリパラメータ
        $this->url_param_list['id'] = empty($id) ? NULL : $id;

        // 共通処理をコール
        parent::common();
    }

    public function get($param_list)
    {
        $arg_list = array();
        $sql  = "SELECT `id`, `login_id`, `password` ";
        $sql .= "FROM   `user` ";
        if (preg_match("/^[0-9]{1,9}$/", $this->url_param_list['id']))
        {
            $sql .= "WHERE  `id` = ? ";
            $arg_list[] = $this->url_param_list['id'];
        }

        return $this->db->query($sql, $arg_list)->result();
    }

    public function post($param_list)
    {
        $arg_list = array();
        $sql  = "INSERT INTO `user` (`login_id`, `password`) ";
        $sql .= "VALUES (?, ?) ";
        $arg_list[] = $param_list['login_id'];
        $arg_list[] = $param_list['password'];
        $this->db->query($sql, $arg_list);

        return array(
            'id'       => $this->db->insert_id(),
            'login_id' => $param_list['login_id'],
            'password' => $param_list['password'],
        );
    }

    public function put($param_list)
    {
        $arg_list = array();
        $sql  = "UPDATE `user` ";
        $sql .= "SET    `login_id` = ? ";
        $sql .= "      ,`password` = ? ";
        $sql .= "WHERE  `id` = ? ";
        $arg_list[] = $param_list['login_id'];
        $arg_list[] = $param_list['password'];
        $arg_list[] = $this->url_param_list['id'];
        $this->db->query($sql, $arg_list);

        return array(
            'id'       => $this->url_param_list['id'],
            'login_id' => $param_list['login_id'],
            'password' => $param_list['password'],
        );
    }

    public function delete($param_list)
    {
        $arg_list = array();
        $sql  = "DELETE FROM `user` ";
        $sql .= "WHERE       `id` = ? ";
        $arg_list[] = $this->url_param_list['id'];
        $this->db->query($sql, $arg_list);

        return array(
            'id' => $this->url_param_list['id'],
        );
    }
}

また長々と貼っていますが、__construct と index に加え、get, post, put, delete の4つの関数があります。
どこから来てもまずは index で受けることになり、ルーティングで定義されていた引数(ここではID)を確保したのち、あとはさっきのコアクラスのcommonにお任せします。
で、commonからは get, post, put, delete のいずれかにやってきて、必要なクエリを実行したら戻り値を返しておしまい、です。

バリデートなどは無視しているとはいえ、意外にサクッと実装できました。
動作確認は「Advanced REST Client(Chromeアプリ)」などを使えば簡単に行えます。

CodeIgniter入門 #8:データベースの操作<クエリビルダ編・更新系の巻>

CodeIgniter入門シリーズ カテゴリーの記事一覧 - アナログCPU:5108843109

前回でだいぶ疲れたのですがせっかくなので更新系もやっておきます。
参照系は基本的に「は?」と思ってたんですが、更新系は「これ上手く使うと効率上がるかも?」と思うところもちらほら。
まあ確かにSQLでもSELECTの方が厄介だよね、普通は…

公式マニュアルはこちら。
クエリビルダクラス — CodeIgniter 3.2.0-dev ドキュメント

今回やること

  • クエリビルダを使ってINSERT・UPDATE・DELETE・TRUNCATEしてみる

INSERT ... VALUES ...(1件のレコード追加)

$data = array(
    'name'     => "ほげほげさん",
    'login_id' => "username",
    'password' => "password"
);
$this->db->insert("user", $data);

おおお…わかりやすい。
これはPHPで書いてると相性良さそう。

オブジェクトも使えるようです。
(公式からの引用)

/*
class Myclass {
        public $title = 'My Title';
        public $content = 'My Content';
        public $date = 'My Date';
}
*/

$object = new Myclass;
$this->db->insert('mytable', $object);

INSERT ... VALUES ... (複数件のレコード追加)

$data = array(
    array(
        'name'     => 'ふがふがさん',
        'login_id' => 'user2',
        'password' => 'pass'
    ),
    array(
        'name'     => 'ぴよぴよさん',
        'login_id' => 'user3',
        'password' => 'pass'
    ),
);
$this->db->insert_batch('user', $data);

おおおお…

UPDATE(1件のレコード更新)

$data = array(
    'name'     => "ほげほげさん_update",
);
$this->db->where("id", 1);
$this->db->update("user", $data);

SELECTと同じく、whereを使って絞り込みができました。

UPDATE(複数件のレコード更新)

$data = array(
    array(
        'id'   => 3,
        'name' => "更新されるふがふがさん",
    ),
    array(
        'id'       => 4,
        'name'     => "更新されるぴよぴよさん",
        'password' => "newpass"
    )
);
$this->db->update_batch("user", $data, "id");

!!!
今回一番の感動。
めんどくさいbulk-updateをいい感じに作ってくれます。
第一引数がテーブル、第二引数がデータなのはこれまで通りで、第三引数に渡したカラム名に対応するデータを使ってくれます。
あっこれならクエリビルダ使ってもいいかも!と思いましたが、しかしbulk-updateやる機会はそんなにないな…。

-- ↑は↓を作ってくれます!!!!!すごい!!!
UPDATE `user` 
SET
  `name` = CASE 
     WHEN `id` = 3 THEN '更新されるふがふがさん'
     WHEN `id` = 4 THEN '更新されるぴよぴよさん'
    ELSE `name`
  END
 ,`password` = CASE
    WHEN `id` = 4 THEN 'newpass'
    ELSE `password`
  END
WHERE `id` IN(3,4)

あればUPDATE、なければINSERT

※タイトル詐欺です。ちゃんと読んでね。

$data = array(
    'id'       => 1, // ここがPK
    'name'     => "ほげほげさん_update_or_insert",
);
$this->db->replace("user", $data);

渡されたデータの中に、primaryかuniqueのカラムがあれば、それを元に「あればUPDATE、なければINSERT」をやってくれます。
やったー超簡単!

……

…………ん? replace?

はい、実際に発行されるクエリは「REPLACE INTO ... 」でした。
これはMySQLにおいて「(あろうがなかろうが)DELETEしてINSERT」する構文です。

例えばレコード作成日時をデフォルトで現在日時にしているとREPLACEした日時にすげ変わりやがりますし、
on updateだって当然反応します。
律儀にアプリ側で日時を渡して記録しているならともかく、ある程度DB任せの設定の場合は危ないのでご注意を。

完全にMySQLの話なので参考URLの紹介にとどめておきますが、
基本的には「INSERT ... ON DUPLICATE KEY UPDATE」構文をおすすめします。
(今回は検証しませんがREPLACEの方が速いという説もありますので、よーーーーく検討して選んでください)

で、ON DUPLICATE... するにはクエリビルダで用意されていないようなので、自分で拡張する必要があるようです。
やっぱりSQLベタ書きでよくない?
【CodeIgniter3】クエリビルダーにINSERT ON DUPLICATE KEY UPDATE構文によるバッチ処理を追加する方法 - あずみ.net

DELETE

さすがにDELETEはシンプルでした。

// DELETE FROM `user` WHERE `id` = 3
$this->db->delete('user', array('id' => 3));

条件の指定方法はWHEREと同じのようで、

$this->db->where("id", 3);
$this->db->delete("user");

でもOKでしたし、なんならFROM句も分離できました。

$this->db->from("user");
$this->db->where("id", 3);
$this->db->delete();

全データ削除

「$this->db->delete("user");」でええんちゃうんかと思ったらダメでした。(うっかり防止?)

ご丁寧に全消し専用メソッドが用意されています。

// DELETE FROM `user`
$this->db->empty_table("user");

TRUNCATEもあります。

// TRUNCATE `user`
$this->db->truncate("user");

DELETEとTRUNCATEの違いはこれまたMySQLの話なので軽くにしておきますが、
DELETEは「レコードを削除する」で
TRUNCATEは「テーブルを作り直す」です。
TRUNCATEの方が速くてキレイになりますが、トランザクションが効きません。
適切な方を選んでください。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.1.33 TRUNCATE TABLE 構文

やっぱりかゆいところに手が届かない…

「INSERT ... SELECT ...」とか「UPDATE ... LEFT JOIN ... SET ...」とかやりたいんですけど一体どうすれば…?
調べてみても有力な情報が見当たらないし、いろいろ試してもみましたが上手くいきませんでした。うーんいまいち。

REST APIの設計むずかしくない?(素人)

最近、SESとしてWeb系案件探すときに「REST APIが作れる人」っていう条件を時々見かける。

気になるので、趣味のサイトをRESTAPIで実装できないかちょっと考えている。
真面目に勉強しろって話なんだけど、とりあえず見よう見まねで。

参考にしているのはこのへん
REST APIとは? - API設計のポイント
RESTのベストプラクティス | POSTD

GET /users/ でユーザー一覧を取得する。わかる。/users/list/とかでもいいっぽいけど。
GET /users/1/ でID1のユーザーを取得する。まあよかろう。
POST /users/ で新規ユーザー登録。ほう。
PUT /users/1/ でID1のユーザー情報の変更。なるほど。
DELETE /users/1/ でID1のユーザーの削除。せやな。

基本はこんな感じっぽい。
なるほどわかりやすいかな、と思ったけど、もっといろいろ考えてるとちょっと詰まった。

たとえばSNSで、
「ログインしていない人が見るID1さんのプロフィール」と
「ログインしている他人から見るID1さんのプロフィール」と
「ID1さんが自分で見るマイページのプロフィール」と
「ID1さんが自分で見る公開用ページのプロフィール」と
SNSの管理者が見るID1さんのプロフィール」って
必要な情報が違うかもしれないよね…。
欲しいデータを絞り込むとかどうとかじゃなくて、権限によって取得自体できないようにしたいよね…。

それどうするんだ?
GET /users/1/ で全部賄うもんなのか?
ログインしている人からは固有のTokenを送ってもらうとすれば確かに判別自体はできるけど…。
まず GETで一覧と詳細の2機能ある時点でそこそこ気持ち悪くない??
そこに権限によっての細かい制御入れるの???
そもそもサイトの仕様をもっとシンプルにしろよって話なの?????教えてエロい人。

CodeIgniter入門 #7:データベースの操作<クエリビルダ編・参照系の巻>

CodeIgniter入門シリーズ カテゴリーの記事一覧 - アナログCPU:5108843109


正直PHPよりSQLの方が得意なくらいでクエリビルダは大嫌いなんですが、
仕事で使うことは多い(のにいまいちわかっていない)ので
ひたすら公式マニュアル見て書いて動かしてみました。
クエリビルダクラス — CodeIgniter 3.2.0-dev ドキュメント
いろいろ動かしてみたのを全部書くと上記の公式マニュアルと大体同じになっちゃうので、軽くまとめ直してみました。
それでも長くなったので今回はSELECT編。

今回やること

  • クエリビルダを使ってSELECTしてみる
  • WHERE, LIMIT, ORDER, JOIN, 集計系などある程度カバーする

先に書いておくと

今回と次回でクエリビルダの書き方をいろいろ試していますが、
仕事ではともかく、趣味の開発ではSQL直書きでいくことにします。

上記公式マニュアルには、

クエリを自分で書きたい場合は、データベース設定ファイルでこの クラスを使用できないようにすることもできます。そうすることで、コアの データベースライブラリとアダプタに、少ないリソースを有効活用させることができます。

とありますので、クエリビルダを使わない場合は config/database.php

// クエリビルダを使わないのでFALSEにする
// $query_builder = TRUE;
$query_builder = FALSE;

こうしてやればよさそうです。

とりあえず

まあそういうわけで「SQL直書きで良くねえ?」という感想です。

例えば、「SELECT * FROM `user`」なら

// SQL直書き
$result1 = $this->db->query("SELECT * FROM `user`")->result();
// クエリビルダ
$result2 = $this->db->get('user')->result();

となり、まあ短いしテーブル名だけ書けばいいだけだし…というわけですが…

…いや、実務でそんなクエリほとんど書かないじゃん?

ややこしいクエリになればなるほど、クエリビルダではわけがわからない見た目になる印象でした。
でもやってみる。

現実的(実務で使う程度)なレベルの簡単なSELECT

とりあえずSELECTしてみる

実務で使う程度に簡単なクエリといえばこれくらいですかね。
退会していないユーザーをID降順に50件取得、という感じです。
ページャ想定しての50件として、全部で何件あるかも知れればいいですね。

SELECT `id`, `name`
FROM `user`
WHERE `delete_flag` <> 1
ORDER BY `id` DESC
LIMIT 50, 0

これをクエリビルダで書いてみるとこうなりました。

$this->db->select("id, name");
$this->db->from("user");
$this->db->where("delete_flag <>", 1); // 比較演算子はイコールなら省略できますが、それ以外ならこんな感じになります
$this->db->order_by("id", "DESC"); // ※第二引数は省略するとASCになります
$this->db->limit(50);
$this->db->offset(0);
$result = $this->db->get()->result(); // $result に戻り値が入る

うーん…?
既に微妙な感じが…。
もう少し省略することも可能なのですが(from, limit, offsetは省略してgetの引数に入れることもできる)
省略すればするほど、省略できない書き方になったときに色んな書き方が混在しやすくなります。

全部で何件あるかを調べるには

クエリビルダではできなさそうでした(さっそくつまづく)
やり方あるのかな…ご存じの方は教えてください。
ということでただのMySQLの話になってしまうのですが、以下のように書き換えればOKです。

$this->db->select("SQL_CALC_FOUND_ROWS `id`, `name`", FALSE); // ここを書き換え
$this->db->from("user");
$this->db->where("delete_flag <>", 1);
$this->db->order_by("id", "DESC");
$this->db->limit(50);
$this->db->offset(0);
$result = $this->db->get()->result();
$count_all = $this->db->query('SELECT FOUND_ROWS() AS cnt')->row()->cnt; // $count_all に全件数

selectの第二引数のFALSEは、簡単に言うと第一引数のカラム名をバッククォートでくくるかどうかです。
これを省略(TRUE)していると、内部では「SELECT `SQL_CALC_FOUND_ROWS` `id`, `name` ... 」となってしまいます。
なのでここではFALSEを指定の上、必要なところは自分でバッククォート入れています。

参考: [CodeIgniter] SQL_CALC_FOUND_ROWSを使って一覧と件数を一発で取得する │ M0DE

SELECTで取得してくるカラムやFROMで指定するテーブルに別名を付ける

select内やfrom内にベタ書きでOKでした。

// SELECT `A`.`id` AS `A_id`
// FROM `user` AS `A`
$this->db->select("A.id AS A_id");
$this->db->from("user AS A");
複数指定したいときは…

例えば、WHERE句で複数条件をANDでつないで指定したいとき。
ORDER句やGROUP句の中身を複数並べたいとき。
SELECTの中身が長くなるのでバラして書きたいとき。
こんな感じで並べるだけでOKでした。

// SELECT `id`, `name`
// FROM `user`
// WHERE `delete_flag` <> 1
//   AND `address` = 'tokyo'
// ORDER BY `id` DESC
//         ,`age` ASC
// LIMIT 0, 50
$this->db->select("id");
$this->db->select("name");
$this->db->from("user");
$this->db->where("delete_flag <>", 1);
$this->db->where("address", "tokyo");
$this->db->order_by("id", "DESC");
$this->db->order_by("age", "ASC");
$this->db->limit(50);
$this->db->offset(0);
複数指定したいときは… その2

配列を渡せるケースもあります。

// WHEREだけ抜粋:WHERE `age` < 20 AND `address` = 'tokyo')
$this->db->where(array("age <" => 20, "address" => "tokyo"));
SQL直書きできるケースも多い
$this->db->select("id, name");
$this->db->from("user");
$this->db->where("delete_flag <> 1 AND address = 'tokyo'");
$this->db->order_by("id DESC, age ASC");
$this->db->limit(50);
$this->db->offset(0);

個人的には「は???」って印象でしかないのですが
このような感じで引数にSQL文をそのまま渡すこともできることが多いようです。

WHERE句のバリエーションがすごい

id = 1
$this->db->where("id", 1);
id > 1
$this->db->where("id >", 1);
id > 1 AND age < 20
$this->db->where("id >", 1);
$this->db->where("age <", 20);
id > 1 OR age < 20
$this->db->where("id >", 1);
$this->db->or_where("age <", 20);

えっこれ気持ち悪くない?自分だけ?

id IN (1, 2, 3)
$this->db->where_in("id", array(1, 2, 3));
id IN (1, 2, 3) OR address IN ('tokyo', 'osaka')
$this->db->where_in("id", array(1, 2, 3));
$this->db->or_where_in("id", array("tokyo", "osaka"));
id NOT IN (1, 2, 3) OR address NOT IN ('tokyo', 'osaka')
$this->db->where_not_in("id", array(1, 2, 3));
$this->db->or_where_not_in("id", array("tokyo", "osaka"));

or_where_not_inて。

部分一致検索
// 名前に「ほげ」を含み、住所が「東京」で始まり、メールアドレスが「@gmail.com」で終わる
// (name LIKE '%ほげ%' AND address LIKE '東京%' AND mail LIKE '%@gmail.com')
$this->db->like('name', 'ほげ', 'both');
$this->db->like('address', '東京', 'after');
$this->db->like('mail', '@gmail.com', 'before');

第三引数は、bothの場合は省略できる模様。
もう書かないけど、whereと同じようなノリで or_like, not_like, or_not_like もあります。

集計やグルーピング

GROUP BY
// SELECT group FROM user GROUP BY group
$this->db->select("group")
$this->db->from("user")
$this->db->group_by("group");
DISTINCT
// SELECT DISTINCT group FROM user
$this->db->select("group");
$this->db->from("user");
$this->db->distinct();
集計関数を使う

MIN, MAX, AVG, SUMといった集計関数もちゃんと用意されてました。

// SELECT MAX(age) AS max, MIN(age) AS min, ...
// FROM user
// GROUP BY `group`
$this->db->select("group"); 
$this->db->select_max("age", "max"); // 第二引数は別名(MAX(age) AS max)
$this->db->select_min("age", "min");
$this->db->select_avg("age", "avg");
$this->db->select_sum("age", "sum");
$this->db->from("user");
$this->db->group_by("group");

…COUNT関数は…?(調べてみても見当たらなかった)
仕方ないのでベタ書き戦法です。かゆいところに手が届かないなあ。

$this->db->select("COUNT(`id`) AS `count`", FALSE);
$this->db->from("user");
$this->db->group_by("group");
HAVINGする

HAVINGはサブクエリ使って書くより遅くなりがちなんですけど
HAVING句の使い方と速度検証 - アナログCPU:5108843109
クエリビルダだと複雑なクエリが余計に読みにくくなるし書くのも嫌になってくるので、もうHAVINGでいいかって気になってくる。
いいのかそれで。

// グループごとの合計人数が5人より多いレコードだけSELECT
$this->db->select("COUNT(`id`) AS `count`", FALSE);
$this->db->from("user");
$this->db->group_by("group");
$this->db->having("count >", 5);

WHEREと同じく、比較演算子がイコールなら省略してOKです。

そしてやっぱりor_havingも存在する。

JOINする

// SELECT user.id AS user_id, log.id AS log_id
// FROM user
// LEFT JOIN log ON user.id = log.user_id
$this->db->select("user.id AS user_id, log.id AS log_id");
$this->db->from("user");
$this->db->join("log", "user._id = log.user_id", "left");

join句の第三引数で「left」を指定していますが、もちろん必要に応じてinnerも指定できます。
rightとかもあるけど使わんかろ。

複数JOINする場合は例によって並べるだけでした。

// SELECT user.id AS user_id, log.id AS log_id, log_category.id AS log_category_id
// FROM user
// LEFT JOIN log ON user.id = log.user_id
// LEFT JOIN log_category ON log.category = log_category.id
$this->db->select("user.id AS user_id, log.id AS log_id");
$this->db->from("user");
$this->db->join("log", "user._id = log.user_id", "left");
$this->db->join("log_category", "log.category = log_category.id", "inner");

また、ON句の条件を複数にしたい場合は…クエリビルダっぽい書き方は見つけられませんでした。
ベタ書きで動きます…。

$this->db->join("log", "user._id = log.user_id AND delete_flag <> 1", "inner");

とても複雑なクエリを書く

めんどくさいので公式マニュアルからの引用です。

クエリのグルーピングでは、 WHERE 句を括弧で囲むことでグループを作ることができます。 これにより複雑な WHERE 句のクエリを作ることが可能です。
例:

$this->db->select('*')->from('my_table')
        ->group_start()
                ->where('a', 'a')
                ->or_group_start()
                        ->where('b', 'b')
                        ->where('c', 'c')
                ->group_end()
        ->group_end()
        ->where('d', 'd')
->get();

// 次のようになります:
// SELECT * FROM (`my_table`) WHERE ( `a` = 'a' OR ( `b` = 'b' AND `c` = 'c' ) ) AND `d` = 'd'

お、おう…。
引数にクエリをベタ書きするのを避けるならこうなるんですかね。
これくらいのネストができるクエリって結構あると思うんですけど、慣れてないと読みにくいっすね…。

クエリビルダが嫌いな理由

突然の愚痴

データベースのエラーとかスローログとかを元に調査するとき、検索性が悪くて困る…。
自分が書いたコードなら大体の場所は見当つくかもしれないけど、そうでないと結構むずい。
これがSQL直接書いてるだけなら特徴的な部分を抜き出して一発grepかけるだけなんだけど。
何かいい方法あるのかなこれ。

あと、クエリビルダ自体が悪いわけじゃないんだけど、
こっちのほうが書きやすい・見やすいって言う人は「そもそもSQLやデータベースそのものがよく分かっていない」っていうケースが多い。
SQLを理解せずに見よう見まねのクエリビルダで書かれるとヤバいコードが生産されがちで困る。
(ありがちなのが、JOINの概念が分からなかったのか適当にSELECTしてきたデータをアプリ側でつなぎ合わせてるとか、集計関数を知らないのか同じくアプリ側で集計しなおしてるとか)
データベースの知見がある上でクエリビルダを使いこなしている方も当然いるんだろうけど、いくつかの現場を見た限り出会うことはなかったのでたぶん少数派。


うーん、使うメリットがわからん…。今は嫌いだから使いたくないけどメリットがあるのならもっと考えたいから誰か教えてくれ。
データベースの種類ごとまるっと乗り換える(MySQLからPostgreSQLにするとか)場合は修正箇所が少なくて済む、とかもあるんだろうけど、日頃の使い勝手悪い方が困るわ。そんな機会そうそうないでしょ。
(と言いつつ今やってる案件ではあるんですが(乗り換えではないけど、ほとんど同じシステムのDB違い版がある)結局レアケースだろうし…)
MySQLだろうがOracleだろうが書き方が変わらないので覚えることが少なくて済む…かもしれないけど、クエリレベルの話なんだからせいぜい方言差だし、どっちかというとフレームワークごとのクエリビルダの違いの方が大きいんじゃないのか??
SQL知らなくても使えるっていうのは先述のとおりメリットよりデメリットの方が大きそうだし。

複数のファイルをフォルダごとコピーするスクリプト

今の現場、開発サーバーにソースコード上げる作業がかなりめんどくさいので、ちょっとだけスクリプト化しました。

gitにコミットしても自動で反映されるわけじゃない。
しかもFTPとかじゃなくてリモート接続。

なので、

  • サーバー側の変更対象ファイルをバックアップしておく
  • バックアップしたファイルとローカル側のファイルを差分チェックする
  • ローカル側のファイルをサーバーに設置する
  • 動作確認とかする
  • 場合によっては元に戻したりする

とかいう手順を踏むわけです。

このうちサーバー側の必要ファイルバックアップと、ローカルからサーバーに持っていくファイルを抽出する作業が地味にめんどくさいのですが
この二つの作業が大体同じなので、その部分だけスクリプト化してみました。
ローカルもサーバーもWindowsなのでまあVBSで。

やっていることは、「テキストファイルに一覧化したファイルを全部コピーして、フォルダ構造も再現しつつ指定したフォルダにペースト」です。
例えば、

C:\hoge\fuga\a.txt
C:\hoge\piyo\b.txt

というリストを作り、「C:\backup\」にコピーするようにしてスクリプトを実行すると、

C:\backup\hoge\fuga\a.txt
C:\backup\hoge\piyo\b.txt

にコピーされてくる、という感じです。
フォルダ階層が深いときはコピー先も無駄に深くなるけど…。

これさえあれば、このスクリプトでバックアップ対象と更新対象を抽出して、そのフォルダごとWinmergeで確認して、更新対象フォルダをサーバー側に放り込むだけで済みます。
もちろん元に戻すときはバックアップしておいたフォルダを放り込むだけ。
(とはいえフォルダを放り込むだけなので、更新にしろ元に戻すときにしろ、削除が発生する部分は手作業ですが…)

使い方

  • テキストファイルに、コピーしたいファイルをフルパスの改行区切りで並べておく
  • そのテキストファイルのパスと、コピー先のディレクトリパスを下記スクリプト内で設定する
  • スクリプト実行

プログラム

続きを読む

CodeIgniter入門 #6:データベースの操作

CodeIgniter入門シリーズ カテゴリーの記事一覧 - アナログCPU:5108843109


まあ公式マニュアルにまとまってるんですけどね。
データベースへの接続 — CodeIgniter 3.2.0-dev ドキュメント
クエリ — CodeIgniter 3.2.0-dev ドキュメント

今回やること

  • コントローラからデータベースの操作(SELECT、INSERT、UPDATE、DELETE)を行う
  • プレースホルダを使う
  • エスケープする

データベース

データベースはこんな感じ。
ユーザーマスタ的なものを想定。
とはいえお試しなので超シンプルに、users テーブルに、連番の id と文字列を入れる name で。

CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(16) NOT NULL , PRIMARY KEY (`id`)) ENGINE = InnoDB;

データベースへの接続

以前書いたことと少しかぶるので簡単に。
まず、設定値は config の database.php へ。
そこだけ設定しておけば、コントローラ内に

$this->load->database('default');

と書くだけで接続できます。

ほとんどのコントローラで接続を行う場合など、これをいちいち書きたくない場合は config の autoload.php のライブラリに「database」を追加しておけばOKです。

$autoload['libraries'] = array(
	'database',
	// 他にもライブラリ系を毎回ロードしておきたい場合はここに追加するものと思われる
);

INSERTする

テストデータを手動で入れるのすら面倒なので、INSERTから作ります。

今回はUserコントローラを用意し、その中に insert 関数を設置しました。
/user/insert/ でアクセスできます。

一番シンプルなのはこれだけ。

public function insert()
{
    $sql  = "INSERT INTO `user` (`name`) ";
    $sql .= "VALUES('なまえ') ";
    $this->db->query($sql);
}

SELECT

次はINSERTされているかどうか確認するためにもSELECT作ります。

とりあえず全行SELECTしてくるやつ。

public function select()
{
    $sql  = "SELECT `id`, `name` ";
    $sql .= "FROM `user` ";
    var_dump($this->db->query($sql)->result());
}

UPDATE

無事にINSERTしたやつがSELECTできることを確認できたので、次はUPDATEしてみます。
変わり映えしないですが…。

public function update()
{
    $sql  = "UPDATE `user` ";
    $sql .= "SET `name` = '変更後だよ!!!!!' ";
    $sql .= "WHERE `id` = 3 ";
    $this->db->query($sql);
}

SELECT動かして変更されることを確認。

DELETE

追加・変更・取得ときたので最後は削除。

public function delete()
{
    $sql  = "DELETE FROM `user` ";
    $sql .= "WHERE `id` = 3 ";
    $this->db->query($sql);
}

これで一通り動かしてみることができました。

プレースホルダを使う

値を動的に組み込みたい場合はプレースホルダを使います。
もちろんエスケープ等はCodeIgniterさんがやってくれるのでこれだけで安全になります。
INSERTのコードの、VALUES句の中身を動的にしてみます。

public function insert()
{
    $sql  = "INSERT INTO `user` (`name`) ";
    $sql .= "VALUES(?) ";
    $this->db->query($sql, array("ほげ"));
}

query の第二引数に渡した値が「?」に入ってくれます。

複数にも当然対応しています。
例えばSELECTで検索条件やページャ用の取得件数制御を入れてみます。

public function select()
{
    $sql  = "SELECT `id`, `name` ";
    $sql .= "FROM `users` ";
    $sql .= "WHERE `id` > ? ";
    $sql .= "LIMIT ?, ? ";
    $arg_list = array(
        1,
        0,
        5,
    );
    var_dump($this->db->query($sql, $arg_list)->result());
}

この場合はクエスチョンの順番通りに当てはめられていきますので、「WHERE `id` > 1 LIMIT 0, 5」となります。

明示的にエスケープする

escape メソッドを使えばクエリの文字列にエスケープした文字列を結合することもできます。
動的な値を入れるときはプレースホルダが使えればOKなんですけど、
テーブル名やカラム名を動的にするときなど場合によっては必要になるかも…?

public function insert()
{
    $sql  = "INSERT INTO `user` (`name`) ";
    $sql .= "VALUES(" . $this->db->escape("'',''ふが~~") . ") "; // なんか危なそうな文字列をエスケープして結合
    $this->db->query($sql);
}

CodeIgniter入門 #5:ログが記録されるようにする

CodeIgniter入門シリーズ カテゴリーの記事一覧 - アナログCPU:5108843109

今回やること

  • エラーページを設置する
  • エラーログが記録されるようにする

謎エラーが出た

なんかね、ちょっとコントローラがつがつ書いてたら、なんかミスってたらしくエラーが出たんですよ。
エラー内容読む前に「あそこ間違えてるわ」と気付いて直してみたものの、エラーが消えない。

Warning: include( ... \application\errors\html\error_php.php): failed to open stream: No such file or directory in  ... \system\core\Exceptions.php on line 268

… application/errors/html/error_php.php が存在しない?
ええ、ないですけど。
最初からそんなのないんですけど。

自前でとりあえず適当に設置してみたらそれが呼び出されました。
どういうこと???勝手に呼び出しておいて自分で設置する前提???
エラー処理 — CodeIgniter 3.2.0-dev ドキュメント

コードを漁ってみると、error_phpの他に、error_general, error_db, error_404, error_exception が呼び出される可能性があるっぽい?
あと、htmlディレクトリの他にcliディレクトリも。
うーん。また必要になったら考えることにする。同じように設置すればいいはず。

エラーログが更新されてなかった

で、自分で設置したファイルはほぼ空なので、結局本来のエラー内容が不明。
logsディレクトリを見てみても特に何も入っていない。

これは config/config.php 内に設定があったので以下のように調整。

$config['log_threshold'] = 1;

これでlogsディレクトリ以下にエラーログが記録されるようになりました。