IDL「Baal」について

はじめに

こんにちは、ソーシャルゲーム事業部 Unityエンジニアの佐藤です。
今年は体重が9kg増加するという成長ぶりでした( ・`ω・´)
もうちょっとで桁が一つ増えるところまで来てしまいました...(´・ω・`)

この記事はカヤックUnityアドベントカレンダー2016 20日目の記事となります。

今回は、社内で使っているIDLについてご説明したいと思います。

Baalとは

BaalはKAYACで用いているIDL(インターフェース定義言語)です。
Unityで作ったクライアントアプリと、サーバーが通信する際のデータ形式の定義をしたものです。
ドキュメントの生成や、データ定義に基づいたC#ファイルの生成などを行います。

Baalを導入した経緯

Baal導入以前は、サーバーサイド担当のエンジニアとクライアント担当のエンジニアでAPIの設計を話して作るというフローでしたが、

  • 設計の認識にズレが生じることがある
  • エンドポイント毎に、APIを実行するクラスやレスポンスのデータ型、パーサーを書くのが大変

といった問題が度々発生しました。

それらの問題を解決するためにBaalが導入されました。

Baalは、APIやデータの型の定義する書式から、APIの実装やデータの型クラスファイル、パーサーを自動で生成することができます。

Baalの利用例

  • データ型の定義(ステージの構成・敵の配置・アイテムの効果など)
  • APIの定義(リクエスト・レスポンス)

Baalの使い方

Baalの定義例です。 ユーザーがログインするAPIのサンプルで紹介したいと思います。

Baal定義

定義を元に、C#のソースコードを生成したいと思います。

ランタイムはat.pkgs.baalを使用します。

生成されたファイルは下記となります。

// UserAPI_generated.cs

/*
 * This file is auto-generated by program.
 * Changes to this file may lost.
 */

using System.Collections.Generic;
using Kayac;
using Kayac.Net.Http;
using Models;
using Models.Data;
using Models.Data.Response;

namespace Models.Service
{
    /// <summary>
    /// ユーザーに関するAPI
    /// </summary>
    public sealed partial class UserAPI : APIBase<UserAPI.Api>
    {
        public enum Api
        {
            Login
        }

        /// <summary>
        /// ログイン
        /// このメソッドはリクエストをキャンセルできません
        /// </summary>
        public Operation<LoginResponse> Login(
            System.String email,
            System.String password
        )
        {
            return Login(
                email,
                password,
                CancellationToken.None
            );
        }

        /// <summary>
        /// ログイン
        /// </summary>
        public Operation<LoginResponse> Login(
            System.String email,
            System.String password,
            CancellationToken cancellationToken
        )
        {
            var context = GetContext(HttpMethod.Post, "api/login", Api.Login);

            context.AddParameter("email", email);
            context.AddParameter("password", password);

            return context.GetResponseAsync<JsonResponse<LoginResponse>>(cancellationToken).
                Select(resp => resp.Result.resultObject);
        }

    }
}
// LoginResponse_generated.cs

namespace Data.Response
{
    public class LoginResponse
    {
        public System.Boolean result
        {
            get;
            private set;
        }
    }
}

このようにBaalを用いてデータ定義をクラスにしたり、API定義を自動で実装したり...ということをKAYACではやっています。

また、データ型の継承もBaal定義で行うことが出来ます。

namespace Data.Request
{
  abstract entity WithDeviceId
  {
    DeviceId: !string
  }

  entity LoginRequest
  += WithDeviceId             // ←←DeviceIdがLoginRequestクラスに追加される
  {
    Email: !string;
    Password: !string;
  }
}

生成するファイルのテンプレートをカスタムできるので、プロジェクト毎に必要な形式を用意できます。

実務での利用

  • GitHubを用いている場合は、Baal定義のプルリクエストを作り、サーバー/クライアントエンジニアでレビューする
  • Git Subtreeを用いて、サーバーとクライアント、それぞれのリポジトリに定義を反映する
  • Baal定義に問題がないか、自動テストをまわす
  • Baal定義からのソースコード生成をJenkinsのジョブにする

といったようにプロジェクト毎にBaalのワークフローを拡張して開発速度や精度をあげる取り組みを行っています。

おわりに

明日はアファトのパフォーマンスチューニングについての記事です。

サクサク動くゲームってそれだけで楽しくなりますよね! ヒントは明日の記事にあるかと思いますので、ぜひ御覧ください!!

Goでのmigrationについて ~ ddl-maker × schemalex ~

この記事はカヤックアドベントカレンダー19日目の記事です。

はじめに

こんにちは @Konboi です。

皆さん忘年してますか? 私は今のところ順調に忘年できており、今年何をしてたのか大分忘れました。

ちなみに先日行われたカヤック技術部の忘年会の様子です。

ということで表題の通り、今日の記事ではカヤックでのこれからのGoプロジェクトでのmigrationについて紹介しようと思います。

本記事であつかう migrationはDB更新を差し

  • 新規テーブルの作成
  • テーブル/カラム名の変更
  • テーブルへのカラムの追加/削除
  • テーブルへのindexの追加/削除

などがあげられます。

TL;TR

migrationの方法

migrationの方法については大きく分けて3つあると思います (細かい所ではもっと分かれるとは思います。 異論は認めます)

  1. 更新分をSQLファイルで追加していく方法
  2. Rails like な DSLを記述し差分をファイルとして追加していく方法
  3. 現在のDBの状況と定義ファイルの差分を適用する方法

更新分をSQLファイルで追加していく方法

  • ベースとなるSQLファイル (サービスリリース時点のSQL) が存在する
  • DB更新時には更新分のSQLを作成し、 ベースファイルに更新を適用する
  • 本番ヘは、更新分のSQLのみ適用する
  • 新しく入ってきたメンバーはベースファイルを適用し、更新分を範囲していく

メリット

  • 更新分がわかりやすい
  • 書かれたSQLが適用されるのでレビューがし易い
  • migration実行時に別途ツールが必要ない

デメリット

  • 管理がベースファイルと新規ファイル両方に適用しないと行けないので煩雑

※ 個人の意見です

rails likeなmigration方式

  • 基本的な方針はSQLを追加してく方式と同じ
  • SQLファイルを追加していくのではなくDSLで定義されたファイルを追記していく
  • 定義ファイルを作成/編集 (rails g migration <Name>)
  • 定義ファイルを反映 (rails db:migrate)

メリット

  • アプリケーション開発と同じ言語で書ける
  • 反映分を切り戻す戻すことも可能 (rails db:rollback)

デメリット

  • DSLを覚える必要がある
  • 複数人で開発しているとコンフリクトする
  • どのようなSQLが吐かれるかをきちんと確認する必要がある

※ 個人の意見です

現在のDBの状況と定義ファイルの差分を適用する方法

カヤックのゲーム事業部ではこの方法でmigrationを行っています。

GitDDLというツールを使用しています。

  • その名の通りgitで管理されていることが前提
  • 反映時のコミットハッシュをdatabase上のテーブルで管理
  • 現在のschemaファイルとdbに保存されているコミットハッシュ時のschemaの状態から差分を生成
  • 差分を適用し、DBに保存されているコミットハッシュを更新

GitDDL以外で似たようなツールには Anego があげらると思います。

Perl以外ではridgepole などがあります。

メリット

  • 管理がし易い
  • コンフリクトが起きにくい

デメリット

  • 差分のSQLを確認する必要がある
  • 定義ファイルを何かしらの方法で管理する必要がある
  • どのようなSQLが吐かれるかをきちんと確認する必要がある

Goでのmigrationの話

長々と既存のmigrationの話をしましたが、Goの話をします。

Go製マイグレーションツールの現状確認Githubで調べてみるとGoではrails like なDBのmigrationが多いように見受けられます。

しかし、僕たちはGitDDLを使った運用が長くその形での運用ノウハウが多く溜まっているので、GoのプロジェクトでもGitDDLの様な差分を元にmigrationを実行したいと考えていました。

そこで目をつけたのが schemalex です

github.com

schemalexはREADME.mdを見てもらえるとわかりますが、 SQL::Translator::Diff のように2つのSchemaの差分からDiffを生成してくれるツールです。

ある程度複雑な差分を与えても問題なく動いています。

GoのstructからSQLを自動生成する

既存のPerlプロジェクトではGitDDLで使用するSchemaファイルを自動生成*1しています

Goのプロジェクトでも適用するSQLファイルを自動生成したい要望があがりました。

そこで作ったのが ddl-maker です。

github.com

ddl-makerはGoのstructからSQLを生成してくれます。 詳しい使い方は README.md や example をみていただければと思いますが簡単な概要を

type User struct {
    Id        uint64
    Name      string
    CreatedAt time.Time
    UpdatedAt time.Time
}

func (u User) PrimaryKey() dialect.PrimaryKey {
    return mysql.AddPrimaryKey("id")
}

このような struct を ddl-maker を介すと以下の様なSQLを生成することができます

SET foreign_key_checks=0;

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
    `id` BIGINT unsigned NOT NULL,
    `name` VARCHAR(191) NOT NULL,
    `created_at` DATETIME NOT NULL,
    `updated_at` DATETIME NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;

SET foreign_key_checks=1;

以下の様なメソッドを定義してあげることでテーブル名の指定しての生成ややインデックスやプライマリーキーも生成することができます。

tagではなく、メソッドにしているのはPrimaryKeyなどはパーティションを切るときなどきちんと順番を指定したい場合があるためです

func (u User) Table() string {
    return "player"
}

func (u User) Indexes() dialect.Indexes {
    return dialect.Indexes{
        mysql.AddIndex("name_idx", "name"),
    }
}
CREATE TABLE `player` (
    `id` BIGINT unsigned NOT NULL,
    `name` VARCHAR(191) NOT NULL,
    `created_at` DATETIME NOT NULL,
    `updated_at` DATETIME NOT NULL,
    INDEX `name_idx` (`name`),
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;

これで、Goの開発においては

  1. struct を定義する
  2. ddl-makerでeructからSQLを生成
  3. schemalexでDBに反映

という流れができました。

最後に

各社migrationについて色々と思うところはあると思います。 今回の記事が少しでも参考にされば幸いです。

明日は 今年のISUCON6で準優勝したmorimoto組組長こと@jinnです

きっと凄いのがくると思います!! お楽しみに!!!

おまけ

カヤックでは美味しいビールと美味しいお肉で忘年したいと人を募集しています

入社はしないけど、ビールとお肉片手にカヤック社員とGo談義したい人も募集しております。 話したい/飲みたいというかたは @Konboi まで気軽にmention下さい!

みんなのGo言語【現場で使える実践テクニック】

みんなのGo言語【現場で使える実践テクニック】

*1:DBIx::Class + GitDDLでなんかそれっぽいSQL出す。 http://hisaichi5518.hatenablog.jp/entry/2013/08/12/182356