旅行しながら働く ムラサメ研究所社長ブログ

主にゲームやプログラミングのログ

Elixir Phoenix 試食

はじめに

ここ1か月にたくさんの言語仕事で触っている
PHP(なぜか今まで触ったことなかった)、GolangPython、Elixir、RailsScala、node(5年ぶり2回目) など
1か月程度なので知見がほとんどないけど、知識をまとめていく

作るもの

APIサーバのサンプル(評価)プロジェクト
Redis、MySQL、MongoDBなどのCRUDテスト
言語の評価
Docker構築

Docker構築

最初は OSから自分で入れた
とりあえずUbuntu

FROM ubuntu:17.04

RUN apt-get update

RUN apt-get install -y sudo wget
RUN apt-get install -y language-pack-ja
RUN wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && dpkg -i erlang-solutions_1.0_all.deb
RUN apt-get update
RUN apt-get install -y esl-erlang
RUN apt-get install -y elixir

ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8

# Set app env
ENV HOME /root

WORKDIR /usr/local/src
RUN apt-get install -y libssl-dev ncurses-dev
RUN apt-get install -y sudo wget git tar bzip2 incron vim nodejs npm
RUN apt-get install -y inotify-tools


RUN yes | mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez


#RUN mkdir /runnet
#WORKDIR /runnet

ENV PHOENIX_APP_NAME runnet
ENV PHOENIX_APP_PORT 4000
ENV PHOENIX_APP_ROOT /${PHOENIX_APP_NAME}

WORKDIR /$PHOENIX_APP_ROOT

EXPOSE ${PHOENIX_APP_PORT}
COPY . $PHOENIX_APP_ROOT
WORKDIR /runnet/$PHOENIX_APP_ROOT

# Compile phoenix(FOR dev)
RUN yes | mix local.hex && yes | mix local.rebar && mix do deps.get, compile

RUN mix deps.get
RUN mix ecto.create
RUN mix ecto.migrate

# npm install
#RUN npm install
#RUN npm install -g brunch

# Run Phoenix
CMD ["/bin/bash", "-c", "mix phoenix.server"]

色々無駄なものがあるけど、とりあえず Erlang、Elixir、Phoenix と必要なものをセットアップ
結構めんどくさいね。イメージも大きいし
時間あったら Alpineにしようと思っていた

docker-composeも下記のかんじで

version: '2'
services:
  app:
    build: .
    environment:
      MYSQL_ROOT_USERNAME: 'root'
      MYSQL_ROOT_PASSWORD: 'pass'
      MYSQL_HOSTNAME: 'mysql'
      MYSQL_PORT: '3306'
    ports:
      - '4000:4000'
    volumes:
      - .:/runnet
    links:
      - mysql
  mysql:
    image: mysql:5.7.10
    environment:
      MYSQL_ROOT_PASSWORD: 'pass'
    ports:
      - '3306:3306'
    volumes:
      - mysql-data:/var/lib/mysql
volumes:
  mysql-data:
    driver: local

PhoenixはデフォがPostgresだけど MySQLを入れた

router

  scope "/", Runnet do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    resources "/users", UserController
  end

controller

defmodule Runnet.UserController do
  use Runnet.Web, :controller

  alias Runnet.User

  def index(conn, _params) do
    users = Repo.all(User)
    render(conn, "index.html", users: users)
  end

  def new(conn, _params) do
    changeset = User.changeset(%User{})
    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"user" => user_params}) do
    changeset = User.changeset(%User{}, user_params)

    case Repo.insert(changeset) do
      {:ok, _user} ->
        conn
        |> put_flash(:info, "User created successfully.")
        |> redirect(to: user_path(conn, :index))
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def show(conn, %{"id" => id}) do
    user = Repo.get!(User, id)
    render(conn, "show.html", user: user)
  end

  def edit(conn, %{"id" => id}) do
    user = Repo.get!(User, id)
    changeset = User.changeset(user)
    render(conn, "edit.html", user: user, changeset: changeset)
  end

  def update(conn, %{"id" => id, "user" => user_params}) do
    user = Repo.get!(User, id)
    changeset = User.changeset(user, user_params)

    case Repo.update(changeset) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "User updated successfully.")
        |> redirect(to: user_path(conn, :show, user))
      {:error, changeset} ->
        render(conn, "edit.html", user: user, changeset: changeset)
    end
  end

  def delete(conn, %{"id" => id}) do
    user = Repo.get!(User, id)

    # Here we use delete! (with a bang) because we expect
    # it to always work (and if it does not, it will raise).
    Repo.delete!(user)

    conn
    |> put_flash(:info, "User deleted successfully.")
    |> redirect(to: user_path(conn, :index))
  end
end

model

defmodule Runnet.User do
  use Runnet.Web, :model

  schema "users" do
    field :name, :string
    field :email, :string
    field :encrypted_password, :string

    timestamps()
  end

  @doc """
  Builds a changeset based on the `struct` and `params`.
  """
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:name, :email, :encrypted_password])
    |> validate_required([:name, :email, :encrypted_password])
  end
end

migrateファイル

defmodule Runnet.Repo.Migrations.CreateUser do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :name, :string
      add :email, :string
      add :encrypted_password, :string

      timestamps()
    end

  end
end

ユーザ名、メールアドレス、パスワードを保存する、よくあるログイン認証まわり
細かく追ってないけど Routerでresources を指定すると、複数のメソッドに対応してくれるっぽい
そのあたりが自動的に controllerの index、new、create、show、update、delete等に対応される

MySQLとの連携もばっちりだし、そこまで言語的にも覚えにくくないと思う

Postgresで再構築

いちおうPostgresにて構築してみる。Redisも使う
同じようにDockerイメージ作る

docker-compose.yml

version: '3'
services:
  db:
    image: postgres
    ports:
      - 5432:5432
  redis:
    image: redis:latest
    ports:
      - 6379:6379
  web:
    build: .
    container_name: "ages_app"
    volumes:
      - $PWD:/ages
    ports:
      - "4000:4000"
    links:
      - db
      - redis
    environment:
      DATABASE_USER: postgres
      DATABASE_PASSWORD:
      DATABASE_PORT: 5432
      DATABASE_HOST: db
      REDIS_URL: redis://redis:6379
    depends_on:
      - db
      - redis

今回はゲームサンプルを見据えて DB定義もそれっぽくする

router

  scope "/", AgesApp do
    pipe_through :browser # Use the default browser stack

    get "/users", UserController, :index
    get "/user/:id", UserController, :show
    get "/", PageController, :index
  end

コントローラーのソース消失!

model

defmodule AgesApp.User do
  use AgesApp.Web, :model

  schema "users" do
    field :name, :string
    field :os_type, :string
    field :device_id, :string
    field :device_name, :string
    field :os_version, :string
    field :game_money, :integer
    field :ios_charge, :integer
    field :android_charge, :integer
    field :max_hp, :integer
    field :level, :integer
    field :exp, :integer
    field :max_deck_count, :integer
    field :max_deck_point, :integer
    field :max_friend, :integer
    field :tutorial_progress, :integer
    field :last_login_at, :naive_datetime

    timestamps()
  end

  @doc """
  Builds a changeset based on the `struct` and `params`.
  """
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:name, :os_type, :device_id, :device_name, :os_version, :game_money, :ios_charge, :android_charge, :max_hp, :level, :exp, :max_deck_count, :max_deck_point, :max_friend, :tutorial_progress, :last_login_at])
    |> validate_required([:name, :os_type, :device_id, :device_name, :os_version, :game_money, :ios_charge, :android_charge, :max_hp, :level, :exp, :max_deck_count, :max_deck_point, :max_friend, :tutorial_progress, :last_login_at])
  end
end

Migrate、Seedのコードも消失!

Postgresは上手くいったが、その後 Redisにセッション保存をしようと思い、Redis用のライブラリを探す
exredisが有名らしいので調べたところ、キー取得時にエラーがわからないという酷い実装のため変更

exredis を使ってみなかった - Qiita

redixをパッケージに追加するとDependencyエラー
色々なバージョンを試してみたが、簡単にはいかない

その後、うちのメンバーに新人の参入可能性が高まったため、一度JavaScript覚えさせたいのと
最近のnodeは、ES6に対応したり、以前よりも複数人開発がしやすくなっているので
nodeの調査に変更した