2011年12月12日月曜日

日本人ならではのCakePHPへの貢献(CakePHP Advent Calendar 2011)

この記事はCakePHP Advent Calendar 2011に参加しています。13日目です。

前日は@slumbers99さんのPrefixルーティングとthemeのススメでした。3キャリア対応サイトをCakePHPで作った時に利用されたというPrefixルーティングとテーマのお話ですね。テーマは僕も使ったことがありますが、とても便利な機能だと思います!便利なものはどんどん使って楽していきたいですね。

まえおき

みなさん素晴らしい記事を次々と公開されていく中、なにを書こうかとずっと迷っていましたが、今回はタイトルの通りCakePHPへの貢献について少し書きたいと思います。自分自身の経験によるものです。

CakePHPへの貢献しているでしょうか?僕は全然やってません。やってませんが、CakePHPが1.1の頃からずっと使っていて、なにかしら役に立ちたいなぁと思って今まで過ごしてきました。でも、ド田舎に住む1利用者に過ぎない僕みたいな技術者が、何が出来るだろうとも感じていました。そういう人って実際に多いんじゃないかな。CakePHP利用者で僕が昔から知ってる人達の日記を読みながら「勉強会とか楽しそうだな」と羨ましく思ったものです。

でも、最近ちょっと考えが変わってきました。自分が出来ることをすればいいんです。コアデベロッパでもないのに開発に携わろうとしたり、無理に遠くでやってるイベントに参加しようとしたり、そういうことがダメでした。大事なので2回言いますが、自分が出来ることをすればいいんです。僕の場合は2つありました。

  • ブログでアウトプットする
  • CakePHPのドキュメントの翻訳

です。ブログでのアウトプットの重要性は、もう既に多くの人が声高に叫んでいます。アウトプットを頑張るようにしました。

それから、僕は英語が好きです。英語力はある方とはいえないけど、読み書きはある程度できます。なので、それをCakePHPと結びつけて役立てるにはどうしたらいいのか、っていうことを考えた時にドキュメントの翻訳にたどり着きました。

さて今回はこのドキュメントの翻訳について少し書いていきたいと思います。


最初にやること

Cookbook。これがCakePHP2.0のドキュメントです。既にいくらか日本語化されている部分もあります。翻訳に参加したければまずはCakePHPドキュメントチームに挨拶ぐらいしておきましょう(しなくてもいいかもしれませんが、僕はメールを送りました)。一応ここにも「翻訳に参加する場合はメールください」とありますし。簡単な自己紹介と翻訳に参加したい旨のメールです。こんな感じ

Dear CakePHP docs team.

My name is Kengo Tateishi.
I'm a programmer in Japan.

I have developed a web application with CakePHP since its version was 1.1.x.
I love CakePHP.

I would like to participate in translating the CakePHP 2.0 document
into Japanese in order to contribute to CakePHP and its users in
Japan.

Can I participate in translating?
Can I send a pull request to the cakephp/docs repository on the github?

すると次の日にはすぐグラハム(CakePHPのコアデベロッパ)から返信が来ました。

ドキュメント翻訳への参加を歓迎します。
github上でプルリクエストを送ってください。
わからないことがあれば私に聞いてください。

という内容でした。なかなかフレンドリーな感じで、嬉しかったです。すぐお礼の返事を出しましたが、この時はやっぱりちょっとテンションあがりました。


翻訳環境の準備

リポジトリ
CakePHPのドキュメントはgithub上で管理されています。まずはリポジトリをforkすることから。CakePHPドキュメントのリポジトリにアクセスして、forkします。




すると自分のアカウントにリポジトリがコピーされるので、それを作業環境にcloneします。



cd /home/tkengo/
git clone git@github.com:tkengo/docs.git

※sshでcloneするためにはSSHの公開鍵の登録が必要です。別途githubのヘルプを参照してください。



必要なツール群
CakePHPのドキュメントはsphinxで構築されます。 僕の場合、OSはFedora Coreを利用しています。 以下のツールが必要です

  • Python
    • SphinxがPythonで書かれているため必要です。2.7系を使いました。OSにプリインストールされていたものは最初は2.4だったけど、2.4じゃうまくいかなくて、2.7系をソースから入れました。
  • Make
    • いわずと知れたビルドツールです。大概のOSには入ってます
  • Sphinx、PhpDomain for sphinx
    • easy_installでインストールします。

easy_installはPythonモジュールを自動的にインストールしてくれるコマンド。setuptoolsというツールをインストールしたら使えるようになります。

http://peak.telecommunity.com/dist/ez_setup.py

ここにあるファイルをダウンロードして

python /path/to/download/ez_setup.py

とすると使えるようになります。あとは

easy_install sphinx
easy_install sphinxcontrib-phpdomain

とすればSphinxとPhpDomain for sphinxのインストールが終わって、準備完了。


翻訳作業とビルド

こっから頑張って翻訳していきます。CakePHPドキュメントリポジトリの構成は

  • config/
  • en/
  • es/
  • fr/
  • ja/
  • pt/
  • ru/
  • themes/
  • Makefile
  • README.mdown

となってます。enディレクトリにあるのが英語の原本なので、そこから.rstファイルをjaディレクトリの同じ階層にコピーしてそれを翻訳します。がりがり翻訳しましょう。

その後はビルドです。.rstファイルからHTMLを生成します。

cd /home/tkengo
make html

こうすると全言語のHTMLファイルが次々と生成されます。今回は日本語の分だけでいいので

make html-ja

とすると日本語分だけHTMLの生成をします。ビルドするとbuild/というディレクトリが新しく出来ているはず。そのなかに.htmlファイルが出来上がります。


プルリクエスト

実はまだドキュメントのコントローラ部分を翻訳中で、ここまで行ってないのですが、一応今後やる予定の作業です。まず、更新した.rstファイルをコミットして、githubにpushしましょう。

git push origin master

さていよいよプルリクエストです。でも、実際にリクエストを送る前にちょっと立ち止まって。自分が翻訳した部分のオリジナルのリポジトリに変更が加わってないかどうか確認をします。もしここで、オリジナルに変更があった場合、その変更まで翻訳してから、プルリクエストを送りましょう。


ここまで来たら、あとはオリジナルのリポジトリに取り込まれるのを待つのみ。実際に取り込まれたらたぶんまたテンションが上がると思います。


終わりに

長々と書いてきましたが、どうでしたか。こんな僕でも、私でも、なにかしら出来ることがあるよ、っていうアピールになればと思います。大それたことじゃなくても良いと思います。

14日目は@kaz_29さんです。ブログはここですね。よろしくおねがいします!

2011年11月28日月曜日

IEでscrollTopプロパティが効かない場合がある

今日はJavaScriptのお話。

うちの会社で作ってる業務システムで奇妙な動きをしてる所があって、そこの部分の解決策の調査を依頼されて、最終的に解決したので忘れないうちに記録に残しとく。


1. 問題部分

次のようなコードがあったとします。

<div id="Scroller" style="overflow:auto;width:100px;height:100px;">
ああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああ
</div>

この時、高さ100幅100のブロック要素に対して縦のスクロールバーが表示されます。今回の場合、もともとやりたかったことは、スクロールバーが表示されているブロック要素に対して、表示時にスクロールバーを一番下まで持っていきたいということでした。

で、もともとのソースで、どうやってたかというと

$("#Scroller").get(0).scrollTop = 100000000;

ってやってた(※jQueryを使用しています)。なるほど、スクロールバーの位置にありえんくらい大きい値をセットして一番したまで移動させているわけです。通常であればこれで問題なく動作していました。でも、ここでIEの拡大レベル(画面右下にちょこっと出てるでしょ?)を変えるとなぜか動かなくなった。


しかも、ある倍率では動くけど、ある倍率では動かない、という奇妙な現象だった。



2. 解決

たとえば125%の時などは、うまく動いてなかった。IEのデバッガ(開発者ツール。これ使いにくいよね・・・)で該当部分をステップ実行すると、どうもscrollTopに0が入っているようだ。拡大レベルが100%の時はブロック要素内のスクロールバー位置の最大値がきちんと入ってたのに、拡大レベルが125%の時は0が入ってる。

ここでちょっと閃いたヽ(゚∀゚)ノ パッ☆

代入してる数字が大きすぎるっちゃない?!


と思って、試しに2桁減らしてみたらなんと動いた

$("#Scroller").get(0).scrollTop = 1000000;

ああ、なんて簡単な・・・。実際に、そのブロック要素が最大でどれくらいの高さになるのかをきちんと見積もってみたら10000(いちまん)もあれば十分だった。かなり余裕をもってその10倍にしたとしても100000(じゅうまん)だ。もともと指定されてた数値は100000000(いちおく)。なんという欲張り。

ということで、代入する数値を小さくしたら解決しました。実際にどれくらい大きな値だとダメなのか、とかの検証はしてないので正確な境界値はわかりません。

2011年11月16日水曜日

IEにおけるXMLHttpRequestの不具合

ExtJSを使っているとAjax通信を多用しますよね。

さて今日はIEでのAjax通信のお話。

とあるシステムでExtJS(バージョン3.3)のExt.Ajax.requestを使ってPOSTしている部分があるけど、なぜかたまーにコールバックに返ってこない場合がありました。しかもIEでばっかり起こる。パケットキャプチャしてみると、サーバー側にPOSTデータが送られてない。5分後にサーバー側でタイムアウトして、処理を中断してました。

  • IEはデータを送ったつもり
  • でもサーバーには届いていない
  • サーバーは5分間じっと待つけど、もう無理!って諦める
  • IEはサーバーからの返答をじっと待つ

サーバーとIEがすれ違いなんですね。気持ちが届かないって、はがゆいよね。

で、この現象、結論から言うと、IEのバグじゃないの?ってことです。英語ですが、こんな投稿をみつけました。

XMLHttpRequest POST sometimes fails when server is using keep-aliv


ざっくり翻訳しました。ところどころ意味が通りやすいように意訳してる箇所もありますが、意味不明なところがあれば原文を参照してください。。。


サーバーがkeep-aliveを設定している時、XMLHttpRequestのPOSTが時々失敗する


2004年、IE6において、サーバーがコネクションをリセットした場合、POSTリクエストのbodyが失われてしまうというバグがありました(http://support.microsoft.com/kb/831167/en-us)。同じバグがどうやら(少なくとも)IE6,7,8のXMLHttpRequestにも存在しているようです。

この問題は、keep-aliveタイムアウトに短い時間(たとえば10秒とか)を設定しているサーバーに対してXMLHttpRequestをPOSTすることによって簡単に再現できます。もし、POSTリクエストがkeep-aliveが切れる直前に作られていたとして、そこにわずかなネットワークの遅延があったとすれば、Webサーバーはコネクションをリセットして、IEにリクエストを再送することを強要します。この時、IEはContent-Lengthヘッダーは送り直すのですが、bodyを送信するのを"忘れ"ます。結果として、サーバーではタイムアウトが発生するまで、bodyが到着するのを待つことになります。

"keep-aliveの直前"と"わずかなネットワーク遅延"というと、このバグの再現が難しそうに思えますが、モダンなWebアプリを例えばモバイル回線などを使って利用しているとそう難しくありません。

この問題のデモをhttp://artur.virtuallypreinstalled.com/apache2-default/ie-xhr.phpに置いています(訳注:現在はリンク切れのようです)。keep-aliveタイムアウトを15秒に設定しているので、14~15秒待って、ボタンをクリックしてください。レスポンスがきちんと返ってきたらアラートがポップアップされ、ボタンが再度有効になるはずです。少なくとも私がモバイル回線を使ってやると、IE8で2~3回で問題が再現します。

サーバーのkeep-aliveをオフにするのが唯一の解決策だと思われます(もしくは、keep-aliveタイムアウトを60秒以上に設定することでも解決するかもしれません)。他に誰かこのようなことが起こっていませんか?他の解決策はありませんか?過去にこの問題について報告されたり議論されたりしていませんか?



ということらしいです。この投稿のレスで「私もその現象起こったよ!」って言ってる人もいます。実際、うちのサーバーのkeep-aliveは5秒になってました。ためしにkeep-aliveを切ったら(offにした)、システムのレスポンスが著しく低下してしまって、結局切り戻し。今はkeep-aliveの時間を伸ばして様子見。それでもまだ現象は発生してるけど、回数は少なくなったような・・・。

これが、本当にIEのバグならはやく直って欲しいが・・。

プログラムではどうしようもないような、こういう問題はやっかいだなぁ(・。・;

2011年11月5日土曜日

CakePHPでデータベースセッションを使った時の怪現象

CakePHP1.3.5でのお話。

CakePHPを配置するWebサーバーを複数台でロードバランスしている環境でセッションを使おうと思ったら、ファイル以外にセッションを保存しないといけません。そういった理由で、セッションをデータベースに保存しているシステムがあります。このシステムでデータベースに保存されているセッションが突然全消しされる怪現象が何度か起こってて、なんじゃこりゃ(´・ω・`)ショボーンな感じで、原因究明に3日ぐらいかかった。忘れないうちにその全貌を書き残しておこう。

1. 問題部分

CakePHPでデータベースにセッションを保存している場合、有効期限が切れた時、PHPのsession_destroy関数が呼ばれて、それをトリガーにcake/libs/cake_session.phpの__destroyメソッドが呼ばれます。
function __destroy($id) {
    $model =& ClassRegistry::getObject('Session');
    $return = $model->delete($id);

    return $return;
}

ここで見てわかるようにセッションIDをキーとしてデータベースからセッション情報を削除する処理が流れます。さて、このモデルのdeleteメソッドを詳しく見てみると

X. cake/libs/model/model.php deleteメソッド
function delete($id = null, $cascade = true) {
    if (!empty($id)) {
        $this->id = $id;
    }
    $id = $this->id;

    if ($this->beforeDelete($cascade)) {
        $filters = $this->Behaviors->trigger($this, 'beforeDelete', array($cascade), array(
            'break' => true, 'breakOn' => false
        ));
        if (!$filters || !$this->exists()) {
            return false;
        }
        $db =& ConnectionManager::getDataSource($this->useDbConfig);

        $this->_deleteDependent($id, $cascade);
        $this->_deleteLinks($id);
        $this->id = $id;

        if (!empty($this->belongsTo)) {
            $keys = $this->find('first', array(
                'fields' => $this->__collectForeignKeys(),
                'conditions' => array($this->alias . '.' . $this->primaryKey => $id)
            ));
        }

        if ($db->delete($this)) {
            if (!empty($this->belongsTo)) {
                $this->updateCounterCache($keys[$this->alias]);
            }
            $this->Behaviors->trigger($this, 'afterDelete');
            $this->afterDelete();
            $this->_clearCache();
            $this->id = false;
            return true;
        }
    }
    return false;
}

Y. cake/libs/model/datasources/dbo_source.php deleteメソッド
function delete(&$model, $conditions = null) {
    $alias = $joins = null;
    $table = $this->fullTableName($model);
    $conditions = $this->_matchRecords($model, $conditions);

    if ($conditions === false) {
        return false;
    }

    if ($this->execute($this->renderStatement('delete', compact('alias', 'table', 'joins', 'conditions'))) === false) {
        $model->onError();
        return false;
    }
    return true;
}

処理の流れのポイントとしてX11行目でIDが存在するかどうかをチェックして、存在しなければ処理を終了しています。IDが存在すればX27行目でデータソースのdeleteメソッドが呼ばれます。

Y4行目の_matchRecordsメソッドはWHERE句を文字列で返してくれる関数なのですが、中身を辿っていくともう一度モデルのexistsメソッドが呼ばれています。ここでIDが存在すれば

A. WHERE Session.id = 'xxxxx' AND 1 = 1

という文字列が返されます。もしIDが存在しなければ

B. WHERE 1 = 1

という文字列が返されます。

BのWHERE句はなんとも危険な香り(´∀`;) ま、通常はX11行目でIDの存在をチェックして、存在する場合しかYには入ってこないんだけど・・・さて、ここまで来たらなんとなくわかってきた気がします。


2. 非同期通信の怖さ

まず今回のシステムではUIにExtJSというJavaScriptのフレームワークを使っていました。ExtJSからバックエンドのCakePHPにいろんなデータを投げて処理するという基本的な流れです。さてこのExtJS、これを使って開発していると、どうしてもAjax通信を多用することになります。

今回は画面を開いた時に、連続してAjax通信するようにコーディングされている部分がありました。Ajaxは非同期通信なので、連続でAjax通信されている部分があるとすると、前の通信の完了をまたずに次の通信を開始します。これが諸悪の根源でした。

さて、今ここにセッションの有効期限が切れたユーザーがいるとします。このユーザーが画面を開いたとするとAjax通信が非同期に2つ同時に流れます。その2つの通信それぞれについて、セッションの有効期限が切れているためcake/libs/cake_session.phpの__destroyメソッドからモデルのdeleteメソッドが呼ばれます。

それぞれ通信Aと通信Bとしましょう。AとBはそれぞれ__destroyメソッドからdeleteメソッドを呼ばれ、X11行目のID存在チェックを通りぬけYに入ります。たまたまタイミングの問題でAが先にY4行目にさしかかり、WEHERE句を取得してそのままY10行目のSQL実行まで行ったとします。次にBがY4行目にさしかかり、WHERE句を取得して・・・・と、ちょっとここでストップ。そう、Aが先にデータベースからIDを削除しています。すると、BがY4行目を実行してる時点では既にIDは消えてなくなっていることに!

ここまで来るともうわかります。IDが無いのでWHERE 1 = 1という危険なSQLが返ってきて、それを実行しちゃうのでセッションが全消しされます。


3. コーディングルール

そのシステムではAjax通信をする際は、前の通信が終わってから(コールバックなどを利用して)次の通信を始めるというような基本的なルールがあったのですが、今回問題が起こった部分はそれに沿っていない箇所でした。もし、そのルールにのっとっていれば非同期の同時アクセスもなくこんな問題は起きなかった。開発者がJavaScriptについてあまり知らないらしいので、コールバックを使った同期通信の手法を知らなかったのかもしれません。



と、まぁ、今回はタイミングの問題もあるので、ほとんど起こらないような事象だけど、こういうこともあるんだよ、っていうことで誰かが読んでくれればいいな。

ところで、根本的にこれを解決するにはコアをいじらなきゃいけないよな。たぶん。どこをどう変えたらいいんだろう。


2011-11-07追記
CakePHP1.3.6からはモデルのdeleteメソッドが微修正されているようです。たぶんもう起きません。

2011年10月28日金曜日

CakePHPのモデルからSQLを外出しする

CakePHPを使って開発をしていると、自分でSQLを書く機会が劇的に減ると思います。簡単なSQLであればモデルのメソッドでほとんど記述できる。でも、やっぱり難しいSQLを実行したいとなると自分でSQLを書いてそれを実行することが多いですよね。そんな時どうしてますか?

SQLというのは通常、結構行数をくいます。たとえば

$query = <<<EOD
    SELECT
        Post.id,
        Post.subject,
        Post.created
    FROM
        posts Post
    WHERE
        Post.created <= 'YYYY-MM-DD'
EOD;
$results = $this-<query($query);

こんな感じの簡単なSQLでもそれなりに行数が。。さて、簡単なSQLならまだいいけど、こういう風に直接SQLを書いて実行したい場合は、もっと複雑なSQLになることが多いです。たとえばUNION句を使って結合していたり、条件部分にEXIST句を利用していたり、サブクエリを利用していたりする場合。そうすると、これとは比べ物にならないくらい大きな行数になって、時にはテキストエディタの1画面内に入り切らないことも。。。

モデル内に直接SQLを書くとモデルが肥大化してメンテナンスしにくくなってしまうので、SQLを外部に書きだして、それを読み込むことによってモデルの中身を軽量化を目指します。

まず、AppModelにこんな感じで関数を定義。

/**
 * SQLファイルを読み込む
 *
 * @param string $fileName SQLファイル名
 * @param array  $params   パラメータ
 * @param array  $escape   シングルクォーテーションをエスケープしないパラメータのキー
 * @return string SQL
 */
function sql($fileName, $params = array(), $escape = array())
{
 // ファイル名に拡張子まで指定されていれば取り除く
 if (substr($fileName, -4) == ".sql") {
  $fileName = substr($fileName, strlen($fileName) - 4);
 }
 
 $query = "";
 
 // ファイルが存在することを確認
 $paths = array(
  MODELS . "sql" . DS . Inflector::underscore($this->name) . DS . $fileName . ".sql", // 自モデルのディレクトリ
  MODELS . "sql" . DS . $fileName . ".sql"                                            // 共通ディレクトリ
 );
 $sqlFilePath = "";
 foreach ($paths as $value) {
  if (file_exists($value)) {
   $sqlFilePath = $value;
   break;
  }
 }
 
 if (!empty($sqlFilePath)) {
  if (empty($escape)) {
   // エスケープ除外対象がなければすべての変数についてシングルクォーテーションをエスケープする
   array_walk($params, array($this, "escapeQuote"));
  }
  else {
   // エスケープ除外対象があれば、配列から除外する
   $tmp = $params;
   foreach ($params as $key => $value) {
    if (in_array($key, $escape)) {
     unset($tmp[$key]);
    }
   }
   
   // シングルクォーテーションをエスケープする
   array_walk($tmp, array($this, "escapeQuote"));
   
   // エスケープしたものとそれ以外のものをマージする
   $params = array_merge($params, $tmp);
  }
  
  // パラメータを展開
  extract($params, EXTR_OVERWRITE);
  
  // ファイルからSQLを読み込む
  ob_start();
  include $sqlFilePath;
  $query = ob_get_clean();
 }
 else {
  $this->log("sqlファイルが見つかりません。" . $sqlFilePath);
 }
 
 return $query;
}

/**
 * シングルクォーテーションをエスケープする
 */
function escapeQuote(&$value, $key)
{
    if (is_string($value)) {
        $value = str_replace("'", "''", $value);
    }
    else if (is_array($value)) {
        array_walk($value, array($this, __FUNCTION__));
    }
}

で、直書きSQLを呼び出したいときは、app/models/sql/モデル名/以下にSQLファイルを作る。たとえば、app/models/sql/post/list.sqlというファイルにさっきのSQLを書いたとすると

$query = $this->sql("list.sql");
$results = $this->query($query);
と、たったこれだけに!

パラメータを渡したい場合は、2つ目の引数にキーと値の連想配列を渡してあげる。ちょうどビューのエレメントを呼ぶ時と同じようなやり方です。

モデルがSQLで埋まらないように、整理しましょう!

2011年10月17日月曜日

CakePHP 2.0 Stable Release!

CakePHP 2.0 Stableがリリースされた!つい、こないだRC3がリリースされたと思ったらもうStableだよ。素晴らしい。さっそくダウンロードして試してみよう。ダウンロードして解凍すると、こんな感じのディレクトリ構造
  • app
  • cake
  • lib
  • plugins
  • vendors
  • .gitignore
  • .htaccess
  • index.php
  • README
ん?cakeってディレクトリなんだろう?たどっていっても、奥深くのフォルダにtest_element.xmlっていうファイルがあるだけ。よくわからないなぁ。
とりあえず、app/Config/以下のdatabase.php.defaultをdatabase.phpにリネームして設定を変更。それからapp/tmp以下を書き込み可能に設定して、いざアクセス。


おぉきたー。このスクリーンショットはChrome14ですが、見慣れた緑背景にグラデーションがかかってる。かっこいい。

さて、いろいろいじってみよう。

2011年10月7日金曜日

働くということ

僕は大学卒業後に県内のIT企業に就職、それから丸4年半が経ちました。入社前は「県内一の規模のIT企業」ということで、さぞかし凄い人達がたくさん働いているのだろう、と。期待をふくらませて入社したわけです。

しかし、いざ入社して仕事をしてみるとちょっと違和感。1~2年目はまぁこんなものなのかな、と思いつつも任せられた仕事をコツコツとこなしていました。3年目くらいからかな、その違和感がだんだん確信に変わってきたのは。

周りのレベルが低すぎる、とだんだん思うようになってきました。

もちろん自分なんて世の中全体で見ればまだまだ知らないことがいっぱいあって、もっともっと勉強せないかんこともある。そんな僕も入社して4年半の間、色々な人達と一緒に仕事をしてきた。でも、誰一人として技術力で僕を驚かせてくれる人は出てこず。


向上心というのは常に持ち続けたほうがいいもの。常に上を目指したい。
  • オープンソースコミュニティへの参加
  • 新技術、旬な話題へのアンテナ
  • 創ることの喜び
  • 英語の記事を読んで、英語の日記をつける
他にもいっぱいあると思うけど、いつもこういうことを意識して技術者としての向上を目指してさ。働く上でも必要よね。



働くということ。

お金を稼ぐため。やりがいを見つけるため。自己を高めていくため。人それぞれ目標があることだろう。

生活に必要なお金を稼ぐために安定した職業に就く。これはわかる。難しい課題に挑戦して技術者として向上した。これもわかる。才能ある人達と一緒に仕事をしてお互いに刺激し合いたい。いいね。


僕には家族がいる。妻と娘。養っていかんといけん。こういう場合、周りは必然的に安定を求める。技術者としての価値の向上を目指す必要なんてない。安定したお給料が一番大事だ、ってね。


でも、僕は・・・。