二度忘れた事を三度忘れないようにする

しがないフリーランスIT系エンジニア

CentOS 7 でswap領域を追加する

AWSで小さいインスタンスを使っていると、updateなり一時的作業なりでメモリ不足のエラーが出ることがままあるのでそういう時用に。

参考

dev.classmethod.jp

#!/bin/sh

SWAPFILENAME=/swap.img
MEMSIZE=`cat /proc/meminfo | grep MemTotal | awk '{print $2}'`

if [ $MEMSIZE -lt 2097152 ]; then
        COUNT=$[${MEMSIZE} / 512]
elif [ $MEMSIZE -lt 8388608 ]; then
        COUNT=$[${MEMSIZE} / 1024]
elif [ $MEMSIZE -lt 67108864 ]; then
        COUNT=$[${MEMSIZE} / 2048]
else
        COUNT=4096
fi

dd if=/dev/zero of=${SWAPFILENAME} bs=1M count=${COUNT} && chmod 600 ${SWAPFILENAME} && mkswap ${SWAPFILENAME} && swapon ${SWAPFILENAME}

CentOS7の場合、ddコマンドでファイルを作る必要がるので、その辺を弄ったスクリプト担ってます。

Rails4のリレーションを使い分けたい

個人的にはSQL文を組み立てるのは苦手ではありつつも、SQL文をある程度書けるようになるべきだと思っています。とはいいつつも、様々なフレームワークで実装されているモデルの機能というのは素晴らしいと思っています。特にRails4のhas_one,has_manyといったリレーション機能は非常に便利だと思います。

今回、Rails4でリレーションの設定をしたモデルを使っていて、発行されているクエリを見ていると非常に効率の悪いクエリが発生しているのに気付きました。

ざっくりいうと、SNSのようにユーザ一覧ページを表示した時に、一覧表示に必要のない参照先のテーブル情報まで引っ張っていました。一覧で100人表示したら不要なSELECTのクエリが100回(リレーション数によっては200,300となります)発生する動作をしていたので、流石に無いなと思って調べてました。

DB設計がなってない、と言われればそこまでなんですがそこは置いておいて。。。Rails4ではリレーションの設定をした場合、find_by_sqlとかなんとか使っても参照先のテーブルから情報を引っ張るようです。

あまりスマートではないですが、modelファイルをもう一個作って今回は回避しました。

class Users < ActiveRecord::Base
  has_one :profiles
  has_many :followers
# 略
end
class UsersNonRelational < ActiveRecord::Base
  self.table_name = 'users'
end

親テーブルの一覧が欲しいだけなんだけど、、、って時の対応でした。もっとスマートな方法・機能があれば教えてください。。。

Phalconを使う際に最初にすることリスト

どのようなサービスであっても個人的にほぼ必須と思っている設定をつらつら書き留める記事です。
こんなのもあるといいYOってのがあれば教えてください。

環境
CentOS 7
Phalcon 3.0.1
PHP 7.0.10

名前空間(Namespace)

使わない人はとことん使わないイメージですが、個人的にはあまりデメリットを感じないので必ず利用するようにしてます。

// app/config/loader.php
<?php

$loader = new \Phalcon\Loader();

$loader->registerNamespaces([
        'Appname\Controllers' => $config->application->controllersDir,
        'Appname\Models' => $config->application->modelsDir
]);

$loader->register();

ローダに名前空間ディレクトリの関係を定義します。「'Appname' => 'app/'」みたいにするとコントローラとかモデルも同的に定義・読み込みするらしいのですが、少々面倒でも細かく定義するようにしています。サブモジュールとかで階層重なるのを防げたりするので。

// app/config/services.php
// 以下を追記(ルーティング設定により不要
$di->set('dispatcher', function () {
    $dispatcher = new Dispatcher();
    $dispatcher->setDefaultNamespace('Appname\Controllers');
    return $dispatcher;
});

デフォルトのnamespaceをここで指定すると、次のルーティング設定時にnamespaceの指定を省略できます。逆に異なるnamespaceを多用するのであればあまりメリットはでないかもしれません。

ルーティング

ルーティングの実装方法はいくつか用意されていますが、極力読みやすく厳しく設定できると思っている方法を使ってます。といっても公式で紹介されている方法ですが。あと正規表現は適当です。

// app/config/router.php
<?php

use Phalcon\Mvc\Router;
use Phalcon\Mvc\Router\Group as RouterGroup;

// デフォルトの動作を無効化
$router = new Router(false);
// 末尾のスラッシュを自動的に取り除く
$router->removeExtraSlashes(true);

// デフォルトルート
$router->add('/', ["controller" => "index", "action" => "index"]);
// 404のパス設定
$router->notFound(["controller" => "index", "action" => "route404"]);

// ここから
$blog = new RouterGroup(['controller' => 'blog']);
$blog->setPrefix('/blog');
$blog->addGet('', ['action' => 'index']);
$blog->addGet('/edit/{id}', ['action' => 'edit']);
$blog->addPost('/edit', ['action' => 'create']);
$router->mount($blog);
// ここまでを1セット

return $router;
// app/config/services.php
// 以下を追記
$di->setShared('router', function () {
    $config = $this->getConfig();
    require $config->application->routerPath; // configにパスを書いています
    return $router;
});

Groupを使った時のルート指定は「''」とすることで定義可能。
なお、わざと1行で書くようにして見通しをよくしてます。コーディング規約的におかしいとか言われた事もありますが、ルーティング定義はPHPで書かれているとはいえ、個人的には見易さを重視します。
あと、書き方が冗長だ、こうすれば動的に定義できてスッキリする、というのはわかっていますが、明示的に設定した以外の動作を取って欲しくない(特にチームで開発している時)ので、パスやメソッドとか細かく書いてます。

Viewの無効化(View不要時)

APIとか作る際にはViewを無効にするだけで、標準のMVCを使うようにしています。これは学習コストの問題だけなので効率的か、といわれると効率的ではないでしょう。

// app/controllers/ControllerBase.php
    public function initialize()
    {
        $this->view->disable();
    }

Viewに処理が到達するまでに実行されればいいので、どこに記述しても問題はないです。APIを作っている時は初期化時に実行させれば記述漏れとかなくなるので、親コントローラのinitializeに記述していることが多いです。

ログ出力

とりあえず実装して、足りない部分はそれぞれのコントローラやモデルで実装します。

// app/config/services.php
// 以下を追記
use Phalcon\Logger;
use Phalcon\Logger\Adapter\File as FileAdapter;
$di->setShared('logger', function () {
    $config = $this->getConfig();
    $logger = new FileAdapter($config->application->applogPath, ['mode' => 'w']);
    return $logger;
});

手っ取り早いのでファイル出力のロガーを実装。syslogやらもあるので必要に応じて変更する。

Exception

エラーはExceptionで処理する事が多かったので、どこのExceptionでも一箇所でコントロール出来るようにします。個人的には設計しやすいのでよく使います。(主要部分のみコード記載)

// app/config/services.php
// 以下のように編集
use Phalcon\Mvc\Dispatcher as MvcDispatcher;
use Phalcon\Dispatcher;
use Phalcon\Events\Manager as EventsManager;
use Phalcon\Mvc\Dispatcher\Exception as DispatchException;
use Appname\Controllers\ExceptionModule;
$di->setShared('dispatcher', function () {
    $dispatcher = new MvcDispatcher();
    $dispatcher->setDefaultNamespace('Appname\Controllers');

    $eventsManager = new EventsManager();

    // Attach a listener
    $eventsManager->attach("dispatch:beforeException", new ExceptionModule());

    $dispatcher->setEventsManager($eventsManager);

    return $dispatcher;
});
// app/controllers/ExceptionModule.php
<?php
namespace Appname\Controllers;

use Phalcon\Events\Event;
use Phalcon\Mvc\Dispatcher;
use Phalcon\Mvc\Dispatcher\Exception as DispatchException;

class ExceptionModule
{
    public function beforeException(Event $event, Dispatcher $dispatcher, $exception)
    {
        // Handle 404 exceptions
        if ($exception instanceof DispatchException) {
            $dispatcher->forward(array(
                'controller' => 'index',
                'action'     => 'show404'
            ));
            return false;
        }

        // Handle other exceptions
        $dispatcher->forward(array(
            'controller' => 'index',
            'action'     => 'show503'
        ));

        return false;
    }
}

DIのところでbeforeExceptionを独自のものを指定する。いちおう公式に載っている方法ですが、Exception処理部分を別ファイルにして管理しやすいようにしています。(以前、docでもnewして外部ファイルで管理する方法をもうちょっと細かくコード付きで書いてたような記憶がある)
基本的にはinstanceofのif文を必要なだけ追記してException毎に処理を分けていきます。開発時は最初にログ出力処理を書いてデバッグしやすくしたりしてます。

PhalconのRoutingとViewの関係

独自にルーティングを定義する際に、公式docを参考(なので大部分を割愛)に以下のように定義しました。

<?php

use Phalcon\Mvc\Router;

$router = new Router(false);

$router->add( "/", [
"namespace"  => 'Appname\Controllers',
"controller" => "index",
"action"     => "route"
]);

return $router;

ここで「IndexController.php」の「routeAction」が参照する末端のViewは「app/views/index/route.volt」です。
しかしながら、上記ルーティングの「"controller" => "index",」で指定している「index」を「Index」(頭文字を大文字)とすると、参照しにいくViewは「app/views/Index/route.volt」となりました。
実行されるコントローラは大文字小文字関係なく「IndexController.php」が実行されるが、参照しにいくViewはルーティング時の文字列に影響する模様。「"action" => "route"」は大文字小文字区別されるのでちょっと注意が必要。

これはPHPのクラス名は大文字小文字の区別しなくても動作する仕様からくるものだと思われる。

Phalcon3 + PHP7 導入手順

気づいたらPhalcon3がリリースされていたので、PHP7と一緒に試してみました。

・環境
CentOS7(AWS)
PHP 7.0.10
Phalcon 3.0.1
nginx 1.8

$ sudo yum install epel-release centos-release-scl-rh
$ sudo yum install https://centos7.iuscommunity.org/ius-release.rpm
$ sudo yum install rh-nginx18 php70u-devel php70u-mysqlnd php70u-fpm gcc libtool pcre-devel php70u-opcache php70u-json
$ curl -s https://packagecloud.io/install/repositories/phalcon/stable/script.rpm.sh | sudo bash
$ sudo yum install php70u-phalcon
$ php -m
[PHP Modules]
phalcon

公式doc通りではコンパイルエラーが発生し解決できず、試行錯誤しているとphalconブログなるものがあって、そこのレポジトリを利用。なお、公式docでインストールしているパッケージだけでは読み込まれないのでphp70u-jsonを追加でインストールする必要があります。

blog.phalconphp.com

phalcon - Repositories | packagecloud

インストールは以上で完了なのであとはnginxとphp-fpmの設定を各々の環境に合わせてセットしてください。

Unicornをsystemdで管理する

Rails初心者が典型的なハマり方をしたと思うので戒めの意を込めて。

前提
CentOS 7系
・SCL版ruby2.3
・SCL版nginx1.8

nginxはscl版でも標準でsystemctlコマンドで制御できるので特に問題なし。

$ sudo systemctl enable(start) rh-nginx18-nginx

unicronは自分でserviceファイルを記述する必要があるが、sclのrubyを使っているので普通にかくと環境変数がことなることでエラーになります。

ということでserviceファイルは以下みたいな感じで記述

[Unit]
Description=Unicorn Server

[Service]
WorkingDirectory=/var/www/html/
SyslogIdentifier=unicorn
PIDFile=/var/www/html/tmp/pids/unicorn.pid
User=nginx
Restart=always

ExecStart=/usr/bin/scl enable rh-nodejs4 rh-ruby23 -- unicorn_rails -c config/unicorn.rb -E development -D
ExecStop=/usr/bin/kill -QUIT $MAINPID
ExecReload=/bin/kill -USR2 $MAINPID

しかし、ちゃんと起動しない。コマンド直打ちでは問題ないことはわかっているので、ユーザ周りを睨んで調査していると、gemで入れたアレコレがみつからないことがわかった。(経緯のメモを忘れた)

原因 「bundle install」でパスを未指定

これは完全にbundleの理解不足でした。パスを未指定で実行していたことで、カレントユーザのホームディレクトリにインストールされるため、systemdで実行した際に「色々ネーヨ」ってなってました。なので、パスを指定してインストールした以後は「bundle exec」をつけて各種コマンドを実行することでbundle installしたgem群の読み込みをよしなにしてくれた。

ということでserviceファイルのExecStartのコマンドを次に修正することで無事解決

ExecStart=/usr/bin/scl enable rh-nodejs4 rh-ruby23 -- bundle exec unicorn_rails -c config/unicorn.rb -E development -D

CarrierWaveを利用したModelの挙動について

幅を広げるのにRoRを勉強し直すのに簡単なアプリを作ってみたらはまったので、メモ。

・とりあえず
プロフィール的なページを構成するのに、メインとなるプロフィールテーブルとhas_oneで2つ別テーブルを関連づけしてました。画像は特定のディレクトリに保存する形をとって、ファイル名をプロフィールテーブルに保存をしようとした。

・そのまえに
RoRのhas_oneで更新をかけると、子テーブルのほうに続々と不要レコードがたまってきたので更新のタイミングで直前のレコードを削除するように、プロフィールモデルをいじってました。

ざっくり)after_initialize時に子テーブルのidを取得→after_commit時に削除

ネットで調べるとhas_manyであれば別の方法があるっぽいけど、うまく動作しなかったのでこうしました。

参考:
has_many側の不要なやつはdeleteして更新したい - Oh! My! Enter! 〜バッチを起動しようと勢いよくキーを叩いたら、それはシフトキーだった〜


・本題
プロフィール編集フォームにて、画像以外の項目で編集しても問題なかったのに画像をアップロードしようとするとエラーが出た。原因は上記のinitialize時のid取得でエラーが出ていたが、画像の有無で挙動が変わっていた。

・結果?
とりあえず「mount_uploader :image, ImageUploader」としたカラムを更新する時は再度initializeが発生する。(おそらく新しくインスタンスを生成しているのではと思う

なお、SQLの実行内容をみていると次のようになっていた。

# 画像無し
Started PATCH "/my_page" for 127.0.0.1 at 2016-08-22 02:48:31 +0000
Processing by UserProfilesController#update as HTML
  Parameters: {"utf8"=>"", -略- "commit"=>"Update User profile"}
  UserProfile Load (0.2ms)  SELECT  -略- LIMIT 1
# ここらへんでafter_initialize動作しているっぽい
  UseService Load (0.2ms)  SELECT  -略- LIMIT 1 # has_oneその1
  UseLang Load (0.3ms)  SELECT  -略- LIMIT 1  # has_oneその2
# ここらへんでafter_initializeが終わっているぽい
BEGIN
  SQL (0.3ms)  has_oneその1をアップデート(ここで古い情報がゴミレコードになる
  SQL (0.2ms)  has_oneその2をアップデート(ここで古い情報がゴミレコードになる
  UserProfile Exists (0.2ms)  親テーブルの重複チェック?
  UserProfile Exists (0.2ms)  親テーブルの重複チェック?
  SQL (0.2ms)  UPDATE 親テーブルのUserProfileで画像以外のカラムを編集
  SQL (0.2ms)  INSERT has_oneその1に更新情報を格納
  SQL (0.2ms)  INSERT has_oneその2に更新情報を格納
COMMIT
# after_commitが動作
  UseService Load (0.2ms)  SELECT  has_oneその1の古い情報を取得
BEGIN
  SQL (0.3ms)  DELETE has_oneその1の古い情報を削除
COMMIT
  UseLang Load (0.2ms)  SELECT  has_oneその2の古い情報を取得
BEGIN
  SQL (0.3ms)  DELETE has_oneその2の古い情報を削除
COMMIT
# after_commitが終了
Redirected to http://++++
Completed 302 Found in 29ms (ActiveRecord: 9.8ms)
# 画像有り
Started PATCH "/profile" for 127.0.0.1 at 2016-08-22 02:49:29 +0000
Processing by UserProfilesController#update as HTML
  Parameters: {"utf8"=>"", -略- "commit"=>"Update User profile"}
  UserProfile Load (0.2ms)  SELECT  -略- LIMIT 1
# ここらへんでafter_initialize動作しているっぽい
  UseService Load (0.2ms)  SELECT  -略- LIMIT 1 # has_oneその1
  UseLang Load (0.3ms)  SELECT  -略- LIMIT 1  # has_oneその2
# ここらへんでafter_initializeが終わっているぽい
BEGIN
  SQL (0.3ms)  has_oneその1をアップデート(ここで古い情報がゴミレコードになる
  SQL (0.2ms)  has_oneその2をアップデート(ここで古い情報がゴミレコードになる
  UserProfile Exists (0.2ms)  親テーブルの重複チェック?
  UserProfile Exists (0.2ms)  親テーブルの重複チェック?
  UserProfile Load (0.2ms)  SELECT  -略- LIMIT 1
# ここで再びafter_initializeが動作している?
  UseService Load (0.2ms)  SELECT  -略- LIMIT 1 # この時点ではuse_serviceはnil
  UseLang Load (0.2ms)  SELECT  -略- LIMIT 1 # この時点ではuse_langはnil
# ここで再び動作していたafter_initializeが終わっている?
  SQL (0.2ms)  UPDATE 親テーブルのUserProfileに画像ファイルのファイル名を格納
  SQL (0.2ms)  INSERT has_oneその1に更新情報を格納
  SQL (0.2ms)  INSERT has_oneその2に更新情報を格納
COMMIT
# after_commitが動作
  UseService Load (0.2ms)  SELECT  has_oneその1の古い情報を取得
BEGIN
  SQL (0.3ms)  DELETE has_oneその1の古い情報を削除
COMMIT
  UseLang Load (0.2ms)  SELECT  has_oneその2の古い情報を取得
BEGIN
  SQL (0.3ms)  DELETE has_oneその2の古い情報を削除
COMMIT
# after_commitが終了
Redirected to http://++++
Completed 302 Found in 40ms (ActiveRecord: 10.5ms)

パーフェクトRoR買おうかな。。。