出張振り返りその2

実生活が始まる

今となれば一週間の出張と1ヶ月、2ヶ月の出張の違いはよくわかりますが当時は当然何がどうなるのかわからないまま、私のフィリピン生活は始まりました。当時現地に同じように出張していた同僚と、ちょうど入れ替わる形で赴任したため、その期間は住まいの説明や必要事項(危険なことや気をつけたほうが良いこと等)をシェアしてもらいました。

食事

当時から最も困っていたのは食事でした。同僚と入れ替わりで入居したコンドミニアムには炊飯器があったため、まず白米を買うところからでした。フィリピンの一般的なお米は水分が乏しく、おかずやスープと混ぜ合わせて食べるので日本のそれとは決定的な違いがありました。どこのスーパーでも販売しているわけではなく、また1キロ程の小ぶりのものが殆どでした。単身でいっているのでむしろそれはありがたかったです。こちらは基本的に野菜の料理が少ないため、肉とご飯のようなレパートリーが多く、また油の質が多少日本と違います。気をつけないと太るか、あるいは病気になるかの2択だなと思ったのは記憶にあります。料理に関してはフィリピン料理は油分が多く、ビールにあう食事ではあります 🙂 が、、、ひとたび体調を崩すとおかゆや消化の良いものが探しづらく、栄養補給が非常に難しくなります。わたしは日本から持ち込んだ友人からもらったフリーズドライのお粥に助けられました。駐在員は基本的にマルチビタミンのようなものを飲んでいる人も多かったです。

今では多少値が張っても、日本料理のお店や美味しい韓国料理のお店をしっているのでなんとなくローテーションしていますが当時は「外食」そのものにも大きな抵抗がありました。なんといってもオーダーする時に自分の好みや食べたいものがきちんといえない、あるいはよくわからない質問をされたらどうしよう、と思いです。悶々として2時間程空腹になってクラクラしながらさまよったこともあります。日本で玄関を出て3分でコンビニに行き、アイスコーヒーを買っていた頃を思うと泣けてきました。食べたいものを食べたいだけ居酒屋で頼んでいた事を思うと、簡単に手に入れていたものが急にハードルが高いところにいってしまったような、ストレスを感じていました。空腹時に自分が期待していたものが出てこない(美味しくない、思ったものと違う等)時のストレスは日本ではあまり経験がなく、しばらくはそのギャップに苦しみました。もともとスナック菓子が好きなので、スーパーでスナック菓子をかって食べてみたものの全く想像と違う味でびっくりしてなくなく捨てたこともあります。

今思いますが、海外での生活で一番ネックになるのはやはり食事なのではないでしょうか。仕事がきつくても最終的に一人でも美味しいものが食べられれば、ある程度その日の出来事は水に流せる気がします。

 

やっとごはん #finally #lunch #noodles

A post shared by Yashiro Anazawa (@yashiroanazawa) on

 

 

広告

出張振り返り

振り返り

出張も9ヶ月になりました。もともとの予定は6ヶ月でしたが思いの外半年でできることが限られてることがわかったのでもう半年伸ばしてもらうことに。スタッフや家族に協力してもらいながら、あと3ヶ月で任期満了になりつつあります。海外で長期的に働く、生活するということが今後自分の人生においてそう多くないだろうと思っているので、いい機会なのである程度色々残しておこうかなと思います。

ちょうど去年の出張時から何故かinstagramを始めたのでそれとともに色々綴っていこうと思います。

 

出張の目的

フィリピンの子会社への出張です。エンジニアとして現地のエンジニアと、現状抱える技術的な問題や、テクニカルな問題だけでなく課題解決(日本では当たり前に行われてることが行われていない、あるいは事情で現実的でない、ときに日本人に相談しにくいなど)や改善を目的に出張しています。現地スタッフとは英語でのコミュニケーションになります。ミーティング、ドキュメント、飲み会から普段の会話まで日本語とは隔絶された生活に身をおくことになります。日本での基本的な業務からはある程度解放してもらい、基本的には現地のメンバーのプロダクトに関わり、コードレビューや各プロダクトの振り返り、日比を跨いだ特殊なプロジェクトの設計や、日々の運用フローの改善などに関わってきました。

 

海外経験

子会社への短期間の出張はそもそも年に一度程度の経験がありました。1週間程度を目安に特定のプロジェクの推進、あるいは日本での技術的な取り組みをシェアし、現場のエンジニアに反映してもらうなどです。つまり長期出張以前に現地スタッフとはある程度の面識がありました。私生活では夏休みにアジア数カ国を旅行しており、最近は欧州がもっぱらお気に入りですが、チーズが食べられず、チーズ抜きにしてください、と言えない小心ものです。ピザは食べられます。

 

英語

現在の会社に入社するまで英語に関わる業務は一つもありませんでした。学生時代は英語は鬼門で大学自体、英語で留年するところでした。現職の前に海外旅行にいったことが一つの契機になったかもしれません。今の会社では皆が当たり前のように英語に触れ学習するので、勉強したり、学習方法を相談したり意見交換をすることが恥ずかしくなくなっていったのかなと思います。また、全員がネイティブのようにできるわけではない事が私自身落ち着いて学習できたことにつながってると思います。業務上、全てに関して英語が必要かというと、強要もされません。それもあってマイペースでできたのかなと感じます。

 

主に生活面に関してこれからぼつぼつ更新していこうと思います。 🙂

landed #いよいよ

A post shared by Yashiro Anazawa (@yashiroanazawa) on

  • 生活面
  • 困ったこと
  • 想像してよりも良かったこと / 悪かったこと
  • 準備して行ってよかったもの、日本から持ってきてよかったもの
  • 現地で購入してよかったもの
  • 英語

cakephp1.3 saveメソッドで更新日時がudpateされない

大分ご無沙汰しております。ビールがおいしい季節になりました。

cakePHPは、modified,createdなんてフィールドを予め作成しておくと、新規作成・更新の際に勝手にデータをinsert,updateしてくれるというお便利機能があります。

先日、以前と同じように更新のため、

$this->User->save($this->data);

なんて処理をしていたときに、突然、「更新日時が更新されない」という現象に遭遇しました。

実は、探してみると

  • mysqlの設定が悪い
  • そもそも、hiddenとかでPOSTしてた
  • という人たちの記事があって、はじめはそこを疑いました。ですが以前はきちんと動いていたのに、突然save()時に更新日時modifiedが更新されない現象だったので、他の原因を探ってみました。

    結論からいうと、ここのsaveの直前に

    $this->User->id = user_id;
    $this->User->read();
    

    というreadメソッドを呼び出すことで、コアのmodel.php側のsave()内で$this->dataを参照すると、
    read()時にセットしたであろう、更新日を含めた一行まるごとのデータ構成になっていました。
    その後、処理としては実際にsave()に投げられた引数$dataとmodel内の$this->dataとを、マージしているらしく、結果として、

    パスワードだけ変更する画面だったけど、実際はすべてのデータをupdateしてた(実際はパスワード以外のデータは
    データベースから参照された値なので、変更点はなく、更新日時すらも含まれているので結果、更新されていない)
    という現象でした。
    なぜ、直前でreadなんぞしているかというと、変更点を調べたいために、事前に更新前のデータを取得しておく必要があったのです。

    解決方法
    解決方法はいくつかあると思うのですが今回は・・・このようにしてみました(これが推奨されるわけではないと思います)。

    $this->User->data  = null;//readしたデータをリセットする。
    $this->User->save($this->data);
    

    本来の正しい修正方法としては第3引数に更新対象のフィールドを含めて、余計なフィールドは含ませないことがポイントだと思われます。
    恥ずかしながら・・・controller側で参照している$this->data と、model 側で参照している$this->data は完全に一致しているものと思い込んでいましたが、実際のところ違う、ということですね。2.0からは明確に表示が変わるので、今後は意識して使ったほうがいいのだろうか・・。

    おなかへりました!ビールのみにさまよってきまーす。

    cakePHP2.1でファイルのアップロードとダウンロード

    cakePHP2.1の
    ファイルアップロード
    ファイルダウンロード
    をやってみようと思います。ひとまずファイルアップロード用にテーブル作成します。
    今回は、ファイルの実体は特定のディレクトリに保存するので、ファイルの実体はDBには保存しません。


    mysql> desc upload_files;
    +----------------+--------------+------+-----+---------+----------------+
    | Field | Type | Null | Key | Default | Extra |
    +----------------+--------------+------+-----+---------+----------------+
    | id | int(11) | NO | PRI | NULL | auto_increment |
    | user_id | int(11) | NO | | NULL | |
    | name | varchar(50) | NO | | NULL | |
    | file_name | varchar(100) | NO | | NULL | |
    | extension | varchar(10) | NO | | NULL | |
    | size | int(11) | NO | | 0 | |
    | download_count | int(11) | NO | | 0 | |
    | created | datetime | YES | | NULL | |
    | deleted | tinyint(1) | YES | | 0 | |
    | deleted_date | datetime | YES | | NULL | |
    +----------------+--------------+------+-----+---------+----------------+

    次にファイルアップロード用のViewです。
    Viewにはこんな感じでアップロードフォームを作ります。

    echo $this->Form->create(null, array('action' => 'index', 'enctype' => 'multipart/form-data'));
    echo $this->Form->input(null, array('type'=>'file','label'=>'対象ファイル' ));
    echo $this->Form->submit('ファイルアップロード');
    

    次はアップロード用のコントローラー。index()メソッドです。一覧とアップロードされた
    ページを一緒にしているので一覧もとりに行っています。
    定数化したり、モデルに処理を移動する必要があったりしますが、まずはこれで。

    public function index() {
    	$this->UploadFile->recursive = 0;
    	$this->set('uploadFiles', $this->paginate());
    	if(!empty($this->request->data)){
    		$uploaddir = self::FILE_SAVE_PATH.DS;//事前にパスを宣言して、パーミッション変えておいてください。
    		$uploadfile = $uploaddir . date("YmdHis");
    		$this->request->data['UploadFile']['user_id']	 = $this->Auth->User('id');
    		$this->request->data['UploadFile']['file_name'] = self::FILE_SAVE_PATH.DS.basename($uploadfile);
    		$this->request->data['UploadFile']['extension'] = pathinfo($this->request->data['UploadFile']['name'],PATHINFO_EXTENSION);
    		if(!move_uploaded_file($this->request->data['UploadFile']['tmp_name'], $uploadfile)){
    			return;
    		}
    		if($this->request->data['UploadFile']['size'] > 1000000){
    			$this->UploadFile->invalidate('name','ファイルサイズが大きすぎます');
    			return;
    		}
    		if($this->UploadFile->save($this->request->data)){
    			$this->Session->setFlash('アップロードを完了しました', true);
    		}else{
    			$this->Session->setFlash('アップロードに失敗しました', true);
    		}
    		$this->redirect('index');
    	}
    }
    

    さて次は一覧のViewです。ダウンロードリンクですな。

    echo $this->Html->link($uploadFile['UploadFile']['name'], array('controller' => 'upload_files', 'action' => 'download', $uploadFile['UploadFile']['id'])); 
    

    それでは、ダウンロード用のメソッドを見ていきます。ここでポイントは、MediaViewを使うとすんごい便利なんです。
    ダウンロードは断然、cakePHPのコアのMediaViewを使うとダウンロードが簡単にできるのでおすすめ。
    1.3では「$this->view = ‘Media’;」という使い方だったのですが、(2.0はわからない・・・)2.1では
    「$this->viewClass = ‘Media’;」という利用方法に変わっている、というのが本日の重要ポイントです。
    lib/Cake/View にあるMediaView.phpをみると利用方法が書いて有ります。
    ヘッダーの書き出しとか自前でしなくてよくて、このMediaViewが勝手に判断してくれるのでかなり便利です。
    これで、ダウンロードリンクをクリックしてもページ遷移なしでファイルがダウンロードできます。お便利。
    本当はダウンロードされるたびにカウントアップしたものにしようと思っていますが・・今回はとりあえずスルーしました。

    public function download($id) {
    	$this->viewClass = 'Media';
    	$this->UploadFile->id = $id;
    	$this->UploadFile->recursive = -1;
    	$data = $this->UploadFile->read();
    	$params = array(
    		'id'=>basename($data['UploadFile']['file_name']),
    		'name'=>preg_replace("/.[^.]+$/","",$data['UploadFile']['name']),
    		'download'=>true,
    		'extension'=>$data['UploadFile']['extension'],
    		'path'=>dirname($data['UploadFile']['file_name']).DS
    	);
    	$this->set($params);
    }
    

    月末に怒涛のブログ記事を書きましたが、まだまだ2.1関連の内容をお届けできればと思います!

    cakePHP2.1でSoftDeleteBehaviorを使う

    何かというとcakeでは通常$model->delete();すると物理削除でレコードごとなくなりますが、
    これがあら不思議!このビヘイビアを使うと論理削除にしてくれます。

  • 通常のdelete()呼び出しで物理削除ではなく、deletedに1 が入り、delete_dateフィールドに日時が入る
  • 該当するモデルは何もしなければdelete = 0 である条件を必ずつけてくれる
  • というものです。いや幸せ。
    さて準備です。GitHubのcakeDCから
    ここにあるSoftDeleteBehavior.phpだけをapp/Model/Behaviorにぶっこんでみます。
    例としてモデルは、User.phpにしてみましょう。
    私のモデルは、既にACLのビヘイビアが設定されていますが、それに追加してあげればいいですね。

    	public $actsAs = array(
    					'Acl' => array(
    						'type' => 'requester',
    					),
    					'SoftDelete'
    					);
    

    さて、ここでもうひとつ準備。
    テーブルにフィールドがあれば良いですが、usersテーブルに該当するフィールドを用意しましょう。
    mysqlを例にして下記のようなフィールドを追加します。
    ここで注意・・deletedは指定しないで追加するとtinyint(4)として登録されますのでちゃんとtinyint(1)で指定しましょう。
    これが間違ってると検索時に deleted = ”という謎の条件になってしまいます。

    deleted   tinyint(1)
    deleted_date datetime

    さてこれで用意が整ったので、一覧画面を覗いてみましょう。
    WHERE `User`.`id` = 1 AND `User`.`deleted` = ‘0’ LIMIT 1
    こんな条件が一覧についていたら、やりたいことができたも同然です。
    それでは試しに、ユーザを削除してみると、どうやらエラーがでるみたいですが・・・。

    SoftDeleteBehavior は通常のdelete()を呼ぶとどうやらfalseを返すようです。
    なのでひとまず・・こんなかんじでしのいでみました。

    if (!$this->User->delete()) {
    	$this->redirect(array('action'=>'index'));
    }
    

    さてこれで削除したデータを見てみると、
    いえい!削除されていますよぅ!


    mysql> select deleted,deleted_date from users where id =6;
    +---------+---------------------+
    | deleted | deleted_date |
    +---------+---------------------+
    | 1 | 2012-03-24 19:43:51 |
    +---------+---------------------+
    1 row in set (0.00 sec)

    補足*管理画面などで削除されているユーザ自体も表示したい場合は、現在追加されているSoftDeleteを
    detachしてあげればよいです。例えばこんな感じ。

    public function index(){
    	$this->User->Behaviors->detach('SoftDelete');
    	$this->User->recursive = 0;
    	$this->set('users', $this->paginate());
    }
    

    補足その2*UserモデルにhasManyしているPostモデルにもこのSoftDeleteを摘要して、hasManyの条件として
    depend => trueを設定してみたのですが、どうも連動してSoftDeleteはしてくれませんでした。
    UserモデルのSoftDeleteを外して、物理削除状態にすると、PostモデルのSoftDeleteが聞いて削除状態になりました。
    ここらへんはもうちょっと調べて挙動を確認してみないといけないですね。新しいことがわかったらまた追記したいと思います。

    cakePHP2.1でSearchPlugin hasManyを扱う

    先日からSearchPluginを使った画面周りの実装してます。
    今回は特に、一対多データの扱いについてです。
    ブログにタグをつけたチュートリアルが結構あるので、HABTMデータは結構ありますね。
    今回の実際はHABTAMではなく、中間テーブルを介在しません。

    その1.企業モデルに「属性」を外だしで複数持たせる。(いわば、タグと同じ)
    その2.属性は複数もてるが、マスターテーブルは持たない。
    その3.属性テーブルはid,company_id,nameという構成。

    というちょっと特殊なケースかもしれない。でも よくあるUseとPostの関係だと思ってもらってもいいです。
    ただし、属性自体を「AND」条件で検索したい。というのが味噌です。

    SearchPluginの準備
    app配下にSearchというディレクトリ掘って、GitHubのこちら
    一式ごっそりおきます。
    bootstrap.phpのこの箇所が生きていれば、問題ないでしょう。

    CakePlugin::loadAll(); // Loads all plugins at once
    

    次に読みこむコントローラー側で利用できるように

    public $components = array('Search.Prg'); 
    

    こちらもセット。これで準備完了。

    次に親となる企業モデルCompany.phpにこの設定。当然、テーブル構成にこのフィールドが必要です。
    それと、SearchPluginで使う設定も一緒に書き足しておきましょう。
    $filterArgsのmethodについては後ほど。

    	public $hasMany = array(
    		'CompaniesAttribute' => array(
    			'className' => 'CompaniesAttribute',
    			'foreignKey' => 'company_id',
    			'dependent' => true,//企業が削除されたら、属性は一緒に削除されていい
    			'conditions' => '',
    			'fields' => '',
    			'order' => 'order',
    			'limit' => '',
    			'offset' => '',
    			'exclusive' => '',
    			'finderQuery' => '',
    			'counterQuery' => ''
    		),
    	);
    
    	public $filterArgs = array(
    		array('name' => 'attribute_id', 'type' => 'subquery', 'field' => 'Company.id', 'method' => 'searchByAttributes'),
     	);
    

    次に、CompaniesAttribute.phpの方にも設定をしておきます。

    	public $belongsTo = array(
    		'Company' => array(
    			'className' => 'Company',
    			'foreignKey' => 'company_id',
    			'conditions' => '',
    			'fields' => '',
    			'order' => ''
    		)
    	);
    
    

    モデル側の設定はひとまずここまで。次は検索画面ですね。
    先ほど、プラグインを読み込んだコントローラーにこんな検索メソッドを準備。
    属性テーブルCompaniesAttributeから選択させる属性一覧を取得してViewに渡しています。
    あとは検索結果をpaginateするための条件設定です。
    検索画面と検索結果を一緒に表示しています。

    	function search(){
        	  $this->Prg->commonProcess();
        	  $conditions = $this->Company->parseCriteria($this->passedArgs);//SearchPlugin
    		$this->paginate = array(
    		  'conditions' => $conditions,
    		  'limit' => 5,
    		  'order'=>'Company.id asc'
    		);
    		$attributes = $this->Company->CompaniesAttribute->findAllByProjectId($this->Session->read('selectedProjectId'));
    		$attributes = Set::combine($attributes, '{n}.CompaniesAttribute.name', '{n}.CompaniesAttribute.name');
    		$this->set('pager_numbers', $this->pager_numbers);
    		$this->set(compact('attributes'));
    		$this->set('companies', $this->paginate('Company'));  
    	}
    

    次にViewですね。検索フォームの箇所だけ、こんな感じ。
    本当はいろんな検索条件があるのですが、属性だけにしてみました。
    ひとまず属性を複数選択できるドロップダウン形式で表示しています。

    <?php echo $this->Form->create('Company');?>
    <fieldset>
    	<legend><?php echo __('Search Company'); ?></legend>
    <?php
    	echo $this->Form->input('attribute_id',array('multiple'=>true,'options'=>$attributes));
    ?>
    </fieldset>
    <?php echo $this->Form->end(__('検索'));?>
    

    これで肝心の企業モデルCompany.phpに設定していたsearchByAttributesを実装します。
    こちらは @kanonjiさんのこちらの記事が非常に参考になりました。
    属性自体は、IDを持たないので、値としては数字ではなくテキストが入ってきます。
    検索画面のドロップダウンのリストのvalue値は「製造業」のような日本語が入ってくることを前提にしています。

        public function searchByAttributes($data = array()){
            $this->Behaviors->attach('Search.Searchable');
            $attribute_id = Set::extract($data,'/attribute_id');
    	$options = array(
                'conditions' => array('CompaniesAttribute.name'  => $attribute_id),
                'contain' => $this->alias,
            );
            if (( $c = count ( $attribute_id )) !== 1 ){
            	//ここの条件を通すとAND条件として構成される。ここを通らないとOR条件として構成される。
                $options['group'] = 'CompaniesAttribute.company_id HAVING COUNT(CompaniesAttribute.company_id) = '.$c;
            }
    	$data = $this->CompaniesAttribute->find('all',$options);
            $condition = implode(', ', Set::extract($data,'/Company/id'));
            if ( empty( $condition )){
                $condition = 'NULL';
            }
            return $condition;
        }
    

    若干いじったところ
    属性モデルから企業モデルを参照するのでcontainでCompanyモデルを利用しようとしたら、
    find(‘list’)ではLEFTJOINされなかったのでall指定にしてextractでIDを引っ張り出しました。

    ArraySourceの勧め

    今日もお酒のまないんで、ひとつ記事かいてねることにします。本日もcakePHP2.1のお話です。
    さて、設定ファイルでデータベースに持つ程でもない情報というのは、かつてconst.phpに書いてました。
    ところが例えば男・女 とか、普遍的なものだったりあまり情報が書き換わらないものって結構あります。
    で、設定ファイルが結構カオスになってたりします。

    今回は、設定情報もこんなふうに持つと良いよってことで、https://github.com/cakephp/datasources/tree/2.0/Model/Datasource
    ここからArraySource.phpだけをいただきます。そんでapp/Model/Datasourceにポロリと入れます。

    次に、設定です。database.phpにこんな記述を足します。既存のデータベースの設定に追加してくださいね。

    	var $ArraySource = array(
    		'datasource' => 'ArraySource',
    	);
    

    これで、利用できる準備が整いました。
    次にGender.phpっていうモデルをModel配下に作成します。

    <?php
    App::uses('AppModel', 'Model');
    /**
     * Gender Model
     *
     */
     class Gender extends AppModel
    {
        public $name = 'Gender';
    
        public $displayField = 'display';
    
        public $useDbConfig = 'ArraySource';//ここにdatabase.phpで宣言した変数名をセット
    
        public $records = array(
                array(
                    'id' => 1,
                    'display' => '男性',
                ),
                array(
                    'id' => 2,
                    'display' => '女性',
                ),
            );
    
        const MAN     = 1;//一応定数としても利用できるようにしてみた。
        const WOMAN = 2;
    
    }
    

    これで、このモデルをbelongToに持つモデルにこんなふうに設定すると、(注1)

    	public $belongsTo = array(
    		'Gender' => array(
    			'className' => 'Gender',
    			'foreignKey' => 'gender_id',
    			'fields' => 'display',//これがちょっときになる指定
    		)
    	);
    

    親のモデルから芋ずるに引っ張ってくるとあたかもGenderモデルがDBから取得したデータのように、「女性」や「男性」を返してくれます。
    設定ファイルとかから参照しなくて良いので、かなりストレスフルフリーです。

    echo h($value['Gender']['display']);
    

    (注1)
    ここで一点だけ。どうもcontainableを使う場合、$belongsToの条件に‘fields’ => ‘display’,
    指定をするか、あるいはcontainableのconditionsの中に、フィールドを指定する、
    のどちらかをしないと取ってこれませんでした。
    親モデルからfind(‘all’)の場合は別に指定はいらないのですが、ビヘイビアを使う場合には
    上記指定をしないと結果がarray()になってしまい、途中でフィールド名がunsetされてしまうようでした。
    これ仕様なのかちょっとわからないです。
    でもcontainable使ってるだけだからなあ。。。できればフィールド名は指定しなくてもいいようになるといいんですが。

    ここからは確認していませんが、新規登録の時にもちゃんとlistでとってこれれば

     echo $this->Form->input('gender');
    

    こんなかんじでアソシエーションの恩恵を受けられるのではないでしょうか。

    というわけで、設定ファイルの値をあたかもDBから取得したように振る舞えるArraySource。ぜひみなさんも使ってみてください。