ぺい

渋谷系アドテクエンジニアの落書き

goa勉強会 in 六本木一丁目 参加レポート

やっぱgoa流行ってるんじゃね?

f:id:tikasan0804:20170616232424p:plain

istyle.connpass.com

togetter.com

あの素晴らしいOSSのgoaの勉強会が開催されるということで、勢いでトーク枠で登壇することにしました。

スライド一覧

様々な発表がありました。あまり気にせずやってた問題点とかを気づけて面白かったです。というか、自分の口以外から、「goa」というワードが出たことが良かったですね。間違いない。

goa概要@ikawahaさん

speakerdeck.com

あーこのスライド見たことあるわーと思いながら見てました。
私がgoaにコミットするきっかけを作ってくれた人だったので、地味に1人でテンションが上がってました。これでgoaは大体何が良いか分かるので良スライドです。

OSSにコントリビュートしたら楽しかった - ぺい

アイスタイルにおけるgoa導入事例@銀シャリさん

speakerdeck.com

今回のgoa勉強会の主催の銀シャリさんの発表資料です。主催者には本当圧倒的感謝しかないですね。
内容は実際の本番でgoaが動いていますよ。パフォーマンス良いですよ。でも、こんな問題も起きてますよという非常に具体的な内容で、導入を悩んでいる人向けの資料になります。これ聞いてやっぱりgoを書けるエンジニアは少ないんだなーと辛さを感じました。

goa の改善と改良@tchsskさん

スライドURL

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さん

speakerdeck.com

goaはJSのコードを自動生成する仕組みはありましたが、内容はただリクエスト投げるぜ!という単調な動きしか出来ず、せっかく定義したバリデーションや型が使えていない問題がありました。
それらを全て解決したJSのコードを自動生成しますよというサードパーティプラグインを作成しましたというお話でした。(これ本家に入れてもいいんじゃね?と思ったり・・・)
こういったサードパーティ系のプラグインの使い方はまた、このプラグインの使い方と合わせて紹介する予定です。

goaを導入した話@k_yokomiさん

speakerdeck.com

こちらも実際の本番環境でgoaを運用しているお話でした。CIをどうやって運用しているか具体的なお話があったり、やはり、同じく運用していく上で発生した問題などがあり、それらをどうやって解決したかなどなど。こういったナレッジは共有出来るような仕組みがほしいなーと改めて思いました。

飛び込みLT design書くの面倒じゃない?@kawakenさん

www.slideshare.net

vscode goa snippets · GitHub

goaはただでさえ楽ちんにしてくれているのに、designすら楽しようという、完全に堕落しきってる素晴らしい内容でした。解決策として、スニペットを駆使すれば長いDSLもさくっと書けて楽でいいよね?というお話でした。実際ライブコーディングも素早くコードを構築していくので、これは良い!と思ったので、私も明日からそうします。

まとめ

goaの勉強会というか、言語ではないOSSの勉強会は初でしたが、とても楽しかったです。しかも結構ガッツリ技術よりな発表出来たのは個人的に大きな学びになったので、非常に価値ある時間でした。今後どこまでgoaが流行るかは分かりませんが、地味な活動を続けて、有名にしたいなーと思ったりしてます。
また、こういう機会があって、タイミングが合えば是非また参加したいですね!ありがとうございました!

Google Cloud Next 2017まとめ

f:id:tikasan0804:20170615121801j:plain

ザ・プリンス・パークタワー東京で開催されたGCPのカンファレンスに参加してきました。
個人的に費用が比較的に安価なことや学生には十分過ぎる機能が揃ってることから、私は普段からGCPを使っているのですが、実際に何が良いんだろう?というところをより具体的にしたい。知らない機能や何を目的に展開しているサービスなんだろうということを知るために参加してきました。
GCPとはGoogle Cloud Platformのことを指します

Google Cloud Next 2017とは

cloudnext.withgoogle.com

新しいアイデアに出会う。専門家からナレッジを学ぶ。同志とヴィジョンを語りあう。経営者から、IT マネージャー、技術パートナー、デベロッパー、Google エンジニアまで。ダイバーシティに富んだ知が集い、共有することで、次の「未来」が生まれていく。 そんな、クラウドのこれからをリードするイベント “Next” に、あなたも参加しませんか。 Google Cloud Platform から G Suite、マップからデバイスまで。80 を超える幅広いテーマのセッションをご用意してお待ちしています。

  • 80を越えるセッション
  • 40を越える体験セッション

上記に加えて、参加が無料というのもかなり驚愕なんですが・・・ 当日は、弁当や飲み物も会場で配布されますw

f:id:tikasan0804:20170615121418j:plain

GCPの提供する価値とは?

いきなり本題に入るのですが、Googleが言っていたGCPが提供するものは以下の4つです。

  • セキュア
  • カスタマーフレンドリー
  • インテリジェント
  • オープン

f:id:tikasan0804:20170615122631j:plain

※ちょっと見えにくい画像ですが、そこらへんの話をする時によく使ってたスライドです。

ざっくり、ひとつずつどういうことかを説明すると、以下のような感じになります。

セキュア

Googleの基盤を使うことになるので、圧倒的安心感があるのは間違いないと思います。

blog.google

通信の暗号化に力を入れるのはもちろんですが、ユーザーの認証周りにも非常に厳格な監視システムを入れていることも強くアピールしてました。

インテリジェント

高性能なソリューションが揃っている。

f:id:tikasan0804:20170615135804j:plain
様々なケースを想定したストレージやデータベース   BigQueryは使ってる人多いのでは?

f:id:tikasan0804:20170615135903j:plain PaaSやIaaSやコンテナなど

f:id:tikasan0804:20170615135922j:plain 機械学習済みの高性能API

カスタマーフレンドリー

GCPの特徴で、私がGCPをよく使う理由のひとつである。柔軟な課金システム(分単位の課金など)は、高いコストパフォーマンスを発揮してくれます。

f:id:tikasan0804:20170615214355j:plain VMのスペックを要件にあったものを提案してくれる機能があったり。

f:id:tikasan0804:20170615214402j:plain 割引とかなんやら組み合わせると、めっちゃ安くなりますよの図

f:id:tikasan0804:20170615214414j:plain
プレイド社がAWSから移行したことにより、性能とコストの両方で高いパフォーマンスを発揮したそうです。

オープン

f:id:tikasan0804:20170615214913j:plain
様々なソリューションがオープンに使える。これは利用者がこれまでは技術的に不可能だったことが可能になったり、新しいビジネスチャンスを生むことを意味しています。
今後もGoogleOSSへのコミットを続け、業界を引っ張っていくようです。これはこれまでの貢献があるGoogleだからこそ説得力がありました。

テクノロジを使った働き方改革

f:id:tikasan0804:20170615215658p:plain
今回のカンファレンスでGoogleがアピールしていたものはテクノロジも勿論ですが、働き方を変えるという取り組みについてでした。結構面白い取り組みだったので軽く紹介します。

f:id:tikasan0804:20170615215600p:plain
色々な切り口から、働き方を改革をし、実際に業務効率化をした事例がたくさん紹介されました。

ジャムボード

f:id:tikasan0804:20170615215936p:plain
ジャムボードという新しい製品のデモが行われました。遠隔に同時に操作が出来るホワイトボードで、ポストイット機能や文字認識をして、フォント化したりとかなりの高性能でかなりテンション上がる製品でした。

www.youtube.com
公式の動画が分かりやすいので貼っておきます。

チャット

f:id:tikasan0804:20170615220920p:plain
チャットはSlack的な使い方が出来るっぽいです。Botなども作成出来るので、これは使えそう・・・!Googleドキュメントなどと親和性が高いので色々な便利機能があったり、そのままビデオ会議にも繋げたりも可能なそうです。

Drive File Stream

www.itmedia.co.jp
ダウンロードせずに様々なファイルを操作可能

ここでは、紹介しきれないくらい他にも様々なサービスがありました。いま内定者開発でリモート開発をやっているので、こういうソリューションに興味湧いたりしてました。

技術系で面白かったもの

結局どんな技術あったんやっていうのも気になる方向けに面白かった技術系の紹介をします。

Cloud Spanner

cloud.google.com

f:id:tikasan0804:20170615221614j:plain

一言で言うと、リレーションがあるけど、いくらでもスケールできるデータベースらしいです。(正直意味がわからない)

f:id:tikasan0804:20170615221859j:plain スケールさせるとなると問題になるのが、データの整合性ですが、常に同期が取れるような仕様になっているそうです。何故リレーションがあるのに、そこまで高速に同期が可能になっているか?

f:id:tikasan0804:20170615222021j:plain 上の画像のような一般的なテーブルのものをSpannerでは、以下のように管理しているそうです。

f:id:tikasan0804:20170615222026j:plain うむ。なるほど。理屈はわかる。どうやってるんだ。

f:id:tikasan0804:20170615222356j:plain 基調講演でとんでもないリクエストを処理しきってるデモも行われました。

Cloud Video Intelligence

cloud.google.com

f:id:tikasan0804:20170615222731j:plain
このAPIは投げるだけで、動画にどういったものが写っているかを解析して、時間と情報をまとめて返してくれるという驚異的なものでした。

f:id:tikasan0804:20170615222949j:plain 海と検索すると、動画内に海が登場するものが抽出でき、しかも、実際に写ってる時間が分かるので見たい部分だけ見ることも出来る。

f:id:tikasan0804:20170615223053j:plain こんな感じで、細かい解析を行っており、犬の種類まで特定出来たりと、すごいとしか言えないものでした。

最近は、動画コンテンツがかなり注目を浴びてるので、検索の性能の向上や不適切な動画の摘発などにも一役買いそうな雰囲気がプンプンしました。

Data Loss Prevention API

cloud.google.com

f:id:tikasan0804:20170615223420p:plain
お客さんとのやり取りでもらった個人情報などを保存したくないけど、やり取りは残したい。そんなケース結構あると思います。このAPIを使うと消したい情報を指定して、実行するだけで勝手にマスキングしてくれます。
特に最近はリスクを減らす為、出来る限り顧客の情報を持たずにマスキングしたりする会社が多いので、かなり使えるAPIだと思います。

まとめ

APIひとつ叩くだけで、Googleの技術を使えるという贅沢な時代に生まれてよかった。

今後、追加で記事にする予定かもしれないもの

PHPのLaravel5.4でアプリケーションを作る(Migration編)

f:id:tikasan0804:20170528230547p:plain

マイグレーション

そもそもマイグレーションとは何だ?という人向けに公式のドキュメントが分かりやすく説明してくれてます。

マイグレーションとはデータベースのバージョンコントロールのようなもので、チームでアプリケーションデータベースのスキーマを簡単に更新し共有できるようにしてくれます。通常、マイグレーションはアプリケーションのデータベーススキーマを簡単に構築できるようにする、Laravelのスキーマビルダーと一緒に使用します。

データベースのバージョン管理ということです。

LaravelのSchemaファサードはテーブルの作成や操作をどのデータベースエンジンを使うかに関わらずサポートします。Laravelがサポートしているデータベースシステム全部で同じ記述法と書きやすいAPIを共用できます。

つまり、Laravelのマイグレーションの機能を使えば、様々なデータベースエンジンに対応するマイグレーションが出来上がるということのようです。 では、さっそくマイグレーションのコードを書いていきます。

f:id:tikasan0804:20170512153207p:plain

準備

github.com

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 現在のマイグレーションの状態を参照する

参考になった日本語情報

MigrationとSeederを組み合わせると便利なので、後日また記事にする予定です。

PHPのLaravel5.4でアプリケーションを作る(インストールら編)

f:id:tikasan0804:20170528230547p:plain

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に渡すところまでやりたいと思います。

f:id:tikasan0804:20170512153207p:plain

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

f:id:tikasan0804:20170528224206p:plain
上記のような結果が帰ってきてたらOKです。  

これで、さくっとアプリケーションを作れました。まだ全然触れていませんが、イケてる雰囲気を既に感じている。

GolangのgoaでAPIをデザインしよう(Model編②)

goaはいいぞ!

f:id:tikasan0804:20170505212036p:plain

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の対応例

f:id:tikasan0804:20170515171457p:plain

普通はManyToManyというと、お互いのテーブルに複数出現するような関係なので、bottlescategoriesが直接繋がっていることを想像すると思いますが、gormにおいてはbottle_categoriesを無視して、ManyToManyで、bottlescategoriesを繋げることで参照することが出来ます。

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になります。

github.com

並列処理って何が良いの

何が嬉しいのかって話をまずしますと、以下のような合計で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はいいぞ!

f:id:tikasan0804:20170505212036p:plain

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の使い方を理解していればカスタマイズはいくらでも出来ます。

github.com

さっそくgorma開発

gormaの開発には当然ですけど、RDBMSで使うDDLMySQLなどの環境構築が必要になります。なので、用意しました。最低限必要なのは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リソースに対してのアクションをしてみましょう。 f:id:tikasan0804:20170512152749p:plain f:id:tikasan0804:20170512152559p:plain f:id:tikasan0804:20170512152600p:plain

とりあえず、レスポンスが返ってきました。では、どういう動きだったかについて詳しく説明します。

やったこ

ER図
f:id:tikasan0804:20170512153959p:plain

発行した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の癖が強いというこもあり、かなり苦労しました。

f:id:tikasan0804:20170515171457p:plain

この解決方法が全然日本語情報がない上に、しかも、よく見かけるケースなので、また記事にする予定です。 今回はgormaをさくっと使うところまでということで、ここまでにしておきます。