LoginSignup
5
4

More than 3 years have passed since last update.

Crystal の Lucky という WebFramework 使ってみた on Mac

Last updated at Posted at 2019-06-25

はじめに

友人から Ruby ライクな Crystal 言語が書きやすくて良いと聞いて興味が出てきたので、
CrystalShards というサイトで Crystal の各種ライブラリを眺めていたところ、
Lucky というウェブフレームワークを発見したので勉強がてら触ってみました :large_blue_diamond:

(本当は amber という Crystal の WebFramework を触ってみようとしてたのですが、
Lucky という名前に惹かれて Lucky 触ってみることにした感じです。。。笑)

開発環境

  • macOS Mojave 10.14.5
  • PostgreSQL 11.2
  • OpenSSL 1.0.2s
  • Crystal 0.29.0
  • Lucky 0.15.0

Crystal & Lucky プロジェクトのセットアップ作業

1. Crystal のインストール

crenv という Crystal のバージョンマネージャでインストール作業を進めていきます。
anyenv 経由でのインストール推奨のようなので anyenv 経由で crenv を入れます。

brew install anyenv
anyenv init

# ANYENV_DEFINITION_ROOT(/Users/riywo/.config/anyenv/anyenv-install) doesn't exist. You can initialize it by:
# コマンド実行時に ↑ が都度出力される場合は anyenv install --init を実行
# anyenv install --init
anyenv install crenv

# shell 再起動後、↓ のコマンドを実行して crenv コマンドが使えることを確認する
crenv install --list

正常に crenv コマンドが使用できるようになったことが確認できたら、
本記事で使用する Crystal バージョンである 0.29.0 をインストールします。

# Crystal のバージョン 0.29.0 をインストール
crenv install 0.29.0

# デフォルトで使用する Crystal のバージョンを 0.29.0 に設定
crenv global 0.29.0

$ crystal --version
Crystal 0.29.0 (2019-06-05)

LLVM: 3.9.1
Default target: x86_64-apple-macosx

↑ の出力がターミナルで確認できれば Crystal のインストール作業は完了です:thumbsup:

2. Lucky のインストール

brew を使用して Lucky をインストールします。

ついでに openssl も Lucky のビルド時に必要になるのでインストールします。
また PostgreSQL もユーザデータの登録などに使用するのでインストールしておきます。

brew update

brew install openssl

brew tap luckyframework/homebrew-lucky
brew install lucky

# Lucky の確認 (↓ のコマンドを実行した際に Usage が表示されていればインストール済み)
$ lucky

# PostgreSQL サーバの起動を行う (起動してなかった時)
brew services start postgresql

また PKG_CONFIG_PATH/usr/local/opt/openssl/lib/pkgconfig を追加します。

bash を使用している方は .bash_profile 内に、
export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/opt/openssl/lib/pkgconfig を追記しておけば OK です。

~/.bash_profile
export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/opt/openssl/lib/pkgconfig

fish を使用している方は ~/.config/fish/config.fish 内に、
set -x PKG_CONFIG_PATH "$PKG_CONFIG_PATH:/usr/local/opt/openssl/lib/pkgconfig" を追記しておきます。

~/.config/fish/config.fish
set -x PKG_CONFIG_PATH "$PKG_CONFIG_PATH:/usr/local/opt/openssl/lib/pkgconfig"

各種ライブラリのインストール & 設定が確認できたら、
Lucky のプロジェクトを lucky コマンドを使用して作成します。

3. Lucky プロジェクトのセットアップ

lucky コマンドを使用することで、
プロジェクトの作成からモデルの作成等々を行うことが出来ます。
(rails コマンドみたいな役割ですね)

まずは lucky-backend という名前のプロジェクトを作成します。

$ lucky init
Project name? lucky-backend

Lucky can generate different types of projects

Full (recommended for most apps)

 ● Great for server rendered HTML or Single Page Applications
 ● Webpack included
 ● Setup to compile CSS and JavaScript
 ● Support for rendering HTML

API

 ● Specialized for use with just APIs
 ● No webpack
 ● No static file serving or public folder
 ● No HTML rendering folders

API only or full support for HTML and Webpack? (api/full): full

Lucky can be generated with email and password authentication

 ● Sign in and sign up
 ● Mixins for requiring sign in
 ● Password reset
 ● Generated files can easily be removed/customized later

Generate authentication? (y/n): y

-----------------------

Done generating your Lucky project

  ▸ cd into lucky-backend
  ▸ check database settings in config/database.cr
  ▸ run script/setup
  ▸ run lucky dev to start the server

↑ の出力が確認できたら、コマンドを実行したディレクトリに、
lucky-backend というプロジェクトフォルダが生成されているはずです。

次に Lucky のビルドスクリプトを実行して、
lucky dev というコマンドで開発が進められるようにしていくのですが、
その前に予めチェックしておくべき項目が 3点あるので確認していきます:white_check_mark: 1

1. .crystal-version を確認する

lucky-backend 内に .crystal-version を確認して 0.29.0 と記載されているか確認します。

lucky-backend/.crystal-version
0.29.0

2. shard.yml を確認する

Crystal には shards というパッケージマネージャが公式で用意されているのですが、パッケージの依存関係が shard.yml というファイルに記載されています。

この shard.yml にはパッケージのビルドを行う際の Crystal バージョンも記載されているのですが、これが現在自分の使用している Crystal バージョン (0.29.0) と一致している確認します。

lucky-backend/shared.yml
name: lucky_backend
version: 0.1.0

authors:
  - hoge <fuga@test.com>

targets:
  lucky_backend:
    main: src/lucky_backend.cr

# ここの表記が 0.29.0 であることを確認する
# 私の環境では 0.27.2 になっていてビルドに失敗し続けていた。。
crystal: 0.29.0

dependencies:
  lucky:
    github: luckyframework/lucky
    version: ~> 0.15.0
  authentic:
    github: luckyframework/authentic
    version: ~> 0.3.0
  carbon:
    github: luckyframework/carbon
    version: ~> 0.1.0
  dotenv:
    github: gdotdesign/cr-dotenv

  lucky_flow:
    github: luckyframework/lucky_flow
    version: ~> 0.4.1

3. yarn をインストールする

Lucky ではデフォで webpack を使用しており、ビルドスクリプトの実行するのに yarn コマンドが必要なので、インストールされていない方は brew install yarn で予めインストールしておきます。

brew install yarn

4. データベースの接続情報を設定する (PostgreSQL)

config/database.cr の中を見ると、データベースへの接続状況として環境変数の DATABASE_URLDB_HOST を利用していることが確認できます。

lucky-backend/config/database.cr
# Lucky の実行環境に応じてデータベース名を設定する
database_name = "lucky_backend_#{Lucky::Env.name}"

Avram::Repo.configure do |settings|
  if Lucky::Env.production?
    # 本番環境では DATABASE_URL という環境変数の設定が必須になっている
    settings.url = ENV.fetch("DATABASE_URL")
  else
    # DATABASE_URL は設定しなくとも Avram::PostgresURL.build を使用することで、
    # データベースのユーザ名やホストを指定することで適切な PostgreSQL URL を生成する
    settings.url = ENV["DATABASE_URL"]? || Avram::PostgresURL.build(
      database: database_name,
      hostname: ENV["DB_HOST"]? || "localhost",
      username: ENV["DB_USERNAME"]? || "postgres",
      password: ENV["DB_PASSWORD"]? || "postgres"
    )
  end

  # 後述の説明では DATABASE_URL に値を適切に設定することで DB への接続を実現していますが、
  # 環境変数に情報をセットしておくのが面倒な方でとにかく動かしたいは、
  # ここに決め打ちで ↓ の用な感じで PostgreSQL の URL を設定してしまっても良いです。
  # settings.url = "postgresql://localhost:5432/#{database_name}?user=<ユーザ名>&password=<パスワード>"
  settings.lazy_load_enabled = Lucky::Env.production?
end

今回は DATABASE_URL に一括でデータベースへの接続情報を設定したいと思いますので ↓ コマンドで PostgreSQL の URL をセットします。

export DATABASE_URL=postgresql://localhost:5432/lucky_backend_development?user=<ユーザ名>&password=<パスワード>

fish を使用している方は ↓ コマンドで DATABASE_URL に PostgreSQL の URL を環境変数にセットします。

set -x DATABASE_URL "postgresql://localhost:5432/lucky_backend_development?user=<ユーザ名>&password=<パスワード>"

また PostgreSQL のロールは CREATEDB のものを使用してください。
PostgreSQL のロールの作り方はこちらのサイトを参考にされると良いと思います。

筆者はターミナル開いて psql postgres 実行後 CREATE ROLE <ユーザ名> WITH CREATEDB LOGIN PASSWORD '<パスワード>'; を入力してユーザの作成を行いました :thumbsup:

3.5, Lucky プロジェクトのセットアップ (続き)

ここまで来たら、あとは Lucky プロジェクトのセットアップ用ビルドスクリプトを実行するのみです:relaxed:

# lucky プロジェクトに入っている setup シェルスクリプトを実行する
$ sh script/setup
▸ Installing node dependencies
  yarn install v1.16.0
  [1/4] Resolving packages...
  success Already up-to-date.
  Done in 0.35s.

▸ Compiling assets
  yarn run v1.16.0
  $ yarn run webpack --progress --hide-modules --color --config=node_modules/laravel-mix/setup/webpack.config.js
$ /Users/nika/Desktop/qiita/lucky-backend/node_modules/.bin/webpack --progress --hide-modules --color --config=node_modules/laravel-mix/setup/webpack.config.js
 98% after emitting SizeLimitsPlugin   DONE  Compiled successfully in 1039ms00:56:04

  Done in 3.03s.

▸ Installing shards
  Fetching https://github.com/luckyframework/lucky.git
  Fetching https://github.com/luckyframework/lucky_cli.git
  Fetching https://github.com/mosop/teeplate.git
  Fetching https://github.com/luckyframework/habitat.git
  Fetching https://github.com/luckyframework/wordsmith.git
  Fetching https://github.com/luckyframework/avram.git
  Fetching https://github.com/kostya/blank.git
  Fetching https://github.com/crystal-lang/crystal-db.git
  Fetching https://github.com/will/crystal-pg.git
  Fetching https://github.com/luckyframework/dexter.git
  Fetching https://github.com/luckyframework/lucky_router.git
  Fetching https://github.com/luckyframework/shell-table.cr.git
  Fetching https://github.com/paulcsmith/cry.git
  Fetching https://github.com/mosop/cli.git
  Fetching https://github.com/mosop/optarg.git
  Fetching https://github.com/mosop/callback.git
  Fetching https://github.com/mosop/string_inflection.git
  Fetching https://github.com/crystal-loot/exception_page.git
  Fetching https://github.com/luckyframework/authentic.git
  Fetching https://github.com/luckyframework/carbon.git
  Fetching https://github.com/gdotdesign/cr-dotenv.git
  Fetching https://github.com/luckyframework/lucky_flow.git
  Fetching https://github.com/ysbaddaden/selenium-webdriver-crystal.git
  Using lucky (0.15.1)
  Using lucky_cli (0.15.0)
  Using teeplate (0.7.0)
  Using habitat (0.4.3)
  Using wordsmith (0.2.0)
  Using avram (0.10.0)
  Using blank (0.1.0)
  Using db (0.5.1)
  Using pg (0.16.1)
  Using dexter (0.1.1)
  Using lucky_router (0.2.2)
  Using shell-table (0.9.2 at refactor/setter)
  Using cry (0.4.0)
  Using cli (0.7.0)
  Using optarg (0.5.8)
  Using callback (0.6.3)
  Using string_inflection (0.2.1)
  Using exception_page (0.1.2)
  Using authentic (0.3.0)
  Using carbon (0.1.0)
  Using dotenv (0.2.0)
  Using lucky_flow (0.4.1)
  Using selenium (0.4.0 at a6d0e63ab7ddc6a20923bf4157bc6247c1fa2acc)

▸ Checking that a process runner is installed
  ✔ Done

▸ Setting up the database
  Already created lucky_backend_development

▸ Migrating the database
  Did not migrate anything because there are no pending migrations.

▸ Seeding the database with required and sample records
  Done adding required data
  Done adding sample data

✔ All done. Run 'lucky dev' to start the app

↑ のような出力が確認できれば正常に実行出来てセットアップ完了です:bangbang:

また出力を確認すると、データベースの作成/マイグレーション、
シードデータの投入まで全てが一括で実行されていることが分かります。

ちなみにマイグレーションファイルは db/migrations フォルダに生成されていきます。
初期はメールアドレスとパスワードのみを扱う users テーブルを定義したファイルが存在しています↓

lucky-backend/db/migrations/00000000000001_create_users.cr
# ちなみに Lucky 内部では Avram という ORM が使用されています
class CreateUsers::V00000000000001 < Avram::Migrator::Migration::V1
  def migrate
    create :users do
      # メールアドレスとパスワードを扱う users テーブルが PostgreSQL 内に生成される
      add email : String, unique: true
      add encrypted_password : String
    end
  end

  def rollback
    drop :users
  end
end

それでは早速 lucky dev とターミナルに入力してみます。

$ lucky dev
[OKAY] Loaded ENV .env File as KEY=VALUE Format
01:07:58 assets.1   |  yarn run v1.16.0
01:07:58 assets.1   |  $ yarn run webpack --watch --hide-modules --color --config=node_modules/laravel-mix/setup/webpack.config.js

01:07:58 assets.1   |  
01:07:58 assets.1   |  $ /Users/nika/Desktop/qiita/lucky-backend/node_modules/.bin/webpack --watch --hide-modules --color --config=node_modules/laravel-mix/setup/webpack.config.js
01:08:00 assets.1   |  webpack is watching the files…
01:08:01 assets.1   |   DONE  Compiled successfully in 1359ms01:08:01
01:08:04 web.1      |  yarn run v1.16.0
01:08:04 web.1      |  $ /Users/nika/Desktop/qiita/lucky-backend/node_modules/.bin/browser-sync start -c bs-config.js --port 3001 -p http://0.0.0.0:5000
01:08:05 web.1      |  [Browsersync] Proxying: http://0.0.0.0:5000
01:08:05 web.1      |  [Browsersync] Access URLs:
01:08:05 web.1      |   ----------------------------
01:08:05 web.1      |   Local: http://localhost:3001
01:08:05 web.1      |   ----------------------------
01:08:05 web.1      |  [Browsersync] Watching files...

実行したらブラウザを開いて http://localhost:3001 にアクセスすると、
↓ の画面が確認出来るはずです:clap:

スクリーンショット 2019-06-25 1.09.59.png

ユーザの登録/ログインを行ってみる

lucky init を行った際に、ユーザ認証の仕組みを入れて、
登録/ログインについても既にページが用意された状態でプロジェクトについてセットアップ済みなので、
すぐにユーザの登録からログインまでの動作確認を行うことが可能です。

lucky dev でサーバを起動してから、http://localhost:3001/sign_up にアクセスします。
すると ↓ のユーザ情報入力画面が出てくるので、必要な情報を入力して Sign Up ボタンをクリックします。

スクリーンショット 2019-06-25 13.58.50.png

ユーザの登録に成功すると自動的に http://localhost:3001/me に遷移して、
登録したユーザ情報が確認できる画面が表示されます。

スクリーンショット 2019-06-25 13.59.17.png

また http://localhost:3001/sign_in でユーザのログイン画面に遷移します。

ユーザ登録時に入力した情報でログインすることが可能です。
ログイン成功後は http://localhost:3001/me に遷移します。

正しくユーザ登録が行えているかターミナルから psql コマンドでデータベースの中を見てみます。

psql lucky_backend_development
psql (11.2)
Type "help" for help.

lucky_backend_development=# select * from users;
 id |       created_at       |       updated_at       |     email     |                      encrypted_password                      
----+------------------------+------------------------+---------------+--------------------------------------------------------------
  2 | 2019-06-25 13:59:09+09 | 2019-06-25 13:59:09+09 | hoge@fuga.com | $2a$04$TA1q9jt4U2MPSP3Fi5uwm.qCQcIFd7syy6WJ/SCwz6QC7WDLt6bwm
(1 row)

正しくデータベースにユーザの情報が登録出来ているようです:thumbsup:

モデル関連の操作及び操作のための HTML ページを作成する

次にモデルの作成を行います。
各ユーザが文章を投稿できるサービスを想定して、
Post というモデルを作成したいと思います。

Lucky にはデフォでデータベースを操作する各種コマンドが用意されています。
まずはそれらを活用して Post を作成するのに必要なファイルを作成します。

lucky gen.model コマンドを使用します。

$ lucky gen.model Post

Created CreatePosts::V20190709093425 in ./db/migrations/20190709093425_create_posts.cr
Generated Post in ./src/models/post.cr
Generated PostForm in ./src/forms/post_form.cr
Generated PostQuery in ./src/queries/post_query.cr

lucky gen.model コマンドの実行に成功すると 4つファイルが生成されます。
それぞれのファイルについて説明します :arrow_down:

  • ./db/migrations/20190709092231_create_pictures.cr
  • ./src/models/post.cr
    • モデルが扱う変数や関数の定義、アソシエーションを記載するファイル
  • ./src/forms/post_form.cr
    • モデルのバリデーションやカスタムコールバック定義を記載するファイル
  • ./src/queries/post_query.cr
    • モデルのクエリを記載するファイル (CRUD などの主要な クエリ は書く必要ない)

1. モデルのテーブル定義をマイグレーションファイルに記述する

まずは、出力されたマイグレーションファイルの中身を改修します。
Post テーブルの定義を書いていきます。

./db/migrations/20190709091814_create_pictures.cr
class CreatePosts::V20190709093425 < Avram::Migrator::Migration::V1
  def migrate
    create :posts do # lucky db.migrate コマンド実行時に posts テーブルを作成
      add user_id : Int32 # User の ID フィールドを追加 
      add title : String # タイトルを保存しておくフィールドを追加
      add text : String # 文章を保存しておくフィールドを追加
    end
  end

  def rollback # lucky db.rollback コマンド実行時に posts テーブルを削除
    drop :posts
  end
end

User との紐づけのため user_id のフィールドを追加しています。

この状態で lucky db.migrate を実行することでデータベース上に、
user_idtext フィールドを持った posts テーブルが作成されます。

lucky db.migrate

Migrated CreatePosts::V20190709093425

2. モデルの定義を記述する

PostUser が複数所有しているモデルなので、
アソシエーションを PostUser に記述していきます :pencil:
(今はまだ利用する機会は無いですが、後述する内容で使用します)

まずは ./src/models/post.crPost モデル関連の定義を記述します。
Post には必ず User に所有されています。
このような関係は belongs_to を使用します。

./src/models/post.cr
# User モデルをアソシエーションに設定するためインポートする
# Crystal では別ファイルの内容を参照する場合は require を記述する必要がある
require "./user"

class Post < BaseModel
  table :posts do # Post モデルで扱う変数を定義する
    column title : String # ユーザ入力した文章のタイトルを扱うフィールド
    column text : String # ユーザが入力した文章を扱うフィールド
    belongs_to user : User # user_id を元に User モデルが取得出来るようになる

    # id, updated_at, created_at は宣言しなくてもデフォで参照可能なフィールド
  end
end

:arrow_up: の記述を行うことで Post を作成した User
post.user のような記述で取得することが出来るようになります。

次に Userhas_manyPost を設定します。
User は多数の Posts を所有しているからです。
このような関係にはhas_many を使用します。

has_many を使用することで 1対多の関係でモデルを関連付けることが出来ます。

./src/models/user.cr
# User モデルをアソシエーションに設定するためインポートする
require "./post"

class User < BaseModel
  include Carbon::Emailable
  include Authentic::PasswordAuthenticatable

  table :users do
    column email : String
    column encrypted_password : String

    # has_many にモデルを設定するときは複数形で名前を設定する
    # 今回は Post モデルを has_many で紐づけるため posts としている
    has_many posts : Post
  end

  def emailable
    Carbon::Address.new(email)
  end
end

:arrow_up: の記述を行うことで、User モデルに紐付いた Post モデルリストが、
user.posts のような簡易な記述で取得することが可能になります。

これまで紹介した belongs_tohas_many の構文で宣言したものをアソシエーションと呼びます。詳細は こちら をご確認ください。

これで Post モデル関連の定義は完了しました。

次に Post モデルを実際に作成するために、
ルーティングを追加して Post モデルを作成するページを作成していきます。

3. モデル作成のためのビューを作成する

まずは Post を作成するためフォームを定義します。
./src/forms/post_form.cr に必要なフィールドを宣言します。

./src/forms/post_form.cr
class PostForm < Post::BaseForm
  fillable title # タイトルの入力は必須
  fillable text # 文章の入力は必須
  fillable user_id # User の ID の入力は必須
end

次に :arrow_up: で定義したフォームを利用したページへのルートを追加します。
ルーティングの追加は lucky gen.action.browser コマンドで行います。
(ちなみに lucky gen.action.api コマンドで API のエンドポイントを作成することも可能です)

またルーティングで定義したパスにアクセスした際に表示する html ページは、
lucky gen.page コマンドで作成可能なのでルーティングの追加と同時に作成しておきます。

lucky gen.action.browser Posts::New # Post を作成するためのページへのルートを作成
Done generating Posts::New in ./src/actions/posts

lucky gen.page Posts::NewPage # Post を作成するための html ページの作成
Done generating Posts::NewPage in ./src/pages/posts/new_page.cr

まずはルーティングを設定するため ./src/actions/posts/new.cr を書き換えます。

./src/actions/posts/new.cr
class Posts::New < BrowserAction
  # http://localhost:3001/posts/new へブラウザからアクセスすると、
  # 表示されるページの定義
  get "/posts/new" do
    # Form に ./src/forms/post_form.cr で定義したものを使用
    # 実際にレンダリングするページとして ./src/pages/posts/new_page.cr を使用する
    render NewPage, form: PostForm.new
  end
end

次に実際に Post を作成するためのページ内容を ./src/pages/posts/new_page.cr に記述します。

./src/pages/posts/new_page.cr
class Posts::NewPage < MainLayout
  # ページ内では PostForm の内容を利用する
  needs form : PostForm

  # html ページで表示する内容を書く
  def content
    # h1 タグで "Create Posts" と表示する
    h1 "Create Posts"

    # form_for を使用し Posts::Create アクションを実行する form タグを作成する (詳細は後述)
    form_for Posts::Create do
      # インプットフォームに title に情報を入力するための text インプットフィールドを用意する
      # オプションに autofocus を指定することでページを表示した際に自動でフォーカスが当たるようにしている
      mount Shared::Field.new(@form.title), &.text_input(autofocus: "true")

      # インプットフォームに text に情報を入力するための textarea フィールドを用意する
      mount Shared::Field.new(@form.text), &.textarea

      # インプットフォームに user_id に情報を入力するための hidden インプットフィールドを用意する
      # オプションに value を指定することでデフォの値を指定している
      # @current_user を使用すると MainLayout 内であれば、ログイン中のユーザの情報を取得/利用することが可能となる
      mount Shared::Field.new(@form.user_id), &.hidden_input(value: @current_user.id)

      # submit タグを作成する。クリックした時に Posts::Create アクションが実行される
      submit "Create"
    end
  end
end

次に :arrow_up: で使用している Posts::Create アクションを作成します。
例の如く lucky gen.action.browser で作成します。

lucky gen.action.browser Posts::Create # Post を作成するためのルートを作成
Done generating Posts::Create in ./src/actions/posts

./src/actions/posts/create.cr が作成されるので、中身を書き換えます :arrow_down:

./src/actions/posts/create.cr
class Posts::Create < BrowserAction
  route do
    # PostForm は BaseForm を継承しているのでデフォで create という関数が用意されている
    # create を宣言する際に引数に params を宣言しておくと遷移前のページで入力した内容を取得することが出来る
    # 無事にモデルを作成することが出来れば post に作成したモデルの情報が設定される
    PostForm.create(params) do |form, post|
      if post
        # Post の作成に成功したらターミナルに info ログを出力する
        Lucky.logger.info("Thanks for creating post")
      else
        # Post の作成に失敗したらターミナルに warn ログを出力する
        Lucky.logger.warn("Couldn't create post")
      end
      # Post の作成の成功/失敗に関わらず Home::Index にリダイレクトする
      redirect to: Home::Index
    end
  end
end

ここまで来たら lucky dev を入力して、実際に動作確認してみます :white_check_mark:
正しくコンパイルが通って実行されていれば http://localhost:3001/posts/new にアクセス出来るようになっているはずです。アクセスすると :arrow_down: のような html ページが表示されます。

スクリーンショット 2019-07-12 0.04.36.png

Title と Text の項目に適当な文字列を入力した後に Create ボタンをクリックすると、
PostgreSQL 内のテーブルに実際にデータが挿入されたことが確認出来ると思います。

lucky_backend_development=# select *  from posts;
 id |       created_at       |       updated_at       | user_id |    title     |       text       
----+------------------------+------------------------+---------+--------------+------------------
  1 | 2019-07-12 00:05:14+09 | 2019-07-12 00:05:14+09 |       2 | 投稿タイトル | 
投稿した文章内容
(1 row)

これで無事 Post の作成は出来るようになりました :clap:

4. モデルをリスト表示するためのビューを作成する

次に User に紐付いた Post リストを表示するためのページを作成します。
例のごとく Post をリスト表示するためのルートと html ページを作成します。

lucky gen.action.browser Posts::List # ログインユーザに紐付いた Post モデルのリストを表示するためのパスを追加
Done generating Posts::List in ./src/actions/posts

lucky gen.page Posts::ListPage # ログインユーザに紐付いた Post リストを表示するための html ページを追加
Done generating Posts::ListPage in ./src/pages/posts/list_page.cr

次に ./src/actions/posts/list_page.cr を改修して /posts というパスに GET でアクセスすることで、
現在ログインしているユーザに紐付いた Post リストが表示されるようにします。

./src/actions/posts/list.cr
class Posts::List < BrowserAction
  # /posts というパスに GET アクセスした際に、
  # ユーザに紐付いた Post リストページを表示する
  get "/posts" do
    # Posts::ListPage をレンダリングする際に利用する変数として、
    # ユーザに紐付いた Post リストを使用する
    render ListPage, posts: current_user_posts
  end

  private def current_user_posts
    # User の操作を行うための UserQuery を使用して、
    # User に紐付いた Post リストを取得する
    # preload_posts を使用することで User を検索すると同時に、
    # posts で `Post` のリストが取得出来るようにしている
    UserQuery.new.preload_posts.find(current_user.id).posts
  end
end

次に Post::ListPage で実際にレンダリングする内容を記述していきます。
./src/pages/posts/list_page.cr に記述していきます。

./src/pages/posts/list_page.cr
class Posts::ListPage < MainLayout
  # レンダリング時に使用する設定必須な変数として posts を宣言します
  # posts には Post の配列が設定されます(それ以外を設定しようとするとエラーになります)
  needs posts : Array(Post)

  def content
    h1 "This is your posts."
    ul do
      # each 関数を使用して posts の中身をループで参照します
      # li タグで post のタイトルを出力します
      @posts.each do |post|
        li "#{post.title}"
      end
    end
  end
end

ここまで来たら一旦 lucky dev でサーバを起動し、ブラウザから http://localhost:3001/posts にアクセスしてみます :earth_asia:

スクリーンショット 2019-07-12 0.09.00.png

:arrow_up: のように今まで作成してきた Post の情報が li タグでリストで
見られるようになっていれば Post リストを表示するページは完成です :clap:

5. モデルの詳細を表示するためのビューを作成する

Post リストが表示されるページは作成出来たので、
次に Post の詳細を表示するためのページについて作成します。

例のごとく Post の詳細を表示するためのルートと html ページを作成します。

lucky gen.action.browser Posts::Show # Post の詳細を表示するためのパスを追加
Done generating Posts::Show in ./src/actions/posts

lucky gen.page Posts::ShowPage # Post の詳細を表示するための html ページを追加
Done generating Posts::ShowPage in ./src/pages/posts/list_page.cr

次に ./src/actions/posts/show.cr を改修して /posts/:id というパスに GET でアクセスすることで、Post の詳細が表示されるようにしていきます。

./src/actions/posts/show.cr
class Posts::Show < BrowserAction
  # /posts/1 のような URL で該当する post の詳細が出力出来るようにする
  # :id のように指定することで、例えば /posts/2 という URL でアクセスした時に
  # id を参照することで 2 が取得出来るようになる
  get "/posts/:id" do
    # PostQuery を使用して該当する id の Post を取得する
    post = PostQuery.find(id)

    # Posts::ShowPage をレンダリングする際に利用する変数として、
    # id で検索した Post を使用する
    render ShowPage, post: post
  end
end

次に Post::ShowPage で実際にレンダリングする内容を記述していきます。
./src/pages/posts/show_page.cr に記述していきます。

./src/pages/posts/show_page.cr
class Posts::ShowPage < MainLayout
  # レンダリング時に使用する設定必須な変数として post を宣言します
  # post には Post が設定されます(それ以外を設定しようとするとエラーになります)
  needs post : Post

  def content
    # post のタイトル、更新日時と文章の内容を出力する
    h1 "#{@post.title}: #{@post.updated_at}"
    div do
      text @post.text
    end
  end
end

ここまで来たら一旦 lucky dev でサーバを起動し、ブラウザから http://localhost:3001/posts/1 にアクセスしてみます :earth_africa:

スクリーンショット 2019-07-12 0.24.30.png

:arrow_up: のように Post の詳細内容が見られれば、
Post の詳細を表示するページは完成です :clap:

また Post リストを表示するページで表示していた項目をクリックした時に、
Post の詳細ページを表示するように改修していきたいと思います。
(検証の度に /posts/1 とか id を直接指定するのは面倒なため。。)

./src/pages/posts/list_page.cr
class Posts::ListPage < MainLayout
  needs posts : Array(Post)

  def content
    h1 "This is your posts."
    ul do
      @posts.each do |post|
        li do
          # li タグ内に link 関数を使用することで、
          # クリック時に遷移するページを定義します
          # to: の部分に遷移先のページを指定します

          # 遷移先のページを指定する際は引数に任意のパラメタを渡すことが可能です

          # Posts::Show ページは id を必要とするページなので、
          # id を引数に渡しています
          link post.title, to: Posts::Show.with(post.id)
        end
      end
    end
  end
end

ここまで来たら一旦 lucky dev でサーバを起動し、ブラウザから http://localhost:3001/posts にアクセスしてみます :earth_americas:

スクリーンショット 2019-07-12 0.34.11.png

:arrow_up: のように Post のリストにリンクが設定されているはずです。
実際に設定されたリンクをクリックしてみます。結果 :arrow_down:

スクリーンショット 2019-07-12 0.35.56.png

無事に該当する Post の詳細が表示されれば成功です :clap: :clap:

Post リストを表示するページへの遷移もブラウザで http://localhost:3001/posts と入力しなければならないため、こちらもリンククリックでページ遷移できるよう MainLayout を改修します :pick:

./src/pages/main_layout.cr
abstract class MainLayout
  include Lucky::HTMLPage

  # 'needs current_user : User' makes it so that the current_user
  # is always required for pages using MainLayout
  needs current_user : User

  abstract def content
  abstract def page_title

  # The default page title. It is passed to `Shared::LayoutHead`.
  #
  # Add a `page_title` method to pages to override it. You can also remove
  # This method so every page is required to have its own page title.
  def page_title
    "Welcome"
  end

  def render
    html_doctype

    html lang: "en" do
      mount Shared::LayoutHead.new(page_title: page_title, context: @context)

      body do
        mount Shared::FlashMessages.new(@context.flash)
        render_signed_in_user
        content
      end
    end
  end

  private def render_signed_in_user
    text @current_user.email
    text " - "
    # ヘッダに Posts::List へのリンクを追加する
    link "Posts", to: Posts::List
    text " - "
    link "Sign out", to: SignIns::Delete, flow_id: "sign-out-button"
  end
end

ここまで来たら再び http://localhost:3001/ にアクセスしてみます :earth_asia:
するとヘッダに Posts というリンクが追加されていることが確認出来ます。

スクリーンショット 2019-07-12 0.49.10.png

Posts リンクをクリックすることで、
Posts リストを表示するページへの遷移が確認できれば完了です :clap: :clap: :clap:

6. モデルを削除するためのパスを作成する

Post モデルを削除するためのパスを追加します。
まずは例のごとく lucky gen.action.browser でパスを追加します。

lucky gen.action.browser Posts::Delete # Post を削除するためのページへのルートを作成
Done generating Posts::Delete in ./src/actions/posts

次に ./src/actions/posts/delete.cr を改修して /posts/:id というパスに DELETE でアクセスすることで、Post が削除されるようにしていきます。

./src/actions/posts/delete.cr
class Posts::Delete < BrowserAction
  delete "/posts/:id" do
    # PostQuery で検索した Post モデルを
    # delete 関数を使用することで削除する (モデルが検索出来なかった時の例外処理は無いけど。。)
    post = PostQuery.find(id)
    post.delete

    # 削除後、Post リストを表示するページに遷移する
    redirect to: Posts::List
  end
end

削除の操作は Post の詳細を表示するページから行うため、
./src/pages/posts/show_page.crを改修していきます :pick:

./src/pages/posts/show_page.cr
class Posts::ShowPage < MainLayout
  needs post : Post

  def content
    h1 "#{@post.title}: #{@post.updated_at}"
    div do
      text @post.text
    end

    # Post の詳細を表示しているページの Post に Post を削除するためのリンクを追加する
    link "Delete", to: Posts::Delete.with(@post.id)
  end
end

ここまで来たら再び http://localhost:3001/posts/1 等で存在する Post にアクセスしてみます。
するとページ末尾に Delete リンクが追加されていることが確認できるはずです :arrow_down:

スクリーンショット 2019-07-12 1.07.06.png

試しに Delete リンクをクリックして Post が削除されるか検証してみましょう :arrow_down:

スクリーンショット 2019-07-12 1.08.30.png

すると :arrow_up: のように削除された PostPost リストページから表示されなくなっているはずです。 :clap:

おわりに

Crystal はまだアルファ版ということもあり、(2019/06/25 時点では)
言語仕様の変更が頻繁に入っているようなので、現段階での本番採用は難しいかもしれません。。

しかし Crystal がコンパイル型言語である &
Lucky が Rails 並の書きやすさでサクサク開発出来そうという点が非常に魅力的に見えたので、
引き続き動向をウォッチしていきたいと考えています:eye:

また現段階の記事内容だと、ただプロジェクトのサンプルを動かしただけなので、
今後は本記事に追記する形でビューやルーティング、モデルの追加等も行う予定です。:writing_hand:

  • [2019/07/12] ビューやルーティング、モデルについて記述を追加しました

参考リンク

https://github.com/pine/crenv/blob/master/README.ja.md
https://luckyframework.org/guides/getting-started/installing
https://luckyframework.org/guides/getting-started/starting-project
https://luckyframework.org/guides/database/intro-to-avram-and-orms
https://luckyframework.org/guides/database/managing-and-migrating
https://qiita.com/pinemz/items/e71903532c24cbeb200a
https://github.com/anyenv/anyenv
https://www.dbonline.jp/postgresql/role/index2.html
https://dev.to/mikeeus/uploading-and-validating-images-with-crystal-and-lucky-on-heroku-13p2
http://labyrinth-of-wisdom.hatenadiary.com/entry/2016/02/24/084949


  1. この項目内容をチェックしていなかったせいで、Lucky を動かすのに長時間を犠牲にしました。。。 

5
4
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4