goa勉強会 in 六本木一丁目 参加レポート
やっぱgoa流行ってるんじゃね?
あの素晴らしいOSSのgoaの勉強会が開催されるということで、勢いでトーク枠で登壇することにしました。
スライド一覧
様々な発表がありました。あまり気にせずやってた問題点とかを気づけて面白かったです。というか、自分の口以外から、「goa」というワードが出たことが良かったですね。間違いない。
goa概要@ikawahaさん
あーこのスライド見たことあるわーと思いながら見てました。
私がgoaにコミットするきっかけを作ってくれた人だったので、地味に1人でテンションが上がってました。これでgoaは大体何が良いか分かるので良スライドです。
アイスタイルにおけるgoa導入事例@銀シャリさん
今回のgoa勉強会の主催の銀シャリさんの発表資料です。主催者には本当圧倒的感謝しかないですね。
内容は実際の本番でgoaが動いていますよ。パフォーマンス良いですよ。でも、こんな問題も起きてますよという非常に具体的な内容で、導入を悩んでいる人向けの資料になります。これ聞いてやっぱりgoを書けるエンジニアは少ないんだなーと辛さを感じました。
goa の改善と改良@tchsskさん
goaの開発チームにjoinしているtchsskさんのありがたいお話をいただきました。今後のgoaのロードマップや最近こんなのしましたなど、goaの開発そのものの話を頂きました。
ざっくりまとめると以下のような感じです。
- DSLのv1とv2は互換性がない
- v2からgRPCにも対応する
- まだしばらくかかる
質問で、「コントリビュートするモチベーションはなんですか?」という問いに対して、「趣味・・・いや人生って感じですね」という返しが印象的でしたw
goaを使った開発TIPS@tikasan(俺)
www.slideshare.net
私はいまVOYAGE GROUPの内定者開発である社内外向けのプロダクトを作っていて、それのバックエンドにgoaを使っているので、そこで得た知見や気付きなどを共有するような発表をしました。意外と私のブログを見て頂いている人がいて、かなーーーーり嬉しかったです。今後も継続してアウトプットします。
Generate better JavaScript From Goa Design@rudiさん
goaはJSのコードを自動生成する仕組みはありましたが、内容はただリクエスト投げるぜ!という単調な動きしか出来ず、せっかく定義したバリデーションや型が使えていない問題がありました。
それらを全て解決したJSのコードを自動生成しますよというサードパーティのプラグインを作成しましたというお話でした。(これ本家に入れてもいいんじゃね?と思ったり・・・)
こういったサードパーティ系のプラグインの使い方はまた、このプラグインの使い方と合わせて紹介する予定です。
goaを導入した話@k_yokomiさん
こちらも実際の本番環境でgoaを運用しているお話でした。CIをどうやって運用しているか具体的なお話があったり、やはり、同じく運用していく上で発生した問題などがあり、それらをどうやって解決したかなどなど。こういったナレッジは共有出来るような仕組みがほしいなーと改めて思いました。
飛び込みLT design書くの面倒じゃない?@kawakenさん
www.slideshare.net
goaはただでさえ楽ちんにしてくれているのに、designすら楽しようという、完全に堕落しきってる素晴らしい内容でした。解決策として、スニペットを駆使すれば長いDSLもさくっと書けて楽でいいよね?というお話でした。実際ライブコーディングも素早くコードを構築していくので、これは良い!と思ったので、私も明日からそうします。
まとめ
goaの勉強会というか、言語ではないOSSの勉強会は初でしたが、とても楽しかったです。しかも結構ガッツリ技術よりな発表出来たのは個人的に大きな学びになったので、非常に価値ある時間でした。今後どこまでgoaが流行るかは分かりませんが、地味な活動を続けて、有名にしたいなーと思ったりしてます。
また、こういう機会があって、タイミングが合えば是非また参加したいですね!ありがとうございました!
Google Cloud Next 2017まとめ
ザ・プリンス・パークタワー東京で開催されたGCPのカンファレンスに参加してきました。
個人的に費用が比較的に安価なことや学生には十分過ぎる機能が揃ってることから、私は普段からGCPを使っているのですが、実際に何が良いんだろう?というところをより具体的にしたい。知らない機能や何を目的に展開しているサービスなんだろうということを知るために参加してきました。
※GCPとはGoogle Cloud Platformのことを指します
Google Cloud Next 2017とは
新しいアイデアに出会う。専門家からナレッジを学ぶ。同志とヴィジョンを語りあう。経営者から、IT マネージャー、技術パートナー、デベロッパー、Google エンジニアまで。ダイバーシティに富んだ知が集い、共有することで、次の「未来」が生まれていく。 そんな、クラウドのこれからをリードするイベント “Next” に、あなたも参加しませんか。 Google Cloud Platform から G Suite、マップからデバイスまで。80 を超える幅広いテーマのセッションをご用意してお待ちしています。
- 80を越えるセッション
- 40を越える体験セッション
上記に加えて、参加が無料というのもかなり驚愕なんですが・・・ 当日は、弁当や飲み物も会場で配布されますw
GCPの提供する価値とは?
いきなり本題に入るのですが、Googleが言っていたGCPが提供するものは以下の4つです。
- セキュア
- カスタマーフレンドリー
- インテリジェント
- オープン
※ちょっと見えにくい画像ですが、そこらへんの話をする時によく使ってたスライドです。
ざっくり、ひとつずつどういうことかを説明すると、以下のような感じになります。
セキュア
Googleの基盤を使うことになるので、圧倒的安心感があるのは間違いないと思います。
通信の暗号化に力を入れるのはもちろんですが、ユーザーの認証周りにも非常に厳格な監視システムを入れていることも強くアピールしてました。
インテリジェント
高性能なソリューションが揃っている。
様々なケースを想定したストレージやデータベース
BigQueryは使ってる人多いのでは?
PaaSやIaaSやコンテナなど
カスタマーフレンドリー
GCPの特徴で、私がGCPをよく使う理由のひとつである。柔軟な課金システム(分単位の課金など)は、高いコストパフォーマンスを発揮してくれます。
VMのスペックを要件にあったものを提案してくれる機能があったり。
割引とかなんやら組み合わせると、めっちゃ安くなりますよの図
プレイド社がAWSから移行したことにより、性能とコストの両方で高いパフォーマンスを発揮したそうです。
オープン
様々なソリューションがオープンに使える。これは利用者がこれまでは技術的に不可能だったことが可能になったり、新しいビジネスチャンスを生むことを意味しています。
今後もGoogleはOSSへのコミットを続け、業界を引っ張っていくようです。これはこれまでの貢献があるGoogleだからこそ説得力がありました。
テクノロジを使った働き方改革
今回のカンファレンスでGoogleがアピールしていたものはテクノロジも勿論ですが、働き方を変えるという取り組みについてでした。結構面白い取り組みだったので軽く紹介します。
色々な切り口から、働き方を改革をし、実際に業務効率化をした事例がたくさん紹介されました。
ジャムボード
ジャムボードという新しい製品のデモが行われました。遠隔に同時に操作が出来るホワイトボードで、ポストイット機能や文字認識をして、フォント化したりとかなりの高性能でかなりテンション上がる製品でした。
www.youtube.com
公式の動画が分かりやすいので貼っておきます。
チャット
チャットはSlack的な使い方が出来るっぽいです。Botなども作成出来るので、これは使えそう・・・!Googleドキュメントなどと親和性が高いので色々な便利機能があったり、そのままビデオ会議にも繋げたりも可能なそうです。
Drive File Stream
www.itmedia.co.jp
ダウンロードせずに様々なファイルを操作可能
ここでは、紹介しきれないくらい他にも様々なサービスがありました。いま内定者開発でリモート開発をやっているので、こういうソリューションに興味湧いたりしてました。
技術系で面白かったもの
結局どんな技術あったんやっていうのも気になる方向けに面白かった技術系の紹介をします。
Cloud Spanner
一言で言うと、リレーションがあるけど、いくらでもスケールできるデータベースらしいです。(正直意味がわからない)
スケールさせるとなると問題になるのが、データの整合性ですが、常に同期が取れるような仕様になっているそうです。何故リレーションがあるのに、そこまで高速に同期が可能になっているか?
上の画像のような一般的なテーブルのものをSpannerでは、以下のように管理しているそうです。
うむ。なるほど。理屈はわかる。どうやってるんだ。
基調講演でとんでもないリクエストを処理しきってるデモも行われました。
Cloud Video Intelligence
このAPIは投げるだけで、動画にどういったものが写っているかを解析して、時間と情報をまとめて返してくれるという驚異的なものでした。
海と検索すると、動画内に海が登場するものが抽出でき、しかも、実際に写ってる時間が分かるので見たい部分だけ見ることも出来る。
こんな感じで、細かい解析を行っており、犬の種類まで特定出来たりと、すごいとしか言えないものでした。
最近は、動画コンテンツがかなり注目を浴びてるので、検索の性能の向上や不適切な動画の摘発などにも一役買いそうな雰囲気がプンプンしました。
Data Loss Prevention API
お客さんとのやり取りでもらった個人情報などを保存したくないけど、やり取りは残したい。そんなケース結構あると思います。このAPIを使うと消したい情報を指定して、実行するだけで勝手にマスキングしてくれます。
特に最近はリスクを減らす為、出来る限り顧客の情報を持たずにマスキングしたりする会社が多いので、かなり使えるAPIだと思います。
まとめ
APIひとつ叩くだけで、Googleの技術を使えるという贅沢な時代に生まれてよかった。
今後、追加で記事にする予定かもしれないもの
- Google App Engine
- メルカリ、ソウゾウの導入事例
- Cloud Datastore
PHPのLaravel5.4でアプリケーションを作る(Migration編)
マイグレーション
そもそもマイグレーションとは何だ?という人向けに公式のドキュメントが分かりやすく説明してくれてます。
マイグレーションとはデータベースのバージョンコントロールのようなもので、チームでアプリケーションデータベースのスキーマを簡単に更新し共有できるようにしてくれます。通常、マイグレーションはアプリケーションのデータベーススキーマを簡単に構築できるようにする、Laravelのスキーマビルダーと一緒に使用します。
データベースのバージョン管理ということです。
LaravelのSchemaファサードはテーブルの作成や操作をどのデータベースエンジンを使うかに関わらずサポートします。Laravelがサポートしているデータベースシステム全部で同じ記述法と書きやすいAPIを共用できます。
つまり、Laravelのマイグレーションの機能を使えば、様々なデータベースエンジンに対応するマイグレーションが出来上がるということのようです。 では、さっそくマイグレーションのコードを書いていきます。
準備
Migrationするということは、何でもいいのでデータベースを用意する必要があります。今回はこちらが用意したDockerでMySQLを使うことにします。
make docker/build make docker/start
項目 | 値 |
---|---|
HOST | 127.0.0.1 |
PORT | 3306 |
DATABASE | celler |
USERNAME | celler |
PASSWORD | celler |
config/database.php
に以下のような感じでデータベースとの接続設定をしてください。(個々の環境に合わせてください)
'mysql' => [ 'driver' => 'mysql', 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '3306'), 'database' => env('DB_DATABASE', 'celler'), 'username' => env('DB_USERNAME', 'celler'), 'password' => env('DB_PASSWORD', 'celler'), 'unix_socket' => env('DB_SOCKET', ''), 'charset' => 'utf8', 'collation' => 'utf8_general_ci', 'prefix' => '', 'strict' => true, 'engine' => null, ],
上記に加えて、.env
という設定ファイルにもデータベース情報を追記してください。
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=celler DB_USERNAME=celler DB_PASSWORD=celler
.env
を設定しないと、homestead
ユーザーを使ってデータベースにアクセスしようとします。
[Illuminate\Database\QueryException] SQLSTATE[HY000] [1045] Access denied for user 'homestead' @'localhost' (using password: YES) (SQL: select * from in formation_schema.tables where table_schema = homestead an d table_name = migrations)
config/database.php
はModel用の定義で、.env
はartisanのための設定ファイルのようです。
実装
$ php artisan make:migration create_accounts_table
以下の形式でファイルが作成されます。
database/migrations/yyyy_mm_dd_timestamp_create_accounts_table.php
↑具体例
2017_05_28_143736_create_accounts_table.php
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateAccountsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('accounts', function (Blueprint $table) { $table->bigIncrements('id')->comment('ID'); $table->string('name')->comment('名前'); $table->string('email')->comment('メールアドレス'); $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP')); $table->softDeletes(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('accounts'); } }
php artisan migrate Migrating: 2017_05_28_143736_create_accounts_table Migrated: 2017_05_28_143736_create_accounts_table
上記のようなメッセージが出たら正しくテーブルが作成されます。
他テーブルも対応
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateBottlesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('bottles', function (Blueprint $table) { $table->bigIncrements('id')->comment('ID'); $table->string('name')->comment('ボトル名'); $table->integer('quantity')->default(0)->comment('数量'); $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP')); $table->softDeletes(); $table->unsignedBigInteger('account_id'); $table->foreign('account_id') ->references('id') ->on('accounts') ->onDelete('restrict') ->onUpdate('restrict'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('bottles'); } }
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateCategoriesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('categories', function (Blueprint $table) { $table->bigIncrements('id')->comment('ID'); $table->string('name')->comment('カテゴリ名'); $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP')); $table->softDeletes(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('categories'); } }
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateBottleCategoriesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('bottle_categories', function (Blueprint $table) { $table->bigIncrements('id')->comment('ID'); $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP')); $table->softDeletes(); $table->unsignedBigInteger('category_id'); $table->unsignedBigInteger('bottle_id'); $table->foreign('bottle_id') ->references('id') ->on('bottles') ->onDelete('restrict') ->onUpdate('restrict'); $table->foreign('category_id') ->references('id') ->on('categories') ->onDelete('restrict') ->onUpdate('restrict'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('bottle_categories'); } }
php artisan migrate Migrating: 2017_05_28_143736_create_accounts_table Migrated: 2017_05_28_143736_create_accounts_table Migrating: 2017_05_29_125546_create_bottles_table Migrated: 2017_05_29_125546_create_bottles_table Migrating: 2017_05_30_125835_create_categories_table Migrated: 2017_05_30_125835_create_categories_table Migrating: 2017_05_30_130002_create_bottle_categories_table Migrated: 2017_05_30_130002_create_bottle_categories_table php artisan migrate:status +------+--------------------------------------------------+ | Ran? | Migration | +------+--------------------------------------------------+ | Y | 2017_05_28_143736_create_accounts_table | | Y | 2017_05_29_125546_create_bottles_table | | Y | 2017_05_30_125835_create_categories_table | | Y | 2017_05_30_130002_create_bottle_categories_table | +------+--------------------------------------------------+
よく使うコマンド
コマンド | 説明 |
---|---|
php artisan migrate | マイグレーションを実行する |
php artisan migrate:rollback | 最後に実行したマイグレーションをまとめて元に戻す |
php artisan migrate:reset | 全てのマイグレーションをロールバックする |
php artisan migrate:refresh | 全てのマイグレーションをロールバックし、マイグレーションをする |
php artisan migrate:status | 現在のマイグレーションの状態を参照する |
参考になった日本語情報
- laravelでのマイグレーション作成手順 - Qiita
- LaravelのMigrationについて調べた結果 - Qiita
- Laravelで外部キー制約のあるテーブルを操作する。 - Qiita
- Laravel 外部キーを含むテーブルをmigrationファイルで作成する | たきもと.com
- マイグレーションでテーブルカラムにコメントを設定する - Qiita
- LaravelのDB migrationで日付のデフォルトを指定 - Qiita
- Laravel入門 Lesson 8 - Scheme Builder(Migration) - Qiita
MigrationとSeederを組み合わせると便利なので、後日また記事にする予定です。
PHPのLaravel5.4でアプリケーションを作る(インストールら編)
Laravelとは
PHPでイケてるフレームワークで知られているアレです。最近は日本でも採用例が増えているされているので、使い方を知っておいて損はないと思います。また、日本語情報が本当に充実していて、学習のしやすさも魅力だと思います。
インストールとプロジェクト作成
これに関しては公式ドキュメントを読めばおkだと思います。
$ laravel new celler
上記のコマンドで開発に必要なフォルダ群が作成されます。
celler ├── app ├── artisan ├── bootstrap ├── composer.json ├── composer.lock ├── config ├── database ├── package.json ├── phpunit.xml ├── public ├── resources ├── routes ├── server.php ├── storage ├── tests ├── vendor ├── webpack.mix.js └── yarn.lock
$ php artisan serve
上記のコマンドを実行すると、http://localhost:8000
にアクセスすると、良さげなwelcomeページが見れます。では、ここからはさくっと動かすところまでやっていきたいと思います。
突然登場したartisanですが、これかなり便利です。
作るものについて
以下のMySQLのテーブルを用意して、CRUDするようなアプリケーションを作成します。 今回はとりあえず、固定値をViewに渡すところまでやりたいと思います。
View
resources/views/accounts/list.blade.php
とりあえず、受け取った値を表示するだけのViewを作成します。
ビュー 5.4 Laravel
見た目部分はそれっぽくなるBootstrapを使用しています。
Bootstrap · The world's most popular mobile-first and responsive front-end framework.
<!doctype html> <html lang="{{ config('app.locale') }}"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href=""> <title>Document</title> <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet"> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script> <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> <![endif]--> </head> <body> <div class="container"> <h1>accounts</h1> <table class="table"> <tr> <th>ID</th> <th>name</th> <th>email</th> <th>updated_at</th> <th>created_at</th> </tr> @foreach ($accounts as $account) <tr> <td>{{$account['id']}}</td> <td>{{$account['name']}}</td> <td>{{$account['email']}}</td> <td>{{$account['updated_at']}}</td> <td>{{$account['created_at']}}</td> </tr> @endforeach </table> </div> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script src="js/bootstrap/bootstrap.min.js"></script> </body> </html>
Controller
$ php artisan make:controller AccountsController
app/Http/Controllers/AccountsController.php
コントローラーの雛形が作成されます。
コントローラ 5.4 Laravel
namespace App\Http\Controllers; use Illuminate\Http\Request; class AccountsController extends Controller { public function index() <---追加 { $account['id'] = 1; $account['name'] = 'pei'; $account['email'] = 'example@gmail.com'; $account['updated_at'] = '2016-02-02 10:10:10'; $account['created_at'] = '2016-02-02 10:10:10'; $accounts[] = $account; $account['id'] = 2; $account['name'] = 'pei2'; $account['email'] = 'example2@gmail.com'; $account['updated_at'] = '2017-02-02 10:10:10'; $account['created_at'] = '2017-02-02 10:10:10'; $accounts[] = $account; return view('accounts.list',compact('accounts')); } }
Routing
routes/web.php
にルーティングの定義を追加しました。
ルーティング 5.4 Laravel
// GET /accountsにアクセスされたら、AccountsControllerのindexが実行される Route::get('/accounts', 'AccountsController@index');
いざ実行!
$ php artisan serve $ open http://localhost:8000/accounts
上記のような結果が帰ってきてたらOKです。
これで、さくっとアプリケーションを作れました。まだ全然触れていませんが、イケてる雰囲気を既に感じている。
GolangのgoaでAPIをデザインしよう(Model編②)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
Model層をいい感じにしてくれるgorma
前回の記事で、紹介したgormaですが、gormの仕様上、すごーーーーーく多対多参照が分かりにくいです。これについては慣れれば大した問題ではないので、やり方を紹介したいと思います。
なので、この記事はgormaの記事というよりは、gormでの多対多つまりManyToManyの参照方法の解説に近くなりそうです。
今回使うサンプル
github.com
$ go get github.com/tikasan/goa-simple-sample または $ ghq get github.com/tikasan/goa-simple-sample
今回のケースのManyToManyの対応例
普通はManyToManyというと、お互いのテーブルに複数出現するような関係なので、bottles
とcategories
が直接繋がっていることを想像すると思いますが、gormにおいてはbottle_categories
を無視して、ManyToManyで、bottles
とcategories
を繋げることで参照することが出来ます。
bottlesテーブルの例
関係性の見方
関係性 | 対象Model名 |
---|---|
Belongs To | Account |
Has Many | BottleCategory |
Many To Many | Category |
これを理解した上で、gormaでどうすれば良いか?
Model("Bottle", func() { RendersTo(BottleData) Description("celler bottle") Field("id", gorma.Integer, func() { PrimaryKey() }) Field("name", gorma.String) Field("quantity", gorma.Integer) Field("created_at", gorma.Timestamp) Field("updated_at", gorma.Timestamp) Field("deleted_at", gorma.NullableTimestamp) BelongsTo("Account") HasMany("BottleCategories", "BottleCategory") ManyToMany("Category", "bottle_categories") <-----これ })
上記のような修正を行った上で、コード生成をし直すと。
// celler bottle type Bottle struct { ID int `gorm:"primary_key"` AccountID int // Belongs To Account BottleCategories []BottleCategory // has many BottleCategories Categories []Category `gorm:"many2many:bottle_categories"` <---これ Name string Quantity int CreatedAt time.Time // timestamp DeletedAt *time.Time // nullable timestamp (soft delete) UpdatedAt time.Time // timestamp Account Account }
実装
これで、準備が整いました。実際にどのようにすれば参照することが出来るか、コントローラーとモデルのクエリを実装します。
Model
横に長過ぎるので、gistに上げました。
処理の解説
var native []*Bottle var objs []*app.BottleRelation err := m.Db.Scopes(BottleFilterByAccount(accountID, m.Db)). // bottlesテーブルをベースにクエリを実行する Table(m.TableName()). // PreloadでbottleCategoriesテーブルを呼ぶ Preload("BottleCategories"). // Preloadでaccountsテーブルを呼ぶ Preload("Account"). // 複数件取得する Find(&native). Error
ここまでで処理で取得出来るjson
[ { "account": { "email": "example1@gmail.com", "id": 1, "name": "ユーザー1" }, "categories": null, <---ここは取れていない ], "id": 1, "name": "赤ワイン1", "quantity": 20 }, { "account": { "email": "example1@gmail.com", "id": 1, "name": "ユーザー1" }, "categories": null, "id": 2, "name": "赤ワイン2", "quantity": 10 } ]
カテゴリを取得する(少しパフォーマンスが気になる)
Relatedというメソッドも使えるそうですが、InnerJOINの動きに限定されます。
var native []*Bottle var objs []*app.BottleRelation // 取得したbottlesの情報をループさせる for k, v := range native { // bottlesの中のbottle_categoriesの情報をループさせる for k2, v2 := range v.BottleCategories { // bottles_categoriesの情報を使って、個々のカテゴリの詳細情報を取得する native2 := []*BottleCategory{} m.Db.Preload("Category"). Where("bottle_id = ?", v2.BottleID). Find(&native2) // 取得した情報を元のBottlesの構造体に代入する native[k].Categories = append(native[k].Categories, native2[k2].Category) } }
この処理で出来上がるjson
[ { "account": { "email": "example1@gmail.com", "id": 1, "name": "ユーザー1" }, "categories": [ { "id": 1, "name": "赤" }, { "id": 3, "name": "ワイン" } ], "id": 1, "name": "赤ワイン1", "quantity": 20 }, { "account": { "email": "example1@gmail.com", "id": 1, "name": "ユーザー1" }, "categories": [ { "id": 2, "name": "白" }, { "id": 3, "name": "ワイン" } ], "id": 2, "name": "赤ワイン2", "quantity": 10 } ]
Controller
このメソッドを使って、実装するコントローラーのコードは以下の通りです。
// ListRelation runs the listRelation action. func (c *BottlesController) ListRelation(ctx *app.ListRelationBottlesContext) error { // BottlesController_ListRelation: start_implement // Put your logic here bdb := models.NewBottleDB(c.db) b := bdb.ListBottleFullScan(ctx.Context, 0) // BottlesController_ListRelation: end_implement res := app.BottleRelationCollection{} res = b return ctx.OKRelation(res) }
まとめ
gormaを使えばよくあるクエリへの対応などは問題なく出来ます。ですが、どうしても無理な場面もあるので、やっている案件に発生するクエリの複雑さによって採用するかしないか決めるべきだと思います。今回の記事のやり方はあくまで私が考えたやり方なので、もっと良い方法があれば教えて欲しいです!
Goの並列処理の動作を理解する
Goやるなら並列処理やるでしょ
Goのイメージ = 並列処理というイメージがある人は多いと思います。Goはケアが難しかった並列処理を他の言語よりも比較的扱いがしやすいようになっていて、わりと手軽に書けたりします。ですが、なんとなくで使っていると思わぬメモリリークの発生などが発生したりします。パフォーマンスアップのつもりが、パフォーマンスダウンになってしまうことも・・・。
私自身もちゃんと理解できていないなと感じることがあったので、今回はざっくり理解していきたいと思います。
今回の記事のソースは以下のRepoになります。
並列処理って何が良いの
何が嬉しいのかって話をまずしますと、以下のような合計で6秒どうしてもかかってしまう処理があったとします。確かに順次処理でやっていくと、各処理は2秒ずつかかってしまいます。ですが、例えばこれが同時に処理できたらどうでしょう?最大でも2秒で終わる処理に変わりますよねっていうのが並列処理です。
package main import ( "fmt" "testing" "time" ) func main() { result := testing.Benchmark(func(b *testing.B) { run() }) fmt.Println(result.T) } func run() { fmt.Println("Start!") process("A") process("B") process("C") fmt.Println("Finish!") } func process(name string) { time.Sleep(2 * time.Second) fmt.Println(name) }
$ go run exp1/main.go Start! A B C Finish! 6.010750402s
よくあるミス
Goはgo
キーワードを使って関数を実行するだけで、goroutine(サブスレッドのようなもの)が生成され、並列に処理が実行されますが、使い方には注意が必要です。最初に意味もわからずよくやってしまいがちなミスの例を示すと。
package main import ( "fmt" "time" ) func main() { fmt.Println("Start!") go process("A") <------ goキーワードで関数実行するとgoroutineが生成される go process("B") go process("C") fmt.Println("Finish!") } func process(name string) { time.Sleep(2 * time.Second) fmt.Println(name) }
$ go run exp2/main.go Start! Finish!
上記のような感じで、一瞬で終わりますが、さっきまで出力されていたA,B,Cの文字列が消えています。何故こうなるかについて説明すると、go
キーワードによって生成されたgoroutineに処理を任せてるので、最初のgo run
で生成されたgoroutineと同期を取っていないので勝手に終わってしまっているからです。これを防ぐには何らかの連絡手段を使って同期を取る必要があります。それがchannel
です。
channelを使って同期させよう
ここからの説明では、聞き覚えのあるワードを使って説明します。
スレッド名 | 実行のされ方 | 具体例 |
---|---|---|
メインスレッド | go run *.go | go run exp1/main.go |
サブスレッド | go hgoe() | go process(“A”) |
channel
は何かと言うと、簡単に言うと専用の電話線のようなものです。これを使うことでメインスレッドとサブスレッドの連絡が取れるようになります。以下にサブスレッドの処理と同期をするためのシンプルな例があります。
package main import ( "fmt" "time" ) func main() { fmt.Println("Start!") // boolの型でchannelを作成する ch := make(chan bool) // goroutineを生成して、サブスレッドで処理する go func() { time.Sleep(2 * time.Second) // chに対してtrueを投げる(送信) ch <- true }() // chに受信があったらisFinに返事する。 // 受信があるまで、処理をブロックし続ける(これで同期が取れる) isFin := <-ch // <-chだけでもブロック出来る // chをクローズする close(ch) // 受信した値をprintする fmt.Println(isFin) fmt.Println("Finish!") }
$ go run exp3/main.go Start! true Finish!
以下のどちらかの記述を使うことでブロックすることが出来るのがポイントとなります。
isFin <- ch
<- ch
channelの動作を使って、並列処理をすると以下のようになります。
package main import ( "fmt" "time" ) func main() { // それぞれとの連絡のためのchを作成する isFin1 := make(chan bool) isFin2 := make(chan bool) isFin3 := make(chan bool) fmt.Println("Start!") go func() { process("A") isFin1 <- true }() go func() { process("B") isFin2 <- true }() go func() { process("C") isFin3 <- true }() // 全部が終わるまでブロックし続ける <-isFin1 <-isFin2 <-isFin3 fmt.Println("Finish!") } func process(name string) { time.Sleep(2 * time.Second) fmt.Println(name) }
$ go run exp4/main.go Start! A B C Finish! 2.003493386s
約2秒で終了しました。これだけで、約4秒短く終わらせることが出来るのもすごいですが、非常に簡単に記述することが出来るのもGoのすごいところです。ですが、いまの記述だと非常にひ弱なコードになっています。
例えば、今までとは違い処理の数が変動する場合どうでしょう?chの数が固定だと破綻します。そういったイレギュラーに対応するには、処理の数とchのブロックをうまく使えば、同期処理が可能ですが、直感的でないので、sync.WaitGroup
を使うことをおすすめします。
sync.WaitGroupでいい感じにする
package main import ( "fmt" "testing" "time" "sync" ) func main() { result := testing.Benchmark(func(b *testing.B) { run("A", "B", "C", "D", "E") }) fmt.Println(result.T) } func run(name ...string) { fmt.Println("Start!") // WaitGroupを作成する wg := new(sync.WaitGroup) // channelを処理の数分だけ作成する isFin := make(chan bool, len(name)) for _, v := range name { // 処理1つに対して、1つ数を増やす(この例の場合は5になる) wg.Add(1) // サブスレッドに処理を任せる go process(v, isFin, wg) } // wg.Doneが5回通るまでブロックし続ける wg.Wait() close(isFin) fmt.Println("Finish!") } func process(name string, isFin chan bool, wg *sync.WaitGroup) { // wgの数を1つ減らす(この関数が終了した時) defer wg.Done() time.Sleep(2 * time.Second) fmt.Println(name) isFin <- true }
$ go run exp5/main.go Start! B D E A C Finish! 2.005301726s
sync.WaitGroup
は、処理は走る回数分だけ、wg.Add(int)
します。基本的には、1処理に対してwg.Add(1)
で問題ないと思います。そして、その処理が終わったら、wg.Done
で終了を知らせます。
最後の方に書いているwg.Wait()
はwg.Add(int)
の数分だけのwg.Done()
が通るまで、処理をブロックし続けます。そして、決められた回数処理が終わるとブロックを終了します。
まとめ
以上で簡単な並列処理なら問題なく実行出来ると思います。ただ、実際に使うとなると、例えば並列処理の数は5つまでにしたいとか、ある処理が終わるまでスタートしてほしくないとか、色々あると思います。次回はそこらへんもやってみたいと思います。
GolangのgoaでAPIをデザインしよう(Model編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
Model層をいい感じにしてくれるgorma
goaはAPIの入り口と出口の部分をいい感じにしてくれますが、Modelとのやり取りなどのロジックは手で書く必要があります。正直だるくないっすか?だるいですよね。簡単なRDBMSとのやり取り程度なら実は自動でいい感じにしてくれるプラグインがgormaです。
goaには、goagenというものがあり、これに独自で書いたコード生成ロジックを作成して食わせるといい感じにしてくれます。gormaはそのコード生成ロジックのModel部分のものです。
gormaがやってくれること
では、実際にgormaがやってくれる素晴らしい仕事をざっくりではありますけど、紹介します。
CRUDは秒で終わる
以下によくあるエンドポイントの例があります。これくらいなら、gormaに任せたら実装は終了します。
GET /api/v1/bottles bottlesを複数取得する GET /api/v1/bottles/{id} bottlesから1件取得する POST /api/v1/bottles bottlesを1件作成する DELETE /api/v1/bottles/{id} bottlesを1件削除する PUT/api/v1/bottles/{id} bottlesを1件更新する
ちょっとした入れ子構造はいい感じにしてくれる
bottlesというリソースがあったとします。bottlesのひとつひとつには、所有者であるaccountsのリソースに紐付いています。jsonでいい感じに返してくださいという案件があったとします。以下のようなjsonがほしくなります。
gormaでやると秒で終わります。
[ { "account": { "email": "example@gmail.com", "id": 1, "name": "山田 太郎" }, "id": 1, "name": "シャルドネ", "quantity": 4 }, { "account": { "email": "example@gmail.com", "id": 1, "name": "山田 太郎" }, "id": 1, "name": "シャルドネ", "quantity": 4 } ]
カスタマイズ出来る
gormaってよくわかんないやつ嫌だなーと最初は思ってたんですけど、実はこいつはgormというORMのコードを自動生成してくれてるだけなので、gormの使い方を理解していればカスタマイズはいくらでも出来ます。
さっそくgorma開発
gormaの開発には当然ですけど、RDBMSで使うDDLやMySQLなどの環境構築が必要になります。なので、用意しました。最低限必要なのはDockerが動く環境なので、適宜用意してください。
今回使うサンプル
github.com
$ go get github.com/tikasan/goa-simple-sample または $ ghq get github.com/tikasan/goa-simple-sample
上記のいずれかのコマンドで、ローカルに落としてきてください。
環境構築と実行
Docker立ち上げて、DDL読み込ませて、起動してSwaggerで実行してみます。
$ make docker/build $ make docker/start $ make migrate/up $ make rundb $ make swaggerUI
Bottlesリソースに対してのアクションをしてみましょう。
とりあえず、レスポンスが返ってきました。では、どういう動きだったかについて詳しく説明します。
やったこと
ER図
発行したSQL
SELECT * FROM `bottles` WHERE `bottles`.`deleted_at` IS NULL SELECT * FROM `accounts` WHERE `accounts`.`deleted_at` IS NULL AND ((`id` IN ('1','2','2','3')))
goaコード
MediaType
var Account = MediaType("application/vnd.account+json", func() { Description("celler account") Attributes(func() { Attribute("id", Integer, "id", func() { Example(1) }) Attribute("name", String, "名前", func() { Example("山田 太郎") }) Attribute("email", String, "メールアドレス", func() { Example("example@gmail.com") }) Required("id", "name", "email") }) View("default", func() { Attribute("id") Attribute("name") Attribute("email") }) }) var Bottle = MediaType("application/vnd.bottle+json", func() { Description("celler bottles") Attributes(func() { Attribute("id", Integer, "id", func() { Example(1) }) Attribute("name", String, "ボトル名", func() { Example("シャルドネ") }) Attribute("quantity", Integer, "数量", func() { Example(4) }) // accountのMediaTypeを入れ子構造にする Attribute("account", Account) Required("id", "name", "quantity", "account") }) View("default", func() { Attribute("id") Attribute("name") Attribute("quantity") Attribute("account") }) }) var Category = MediaType("application/vnd.category+json", func() { Description("celler account") Attributes(func() { Attribute("id", Integer, "id", func() { Example(1) }) Attribute("name", String, "名前", func() { Example("ワイン") }) Required("id", "name") }) View("default", func() { Attribute("id") Attribute("name") }) })
エンドポイント
var _ = Resource("bottles", func() { BasePath("/bottles") // --------- さっき実行したのはこれ --------- Action("list", func() { Description("複数") Routing( GET("/"), ) Response(OK, CollectionOf(Bottle)) Response(BadRequest, ErrorMedia) }) // --------- さっき実行したのはこれ --------- Action("show", func() { Description("単数") Routing( GET("/:id"), ) Params(func() { Param("id", Integer, "id", func() { Example(1) }) }) Response(OK, Bottle) Response(NotFound) Response(BadRequest, ErrorMedia) }) Action("add", func() { Description("追加") Routing( POST("/"), ) Params(func() { Param("account_id", Integer, "アカウントID", func() { Example(1) }) Param("name", String, "ボトル名", func() { Default("") Example("赤ワインなにか") }) Param("quantity", Integer, "数量", func() { Example(0) }) Required("account_id", "name", "quantity") }) Response(Created) Response(BadRequest, ErrorMedia) }) Action("delete", func() { Description("削除") Routing( DELETE("/:id"), ) Params(func() { Param("id", Integer, "id", func() { Example(1) }) }) Response(OK) Response(NotFound) Response(BadRequest, ErrorMedia) }) Action("update", func() { Description("更新") Routing( PUT("/:id"), ) Params(func() { Param("id", Integer, "id", func() { Example(1) }) Param("name", String, "ボトル名", func() { Default("") Example("赤ワインなにか") }) Param("quantity", Integer, "数量", func() { Default(0) Minimum(0) Example(0) }) }) Response(OK) Response(NotFound) Response(BadRequest, ErrorMedia) }) })
gormaコード
Model
gormaには、命名規則があります。
種類 | 説明 | gormaでの書き方 |
---|---|---|
テーブル名 | テーブル名は複数形 | bottles,categories |
カラム名 | カラム名はスネークケース | user_id |
Model名 | テーブル名が複数形でみModelは単数形 | Bottle,Category |
PrimaryKey | 必ずid統一する (IntegerかUUID) | id |
timestamp | created_at,updated_at,deleted_atで統一 | --- |
特にModel名とテーブル名が同じにならないのは、結構ハマりポイントだったりする。この他にもあるかもしれませんが、基本的にgormの規則に従っています。
実際に組んだコードを以下に示します。
package design import ( "github.com/goadesign/gorma" . "github.com/goadesign/gorma/dsl" ) var _ = StorageGroup("celler", func() { Description("celler Model") // Mysqlを使う Store("MySQL", gorma.MySQL, func() { Description("MySQLのリレーションナルデータベース") // accountsテーブルのModelなら、Account Model("Account", func() { // MediaTypeで作成したAccountにマッピングする RendersTo(Account) Description("celler account") // PrimaryKeyの設定 Field("id", gorma.Integer, func() { PrimaryKey() }) Field("name", gorma.String) Field("email", gorma.String) // timestamp系の定義 Field("created_at", gorma.Timestamp) Field("updated_at", gorma.Timestamp) Field("deleted_at", gorma.NullableTimestamp) // HasMany(複数形名, 単数形名) HasMany("Bottles", "Bottle") }) Model("Bottle", func() { RendersTo(Bottle) Description("celler bottle") Field("id", gorma.Integer, func() { PrimaryKey() }) Field("name", gorma.String) Field("quantity", gorma.Integer) Field("created_at", gorma.Timestamp) Field("updated_at", gorma.Timestamp) Field("deleted_at", gorma.NullableTimestamp) // BelongTo(単数形) BelongsTo("Account") HasMany("BottleCategories", "BottleCategory") }) Model("Category", func() { RendersTo(Category) Description("celler category") Field("id", gorma.Integer, func() { PrimaryKey() }) Field("name", gorma.String) Field("created_at", gorma.Timestamp) Field("updated_at", gorma.Timestamp) Field("deleted_at", gorma.NullableTimestamp) HasMany("BottleCategories", "BottleCategory") }) Model("BottleCategory", func() { Description("celler bottle category") Field("id", gorma.Integer, func() { PrimaryKey() }) Field("created_at", gorma.Timestamp) Field("updated_at", gorma.Timestamp) Field("deleted_at", gorma.NullableTimestamp) BelongsTo("Category") BelongsTo("Bottle") }) }) })
gormaのコード生成は以下のコマンドです。
$ go get github.com/goadesign/gorma <--- gorma入れてない人はこれする $ goagen --design=$(REPO)/design gen --pkg-path=github.com/goadesign/gorma
models/ ├── account.go ├── account_helper.go ├── bottle.go ├── bottle_helper.go ├── bottlecategory.go ├── bottlecategory_helper.go ├── category.go └── category_helper.go
上記のようなファイル群が出てきたかと思います。それぞれのファイルが持っているコードは以下のようになっています。
ファイル名 | 説明 |
---|---|
model名.go | gormに必要な構造体とCRUDの最低限のメソッド |
model名_helper.go | MediaTypeに合わせたRead系メソッド集 |
では、実際にクエリを投げるために、下準備をします。
gormaでは、外部テーブルからの参照系のクエリをデフォルトでは実装されていません。恐らくなんでもかんでも結合すると、クエリの処理速度が遅くなるからだと思われす。models/bottle_helper.go
// ListBottle returns an array of view: default. func (m *BottleDB) ListBottle(ctx context.Context, accountID int) []*app.Bottle { defer goa.MeasureSince([]string{"goa", "db", "bottle", "listbottle"}, time.Now()) var native []*Bottle var objs []*app.Bottle //err := m.Db.Scopes(BottleFilterByAccount(accountID, m.Db)).Table(m.TableName()).Find(&native).Error // Accountテーブルから、情報を取得したいので、Preloadメソッドを追加する err := m.Db.Scopes(BottleFilterByAccount(accountID, m.Db)).Table(m.TableName()).Preload("Account").Find(&native).Error if err != nil { goa.LogError(ctx, "error listing Bottle", "error", err.Error()) return objs } for _, t := range native { objs = append(objs, t.BottleToBottle()) } return objs }
上記のように、メソッドを直接書き換えるのも良いですが、自動生成されたメソッドをあまり書き換えたくないというのもあるので、私はユーザー定義用のファイルを作成して対応したりしてます。
bottle_defined.go model名_defined.go
そして、modelのデザインに変更が発生して、再生成する時は、以下のMakeコマンドを走らせることで、安全にコードを再生するようにしています。
model: @ls models | grep -v '_defined.go' | xargs rm -f @goagen --design=$(REPO)/design gen --pkg-path=github.com/goadesign/gorma
次にコントローラーを定義します。controller/bottles.go
package controller import ( "github.com/goadesign/goa" "github.com/jinzhu/gorm" "github.com/tikasan/goa-simple-sample/app" "github.com/tikasan/goa-simple-sample/models" ) // BottlesController implements the bottles resource. type BottlesController struct { *goa.Controller db *gorm.DB } // NewBottlesController creates a bottles controller. func NewBottlesController(service *goa.Service, db *gorm.DB) *BottlesController { return &BottlesController{ Controller: service.NewController("BottlesController"), db: db, } } // Add runs the add action. func (c *BottlesController) Add(ctx *app.AddBottlesContext) error { // BottlesController_Add: start_implement // Put your logic here b := &models.Bottle{} b.AccountID = ctx.AccountID b.Name = ctx.Name b.Quantity = ctx.Quantity bdb := models.NewBottleDB(c.db) err := bdb.Add(ctx.Context, b) if err != nil { return ctx.BadRequest(goa.ErrBadRequest(err)) } // BottlesController_Add: end_implement return ctx.Created() } // Delete runs the delete action. func (c *BottlesController) Delete(ctx *app.DeleteBottlesContext) error { // BottlesController_Delete: start_implement // Put your logic here bdb := models.NewBottleDB(c.db) err := bdb.Delete(ctx.Context, ctx.ID) if err != nil { return ctx.BadRequest(goa.ErrBadRequest(err)) } // BottlesController_Delete: end_implement return nil } //--------------------------------- // 例で使った部分 //--------------------------------- // List runs the list action. func (c *BottlesController) List(ctx *app.ListBottlesContext) error { // BottlesController_List: start_implement // Put your logic here bdb := models.NewBottleDB(c.db) b := bdb.ListBottle(ctx.Context, 0) // BottlesController_List: end_implement res := app.BottleCollection{} res = b return ctx.OK(res) } // Show runs the show action. func (c *BottlesController) Show(ctx *app.ShowBottlesContext) error { // BottlesController_Show: start_implement // Put your logic here bdb := models.NewBottleDB(c.db) b, err := bdb.OneBottle(ctx.Context, ctx.ID, 0) if err != nil { return ctx.NotFound() } // BottlesController_Show: end_implement res := &app.Bottle{} res = b return ctx.OK(res) } // Update runs the update action. func (c *BottlesController) Update(ctx *app.UpdateBottlesContext) error { // BottlesController_Update: start_implement // Put your logic here b := &models.Bottle{} b.ID = ctx.ID b.Name = ctx.Name b.Quantity = ctx.Quantity bdb := models.NewBottleDB(c.db) err := bdb.Update(ctx.Context, b) if err == gorm.ErrRecordNotFound { return ctx.NotFound() } else if err != nil { return ctx.BadRequest(goa.ErrBadRequest(err)) } // BottlesController_Update: end_implement return nil }
これで、完成しました。自動的にマッピングも完了し、欲しい情報を簡単に取得することが出来ます。
gormaは多少取っ付きにくさはありますが、理解すると、goaでの開発がさらにスピードアップするので、結構おすすめです。
gormaですんなり出来ない多対多問題
gormaの作成してくれたコードだけでは、当然出来ないことがあります。それは多対多のリレーションの場合です。これは元々のgormの癖が強いというこもあり、かなり苦労しました。
この解決方法が全然日本語情報がない上に、しかも、よく見かけるケースなので、また記事にする予定です。 今回はgormaをさくっと使うところまでということで、ここまでにしておきます。