はじめに
友人から Ruby ライクな Crystal 言語が書きやすくて良いと聞いて興味が出てきたので、
CrystalShards というサイトで Crystal の各種ライブラリを眺めていたところ、
Lucky というウェブフレームワークを発見したので勉強がてら触ってみました
(本当は 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 のインストール作業は完了です
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 です。
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"
を追記しておきます。
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点あるので確認していきます 1
1. .crystal-version を確認する
lucky-backend
内に .crystal-version
を確認して 0.29.0
と記載されているか確認します。
0.29.0
2. shard.yml を確認する
Crystal には shards というパッケージマネージャが公式で用意されているのですが、パッケージの依存関係が shard.yml
というファイルに記載されています。
この shard.yml
にはパッケージのビルドを行う際の Crystal バージョンも記載されているのですが、これが現在自分の使用している Crystal バージョン (0.29.0) と一致している確認します。
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_URL
や DB_HOST
を利用していることが確認できます。
# 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 '<パスワード>';
を入力してユーザの作成を行いました
3.5, Lucky プロジェクトのセットアップ (続き)
ここまで来たら、あとは Lucky プロジェクトのセットアップ用ビルドスクリプトを実行するのみです
# 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
↑ のような出力が確認できれば正常に実行出来てセットアップ完了です
また出力を確認すると、データベースの作成/マイグレーション、
シードデータの投入まで全てが一括で実行されていることが分かります。
ちなみにマイグレーションファイルは db/migrations
フォルダに生成されていきます。
初期はメールアドレスとパスワードのみを扱う users
テーブルを定義したファイルが存在しています↓
# ちなみに 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 にアクセスすると、
↓ の画面が確認出来るはずです

ユーザの登録/ログインを行ってみる
lucky init
を行った際に、ユーザ認証の仕組みを入れて、
登録/ログインについても既にページが用意された状態でプロジェクトについてセットアップ済みなので、
すぐにユーザの登録からログインまでの動作確認を行うことが可能です。
lucky dev
でサーバを起動してから、http://localhost:3001/sign_up にアクセスします。
すると ↓ のユーザ情報入力画面が出てくるので、必要な情報を入力して Sign Up
ボタンをクリックします。

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

また 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)
正しくデータベースにユーザの情報が登録出来ているようです
モデル関連の操作及び操作のための 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つファイルが生成されます。
それぞれのファイルについて説明します
- ./db/migrations/20190709092231_create_pictures.cr
- データベースの マイグレーションファイル
- ./src/models/post.cr
- モデルが扱う変数や関数の定義、アソシエーションを記載するファイル
- ./src/forms/post_form.cr
- モデルのバリデーションやカスタムコールバック定義を記載するファイル
- ./src/queries/post_query.cr
- モデルのクエリを記載するファイル (CRUD などの主要な クエリ は書く必要ない)
1. モデルのテーブル定義をマイグレーションファイルに記述する
まずは、出力されたマイグレーションファイルの中身を改修します。
Post
テーブルの定義を書いていきます。
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_id
と text
フィールドを持った posts
テーブルが作成されます。
lucky db.migrate
Migrated CreatePosts::V20190709093425
2. モデルの定義を記述する
Post
は User
が複数所有しているモデルなので、
アソシエーションを Post
と User
に記述していきます
(今はまだ利用する機会は無いですが、後述する内容で使用します)
まずは ./src/models/post.cr
に Post
モデル関連の定義を記述します。
Post
には必ず User
に所有されています。
このような関係は belongs_to
を使用します。
# 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
の記述を行うことで
Post
を作成した User
を
post.user
のような記述で取得することが出来るようになります。
次に User
の has_many
に Post
を設定します。
User
は多数の Posts
を所有しているからです。
このような関係にはhas_many
を使用します。
has_many
を使用することで 1対多の関係でモデルを関連付けることが出来ます。
# 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
の記述を行うことで、
User
モデルに紐付いた Post
モデルリストが、
user.posts
のような簡易な記述で取得することが可能になります。
これまで紹介した belongs_to
や has_many
の構文で宣言したものをアソシエーションと呼びます。詳細は こちら をご確認ください。
これで Post
モデル関連の定義は完了しました。
次に Post
モデルを実際に作成するために、
ルーティングを追加して Post
モデルを作成するページを作成していきます。
3. モデル作成のためのビューを作成する
まずは Post
を作成するためフォームを定義します。
./src/forms/post_form.cr
に必要なフィールドを宣言します。
class PostForm < Post::BaseForm
fillable title # タイトルの入力は必須
fillable text # 文章の入力は必須
fillable user_id # User の ID の入力は必須
end
次に で定義したフォームを利用したページへのルートを追加します。
ルーティングの追加は 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
を書き換えます。
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
に記述します。
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
次に で使用している
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
が作成されるので、中身を書き換えます
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
を入力して、実際に動作確認してみます
正しくコンパイルが通って実行されていれば http://localhost:3001/posts/new
にアクセス出来るようになっているはずです。アクセスすると のような html ページが表示されます。

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
の作成は出来るようになりました
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
リストが表示されるようにします。
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
に記述していきます。
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
にアクセスしてみます

のように今まで作成してきた
Post
の情報が li タグでリストで
見られるようになっていれば Post
リストを表示するページは完成です
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
の詳細が表示されるようにしていきます。
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
に記述していきます。
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
にアクセスしてみます

のように
Post
の詳細内容が見られれば、
Post
の詳細を表示するページは完成です
また Post
リストを表示するページで表示していた項目をクリックした時に、
Post
の詳細ページを表示するように改修していきたいと思います。
(検証の度に /posts/1 とか id を直接指定するのは面倒なため。。)
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
にアクセスしてみます

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

無事に該当する Post
の詳細が表示されれば成功です
Post
リストを表示するページへの遷移もブラウザで http://localhost:3001/posts
と入力しなければならないため、こちらもリンククリックでページ遷移できるよう MainLayout
を改修します
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/
にアクセスしてみます
するとヘッダに Posts
というリンクが追加されていることが確認出来ます。

Posts
リンクをクリックすることで、
Posts
リストを表示するページへの遷移が確認できれば完了です
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
が削除されるようにしていきます。
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
を改修していきます
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
リンクが追加されていることが確認できるはずです

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

すると のように削除された
Post
は Post
リストページから表示されなくなっているはずです。
おわりに
Crystal はまだアルファ版ということもあり、(2019/06/25 時点では)
言語仕様の変更が頻繁に入っているようなので、現段階での本番採用は難しいかもしれません。。
しかし Crystal がコンパイル型言語である &
Lucky が Rails 並の書きやすさでサクサク開発出来そうという点が非常に魅力的に見えたので、
引き続き動向をウォッチしていきたいと考えています
また現段階の記事内容だと、ただプロジェクトのサンプルを動かしただけなので、
今後は本記事に追記する形でビューやルーティング、モデルの追加等も行う予定です。
- [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
-
この項目内容をチェックしていなかったせいで、Lucky を動かすのに長時間を犠牲にしました。。。 ↩