コミット毎に実行環境をビルドするoasisを書いた

Go

Dockerの理解を深めるため、またGo言語の経験を積むためにoasisというツールを書いた。「とりあえず動いた」レベルの完成度であり、実用で使うにはもっと時間をかけて改善していく必要がある。

これはコミット毎の実行環境をdockerのコンテナとして提供するリバースプロキシだ。例えば、以下のようにoasisを起動する。

% oasis start \
    --proxy master.oasis.local:8080 \
    --container-host 192.168.99.100 \
    --repository github.com/naoty/sample_rails_app

このときhttp://master.oasis.local:8080にアクセスすると、oasisは以下のようなことを行う。

  1. --repositoryで指定されたリポジトリgit cloneする。
  2. サブドメインで指定されたリビジョン、ここではmastergit checkoutする。
  3. リポジトリに含まれるDockerfileを使ってdocker buildする。
  4. ビルドしたイメージをdocker run -P -dして、コンテナを起動する。
  5. コンテナのホスト側ポート(例: 49154)を調べて、oasisへのアクセスを--container-hostで指定されたホスト上のコンテナ(例: 192.168.99.100:49154)にリダイレクトする。

f:id:naoty_k:20150409225939p:plain

実際にOSXで試す場合は、--proxy 127.0.0.1:8080のようなオプションで起動して、サブドメインの解決をPowに任せるといいと思う。

% cd ~/.pow
% echo 8080 > oasis

上のようにすると、http://*.oasis.devのように任意のサブドメインにアクセスできるようになり、8080ポートのoasisにポートフォワーディングされる。

所感

もともとは同僚の方が開発に携わっているmookjp/poolを見て、もうちょっとシンプルにセットアップできるようにしたいと思ったのがきっかけだった。実行ファイルをダウンロードして即実行できるようなものが理想だったので、Go言語を勉強しはじめこんなものを作ってみた。名前の「oasis」は最近ハマっているドミニオン・異郷に出てくるアクションカードであること、コンセプトのオリジナル実装であるpoolに雰囲気が似ていることから採った。

Go言語はとてもシンプルですんなり理解できたし、標準パッケージでリバースプロキシを簡単に実装できたため、短時間でここまで作ることができた。ちょっとしたツールを作るとき、これまではRubyでrubygemを書くようなことをしていたが、Go言語であればrubygemを書くほどのハードルの高さもなく、シンプルで生産性の高いコードを書いてそのまま配布することができていい感じだなと思った。

また、DockerについてもDocker Remote APIを触ってみたり、docker run-p-Pの違いを理解できたり、理解が深まったと思う。あんまり関係ないけど、サンプルアプリで使ったDockerfileはDocker Hubで配布された公式のrails用イメージを使ってるだけで、何も考えなくてよくて便利だった。

ghqを読んだ

Go

Goの勉強のため、普段からお世話になっているmotemen/ghqを読むことにした。なお、現在の僕のGoの知識はgotourを完走した程度だ。最初から現在のコミットを追いかけるのは骨が折れそうだったので、最初のコミットbad21c7df65ccefd74530d6fcc5f0707b63e0266から読むことにした。

Goのプログラムはmainパッケージのmain()から実行されるため、main.gomain()から読む。

import {
    // ...

    "github.com/codegangsta/cli"
}

func main() {
    app := cli.NewApp()
    app.Name = "ghq"
    app.Usage = "Manage GitHub repository clones"
    app.Version = "0.1.0"
    app.Author = "motemen"
    app.Email = "motemen@gmail.com"
    app.Commands = []cli.Command{
        {
            Name: "get",
            Usage: "Clone/sync with a remote repository",
            Action: CommandGet,
        },
        {
            Name: "list",
            Usage: "List local repositories",
            Action: CommandList,
            Flags: []cli.Flag{
                cli.BoolFlag{"exact, e", "Exact match"}
            }
        }
    }

    app.Run(os.Args)
}
  • cliパッケージはcodegangsta/cliというコマンドを簡単に作成するライブラリのもののようだ。
  • cli.NewApp()*cli.App(構造体Appのポインタ)を返している。この構造体はCLIアプリケーションを表している。これに続くコードはそのCLIアプリケーションの情報を設定している。
  • app.Commandsというフィールドにはcli.Command型のスライスが入る。cli.Command型はCLIアプリケーションのサブコマンドを定義するために使われる。サブコマンドの名前、ドキュメント、フラグなどを設定し実際に実行される関数を指定することができる。実行される関数はActionというフィールドに指定する。このフィールドはfunc(context *Context)という型になっている。ここでは、getlistというサブコマンドが定義されており、それぞれCommandGet, CommandListという関数が実行されるように設定されている。
  • 最後にapp.Run()コマンドライン引数を受け取ってCLIアプリケーションを実行している。

とりあえずgetサブコマンドを理解したいので、CommandGetを見ていく。

func CommandGet(c *cli.Context) {
    argUrl := c.Args().Get(0)

    if argUrl == "" {
        cli.ShowCommandHelp(c, "get")
        os.Exit(1)
    }

    // ...
}
  • 上述の通り、Command.Actionfunc(context *Context)という型なので、CommandGet関数もそれに従っている。
  • cli.Context.Args()cli.Args型を返すが、これはtype Args []stringと定義されており、実体はstringのスライスだ。Args.Get(n int)nがスライスのサイズより大きかった場合に空文字を返すようになっている。
  • ShowCommandHelpContextポインタとサブコマンドを表す文字列を渡すことで、そのサブコマンドのヘルプメッセージを出力する。
  • 第1引数をURLとして取得し、それが空であればヘルプメッセージを表示するようになっている。
func CommandGet(c *cli.Context) {
    // ...

    u, err := ParseGithubURL(argUrl)
    if err != nil {
        log.Fatalf("While parsing URL: %s", err)
    }

    path := pathForRepository(u)
    if err != nil {
        log.Fatalf("Could not obtain path for repository %s: %s", u, err)
    }

    // ...
}
  • ParseGithubURL()pathForRepository()いう関数についてはあとで見ていくことにする。
  • エラーがあった場合、log.Fatalf関数でエラーメッセージを表示するものと思われる。logパッケージはGoの標準パッケージで、log.Fatalf関数はエラーメッセージを表示するだけでなくexit(1)によってプログラムを異常終了させる。
func CommandGet(c *cli.Context) {
    // ...

    newPath := false

    _, err := os.Stat(path)
    if err != nil {
        if os.IsNotExist(err) {
            newPath = true
            err = nil
        }
        mustBeOkay(err)
    }

    // ...
}
  • os.Stat関数はファイルの存在をチェックする際によく用いられるようだ。この関数は指定したパスにあるファイルの情報を表すFileInfo構造体と、エラー時にはエラーを返す。
  • os.IsNotExist()関数も存在チェックを行うように見える。ファイルの存在をチェックするGoの実装は諸説あるようだ。
  • mustBeOkay()関数は定義を見てみると、引数の渡したエラーが存在すればエラーメッセージを表示して異常終了させるようだ。アサーションのような役割を果たしているようだ。
  • ファイルパスが存在しない場合はnewPathtrueになる。また、errnilになるため、mustBeOkay()で異常終了は起きなくなる。
func CommandGet(c *cli.Context) {
    // ...

    if newPath {
        dir, _ := filepath.Split(path)
        mustBeOkay(os.MkdirAll(dir, 0755))
        Git("clone", u.String(), path)
    } else {
        mustBeOkay(os.Chdir(path))
        Git("remote", "update")
    }
}
  • filepath.Split()は与えられたパスをディレクトリとファイル名に分け、ディレクトリ、ファイル名の順に返す。
  • Git()関数はあとで詳しく見る。
  • newPathtrueになるのは上述の通りpathが存在しない場合で、このときはgit cloneが行われ、そうでなければgit remote updateが行われるようだ。

ghq getコマンドの全体像についておおまかに理解できたので、飛ばした関数について1つずつ読んでいく。

type GitHubURL struct {
    *url.URL
    User string
    Repo string
}

func ParseGitHubURL(urlString string) (*GitHubURL, error) {
    u, err := url.Parse(urlString)
    if err != nil {
        return nil, err
    }

    if !u.IsAbs() {
        u.Scheme = "https"
        u.Host = "github.com"
        if u.Path[0] != '/' {
            u.Path = '/' + u.Path
        }
    }

    if u.Host != "github.com" {
        return nil, fmt.Errorf("URL is not of github.com: %s", u)
    }

    components := strings.Split(u.Path, "/")
    if len(components) < 3 {
        return nil, fmt.Errorf("URL does not contain user and repo: %s %v", u, components)
    }
    user, repo := components[1], components[2]

    return &GitHubURL{u, user, repo}, nil
}
  • url.Parse()は与えられた文字列をパースしてURL構造体のポインタと失敗した場合はerrorを返す。
  • URL構造体はSchemeHostといったフィールドを持っているため、相対パスであればこれらを設定している。
  • fmt.Errorf()はフォーマット化された文字列からエラー値を返す。
  • strings.Split()は文字列を第2引数で渡されたセパレータで分解しstringのスライスとして返す。

続いてpathForRepository()関数を読んでいく。

func reposRoot() string {
    reposRoot, err := GitConfig("ghq.root")
    mustBeOkay(err)

    if reposRoot == "" {
        usr, err := user.Current()
        mustBeOkay(err)

        reposRoot = path.Join(usr.HomeDir, ".ghq", "repos")
    }

    return reposRoot
}

func pathForRepository(u *GitHubURL) string {
    return path.Join(reposRoot(), "@"+u.User, u.Repo)
}
  • path.Joinはパスの要素を/で結合してパスにする。
  • GitConfig()は後ほど読んでいく。おそらくリポジトリのルートパスを返すものと思われる。
  • reposRootが空であれば$HOME/.ghq/reposを返すようになっている。user.Current()はカレントユーザーを表すUser構造体のポインタを返す。User構造体はユーザー名やホームディレクトリなどの情報を持っている。usr.HomeDirでホームディレクトリを取得している。

続いてGit()関数を読んでいく。

func Git(command ...string) {
    log.Printf("Running 'git %s'\n", strings.Join(command, " "))
    cmd := exec.Command("git", command...)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    err := cmd.Run()
    if err != nil {
        log.Fatalf("git %s: %s", strings.Join(command, " "), err)
    }
}
  • ...stringのように引数の型名の前に...をつけると可変長引数をとることができる。この引数の型は型名で指定した型のスライスとなる。つまりここではstringのスライスとなる。
  • fmt.Printf()関数は標準出力に出力するものだが、log.Printfはロガーで指定された出力先に出力する点が異なる。
  • exec.Command()関数は、第1引数で指定された名前のコマンドを渡された可変長引数で実行するコマンドを表すCmd構造体のポインタを返す。
  • ...で渡された可変長引数は上述の通りスライスなのだけど、スライスを展開して可変長引数として関数に渡す場合はcommand...のようにスライスのあとに...とつける。
  • cmd.Runで指定されたコマンドを実行する。

続いてGitConfig()関数を読んでいく。

func GitConfig(key string) (string, error) {
    defaultValue := ""

    cmd := exec.Command("git", "config", "--path", "--null", "--get", key)
    cmd.Stderr = os.Stderr

    buf, err := cmd.Output()

    if exitError, ok := err.(*exec.ExitError); ok {
        if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); ok {
            if waitStatus.ExitStatus() == 1 {
                return defaultValue, nil
            } else {
                return "", err
            }
        } else {
            return "", err
        }
    }

    return strings.TrimRight(string(buf), "\000"), nil
}
  • cmd.Output()関数はコマンドを実行して標準出力を返す。
  • err.(*exec.ExitError)というのは型アサーションという文法だそうだ。errerrorインターフェイス型で、これが*exec.ExitError型の値であると断定する。変換された値が第1返り値、変換に成功したかどうかが第2返り値になる。
  • ここらへんでやっていることは終了ステータスを取得しようとしている。
  • strings.TrimRight()関数は第2引数を削除したstringスライスを返す。

Railsにコントリビュートした

軽い気持ちでpull requestを送ってみたら数十分後になんとmergeされてしまった。間違ってmasterブランチに送ってしまったため、おそらくrails 5で公開されることになる。

追加したのはrake initializerという簡単なrakeタスクで、railsの起動時に実行されるinitializerを実行順に出力する。

% rake initializer
set_load_path
set_load_path
set_load_path
set_load_path
set_autoload_paths
set_autoload_paths
set_autoload_paths
set_autoload_paths
add_routing_paths
add_routing_paths
add_routing_paths
add_routing_paths
add_locales
add_locales
add_locales
add_locales
add_view_paths
add_view_paths
add_view_paths
add_view_paths
load_environment_config
load_environment_config
load_environment_config
load_environment_config
load_environment_hook
load_active_support
set_eager_load
initialize_logger
initialize_cache
initialize_dependency_mechanism
bootstrap_hook
active_support.deprecation_behavior
prepend_helpers_path
prepend_helpers_path
prepend_helpers_path
prepend_helpers_path
load_config_initializers
load_config_initializers
load_config_initializers
load_config_initializers
active_support.halt_callback_chains_on_return_false
active_support.initialize_time_zone
active_support.initialize_beginning_of_week
active_support.set_configs
action_dispatch.configure
active_model.secure_password
action_view.embed_authenticity_token_in_remote_forms
action_view.logger
action_view.set_configs
action_view.caching
action_view.collection_caching
action_view.setup_action_pack
action_controller.assets_config
action_controller.set_helpers_path
action_controller.parameters_config
action_controller.set_configs
action_controller.compile_config_methods
active_record.initialize_timezone
active_record.logger
active_record.migration_error
active_record.check_schema_cache_dump
active_record.set_configs
brancher.rename_database
active_record.initialize_database
active_record.log_runtime
active_record.set_reloader_hooks
active_record.add_watchable_files
global_id
active_job.logger
active_job.set_configs
action_mailer.logger
action_mailer.set_configs
action_mailer.compile_config_methods
setup_sass
setup_compression
jbuilder
web_console.initialize
web_console.insert_middleware
web_console.templates_path
web_console.whitelisted_ips
web_console.whiny_requests
web_console.acceptable_content_types
engines_blank_point
engines_blank_point
engines_blank_point
engines_blank_point
append_assets_path
append_assets_path
append_assets_path
turbolinks
append_assets_path
add_generator_templates
ensure_autoload_once_paths_as_subset
add_builtin_route
build_middleware_stack
define_main_app_helper
add_to_prepare_blocks
run_prepare_callbacks
eager_load!
finisher_hook
set_routes_reloader_hook
set_clear_dependencies_hook
disable_dependency_loading

もともとは、naoty/brancherというrubygemを作るときに、railsの初期化プロセスにコードを差し込みたくてinitializerが実行される順番を確認するデバッグ用のコードを書いていたのがきっかけだった。似たようなrakeタスクにrake middlewareというものがあり、これがあるならrake initializerもあっていいだろうという軽い気持ちでpull requestを送ってみた。

意外とすんなりmergeしてもらったので、railsへのコントリビュートに対して心理的なハードルがかなり低くなった。上の出力を見てもらえればわかるとおり同じようなinitializerが実行されており、起動が遅くなったり、initializerで定数を定義したときに大量のwarningが出たりする。前々から定数のwarningが大量に出る問題は不思議に思っていたが、これが原因なのかもしれない。またコントリビュートするとしたら、ここらへんを解決するところになりそう。

Rubotyで勤務時間を管理する

bot

リモートワークのときでも勤務時間を自動的に記録するため、botに発言時間を監視させ、だいたいの勤務時間を記録させるようにした。

f:id:naoty_k:20150313220454p:plain

いま使っているbotr7kamura/ruboty製で、これに機能を追加するプラグインを書いた。

このプラグインは発言者ごとに発言時間を記録する。

このプラグインは勤務時間のストレージを提供する。既存のストレージにはr7kamura/ruboty-redisがある。これを利用する場合、保存された勤務時間を取得するコマンドを用意する必要がある。それを非エンジニアに理解してもらうのは厳しいと思ったので、直感的に理解できるGoogle Spreadsheetをストレージとして利用できるようにするプラグインを作った。

ソースコードを読むとわかるけど、この2つのプラグインは密結合しているため、設計上はいい出来とは言えない。ruboty-timecard内でruboty-google_spreadsheetを使うことを想定したアクセスの仕方をしている。rubotyのストレージをうまく抽象化するインターフェイスがあると解決しそうだが、なかなか難しい問題だと思う。

これら2つのプラグインは共に使われることを想定しているため、勤務時間を記録する用のテンプレートを作った。Herokuボタンから簡単にデプロイできる。

Timepieceを0.2.0にアップデートした

Swiftの日付操作ライブラリであるTimepieceに機能を追加し0.2.0にアップデートした。また、1ヶ月くらい前からしれっとCocoaPods(現在はまだrc版)をサポートしたのでCocoaPodsからインストールできるようになってる。

NSDate <-> Stringの変換

let timestamp = 3.years.ago.stringFromFormat("yyyy-MM-dd")
XCTAssertEqual(timestamp, "2012-03-01", "")

if let birthday = "1987-06-02".dateFromFormat("yyyy-MM-dd") {
    XCTAssertEqual(birthday.year, 1987, "")
    XCTAssertEqual(birthday.month, 6, "")
    XCTAssertEqual(birthday.day, 2, "")
}

NSDateFormatterをいちいち初期化するのが面倒だったので、これを内部的に呼び出す直感的なメソッドを追加した。フォーマット文字列は同じ。

オーバーロードの追加

Before: f:id:naoty_k:20150301145950p:plain

After: f:id:naoty_k:20150301150005p:plain

これまではNSDate.date(year:month:day:hour:minute:second:)hour, minute, secondのデフォルト引数を0にセットしていたが、これではBeforeのスクショの通り補完候補にhour, minute, secondも含まれてしまう。デフォルト引数を使うよりも引数を省略したオーバーロードを提供した方が、補完候補に省略版のメソッドが現れるため使いやすいと思う。

今後の予定

  • NSDate同士を比較演算子で比較できるようにしたい。
  • 1.5.hourのようなFloatのサポート(けっこう難しそう)

「UMLモデリングの本質」を読んだ

UMLモデリングの本質 第2版

UMLモデリングの本質 第2版

UMLモデリングの本質」という本を読んだ。最近、ソフトウェアの設計について興味があって、いろいろ調べてみたところ各所でこの本がオススメされていたので手にとってみた。以前のエントリー(「達人に学ぶDB設計徹底指南書」を読んだ - naoty.to_s)でデータベースの設計について理解できたんだけど、結局のところ、そのシステムが扱う業務内容をいかにして実装可能なモデルに落とし込むか(=モデリング)が重要になってくると思う。この本で理解したかったのはそこだった。タイトルからUMLの書き方についての本のようにも思えるが、そうではなく、むしろまったくUMLの書き方は書いてない。UMLを使って、複雑な業務用件をいかにモデリングするかを説いている。本書は300ページに満たないものの、密度が非常にあり2, 3週間でゆっくり読んでも第3章までしか読めていない。それでも、十分に学びがあったので忘れてしまう前にエントリーとして残しておきたいと思う。

モデリングとは

モデリングとは「仕組みや概念を理解するために概念的な要素とそれらの関係を記述すること」とあった。これは普段の業務で多かれ少なかれ必ず行っていると思う。ただ、それをモデリングという工程として認識してはいなかった。そして、モデリングの手法についても特に考えることはなかった。いつも漫然とノートに四角形と線を書いて整理してた(以下、イメージ)。

f:id:naoty_k:20150228013509j:plain:h400

こういう自己流ではなくて、標準的な概念の表記法がある。その1つがUMLだ。UMLを習得することで、他人とのコミュニケーションの手段として使えるようになるし、UMLを通して標準的なモデリングの手法も学べるようになる。

本書によると、モデリングは大まかに以下の順番に行っていくようだ。

  1. 業務フロー図を書いて業務フローを整理する。
  2. 業務フローからユースケース図を書いてユースケースを洗い出す。
  3. ユースケースから概念となる名詞を抜き出し、初期の型図を作成する。型図というのは実際はクラス図のことで、クラスと言っちゃうとクラスとして実装されることを含意してしまうが、実際にはクラスとは限らないため型図という言い方をしている。
  4. ユースケースごとにシーケンス図を書く。これによって各アクターの責務が明確になり、初期の型図を機能的側面から修正することができる。

UMLはけっこうな種類があるような気がするけど、とりあえず上に出てきた図だけ覚えておけばよさそうだ。各ステップの具体的な方法については本書を参照してほしい。読んだだけではあまり意味がないと思うので、近いうちに簡単な例で実践してみたいと思っている。

分類の実装

モデリングの手法の次はより実装に近い話が続く。その中でも分類の実装の話がよかった。例えば、ユーザーに「有料会員」と「無料会員」という分類がある場合に、それをフラグとして実装するのか「Stateクラス」として実装するのかという議論がある。

# フラグで実装

class User < ActiveRecord::Base
  enum plan: %i(free premium)
end
# Stateクラスで実装

class User < ActiveRecord::Base
  has_one :plan
end

class Plan < ActiveRecord::Base
end

class Free < Plan
end

class Premium < Plan
end

個人的にはStateクラスという手法を知らなかったので、いつも前者のフラグとして実装していた。この実装の問題点は、状態が増えた場合にコードを修正する必要がある点と、if文の分岐を多用することになりコードが複雑になってしまう点がある。特に後者は、状態フラグの種類(例えば、「公開アカウント」or「非公開アカウント」)が増えたときに組み合わせが倍になり指数関数的に複雑になってしまうため、重大な問題点だと思う。

Stateクラスであれば、サブクラスを追加するだけでよく既存のコードを修正する必要がない。if文による分岐もダックタイピングによって解決する。状態フラグが複数になった場合は、直交する状態をサブクラスとして定義する(PublicFree, PrivatePremiumなど)のがよいと本書では書かれていた。状態によって振る舞いが異なる場合や状態が増える可能性がある場合は、フラグではなくStateクラスで実装する方がいいのかもしれない。

ファサード

モデリングによって整理された概念をクラスとして実装する際、層別化アーキテクチャを使うのがいいという話が出てくる。よく知られた4層モデルの話で、システムを「ユーザーインターフェイス層」「アプリケーション層」「ドメイン層」「永続層」に分離し、層間の参照を一方向にすることで結合度を抑えるというアプローチだ。このとき、モデリングによって整理された概念はそのままドメイン層に配置することになる。そして、アプリケーション層には業務フローで定義された手続きや処理の手順を定義していくことになる。

アプリケーション層を実装していくなかで、ドメイン層に定義された概念が高度に抽象化されていて扱いづらいときがある。とは言え、これまでのモデリングで練り上げてきた概念をアプリケーション層の都合でねじまげるわけにはいかないだろう。そこで登場するのがファサードというオブジェクトだ。ファサードはアプリケーション層からは冗長に見えるドメインを1つにまとめて扱いやすくする。さらに、複数ドメインにまたがった排他制御も扱う。

ファサードという概念はなんとなく耳にしたことがあったが、モデリング→実装という流れの文脈で捉えるとその必要性を実感することができた。普段RailsMVCだけを書いていると、こうした視点が持てずにドメインをねじ曲げてしまうことがある。なので、ファサードの実装方法について調べて実践していきたいと思った。

「達人に学ぶDB設計徹底指南書」を読んだ

達人に学ぶDB設計 徹底指南書 初級者で終わりたくないあなたへ

達人に学ぶDB設計 徹底指南書 初級者で終わりたくないあなたへ

「達人に学ぶDB設計徹底指南書」を読んだ。最近、Webアプリケーションを開発する中で設計に問題意識を感じるようになった。ただ、具体的にどこが問題なのかうまく言語化できなくて、というか設計という言葉が何を指しているのかイマイチ分かってなかったので、データベースの設計について本を読んでみた。

内容

  • データベースの設計は「論理設計」→「物理設計」の順に行う。論理設計はデータの要素やデータ間の関係を定める工程を指す。物理設計はデータを格納する物理的な領域や格納方法を定める工程を指す。
  • 論理設計は「エンティティの抽出」→「エンティティの定義」→「正規化」→「ER図の作成」の順に行う。エンティティというのは業務内容に現れるデータの集合体を指す。
  • 物理設計は「テーブル定義」→「インデックス定義」→「ハードウェアのサイジング」→「ストレージの冗長構成決定」→「ファイルの物理配置決定」の順に行う。
  • 正規化というのはデータの形式を正規形にすることであり、正規形というのはデータの冗長性が排除され一貫性と効率性が保持されるデータ形式のこと。異なるエンティティを異なるテーブルに分離することによって第3正規形までは実現できる。
  • 正規化の目的はデータの冗長性を排除して不整合な更新を避けることにある。ただし、正規化はテーブルを分離するため、SQLでjoinを使うことになる。joinはパフォーマンスが悪化するため、正規化とパフォーマンスはトレードオフの関係にある。
  • 「非正規化は最後の手段」というのが筆者の意見である。

感想

  • データベース設計で最初に行うのは「エンティティの抽出」であるということがとても重要なポイントに感じた。
  • いつもRailsでアプリケーションを開発するとき、正規化を意識することはない。Modelを定義することはテーブルを定義することと同義だ。なので、適切にエンティティを定義できていれば、意識せずとも第2正規形、第3正規形を実現できると思う。筆者の「非正規化は最後の手段」という立場にたつと、いつもどおりModelを定義することで正規化を行い、非正規化以外の手段でパフォーマンスを向上させていけばいいということになる。つまり、今まで通りでいいってことか。
  • 結局、「いかにしてエンティティを抽出・定義するか」という問題を考える必要がありそうだ。