[Yesod入門(5)] データベースとモデル

[Yesod入門(5)] データベースとモデル

YesodでDBを操作する方法

Stackでプロジェクト作成するときにテンプレートでDBを選択指定ます。Yesodで使えるDBは、

  • SQLite
  • PostgreSQL
  • MySQL
  • MongoDB

などなどが使えます。

YesodでDBを操作する方法を見ていきます。(1)でのプロジェクト作成時にSQLiteのテンプレートを使用したので、SQLiteをそのまま使用します。

config/models に記述されているのが、モデル(DBレイアウト)の定義です。すでにテンプレート作成時にいくつかのモデルがすでに定義されているので、それを参考に新しいモデルを定義してみます。

config/models

Article
    title Text
    published Day
    viewCount Int Maybe
    UniqueArticle title

新しいモデル Article を追加してみます。構文はモデル名(テーブル名), フィールド名, 型の順にそれっぽく書けばいいです。インデントは1桁目から揃える必要があります。また、フィールドはキャメルケースで書かなければ怒られます。つまりビルドが通りません。

ビルド時に自動的にマイグレーションが実行され、DBレイアウトが更新されます。基本的には物理キーとして、id(Integer)が割り振られるようです。

my-project.sqlite3 ファイルの中身を見ると(適当なツールで確認してください)、Articleテーブルが確認できます。

カラム 重複 NULL 主キー
id Integer NG NG
title Text NG NG
published DateTime OK NG
pages viewCount OK OK

Haskellの型とDBの型の対応はここで確認できます。

指定がなければNULLは許容しない重複は許容となります。NULLを許容するには Maybe をつけます。最終行の UniqueBook title で重複を禁止しています。つまりモデルを一意に判別する論理キーとなります。

モデルの定義を変更すれば自動的にマイグレーションが実行され、テーブルレイアウトが更新されます。データも(おそらく)引き継がれます。ただしNULL許容しないフィールドの追加など、データの整合性が取れない更新はエラーとなります。

今回はここまでにして、次回からDBのデータの 一覧表示・新規登録・更新・削除 の機能をそれぞれ実装しながらYesodにおけるDBの扱い方を調べます。

YesodでDBを操作する方法(データの一覧表示)

今回は前回作成したモデルのデータを画面に表示させたいと思います。これから作るDB操作のサンプルはいたってシンプルで、データを表示させる一覧画面とデータを登録・修正するための更新画面の2画面です。

まずは今回必要となるハンドラーの追加を行います。下記のコマンドを実行してハンドラーの追加を行います。

$ stack exec -- yesod add-handler
Name of route (without trailing R): ArticleList
Enter route pattern (ex: /entry/#EntryId): /ArticleList
Enter space-separated list of methods (ex: GET POST): GET

/ArticleList にGETでアクセスすると、Articleテーブルのデータが全権表示される画面を作ります。

処理の流れとしては、BookハンドラーにGETすると、編集用画面が表示され、データ更新時には同じハンドラーでPOSTでデータを受け取り更新します。

ハンドラーを編集します。

module Handler.ArticleList where

import Import

getArticleListR :: Handler Html
getArticleListR = do
    articles <- runDB $ selectList [] [Desc ArticlePublished]
    defaultLayout $(widgetFile "articleList")

ここでDBの操作方法を確認できます。

まだまだ詳しくはわかりませんが、 runDB を使ってDBにアクセスするようです。データの取得には selectList を使います。selectListの引数の2つが取得時の条件となります。

引数の1つ目がフィルター、つまり条件指定(SQLのWHERE句)です。今回は指定なし。

引数の2つ目が並び順の指定とか(SQLのORDER BY句)です?今回は日付の降順にしています。

以上で articles に取得したエンティティが取得できます。これをView側に渡して表示させたりできます。

次は View を作りましょう。 templates/articleList.hamlet を追加します。

templates/articleList.hamlet

<h1>Article一覧画面

<table border="1px">
    <tr>
        <th>タイトル
        <th>公開日
        <th>ビュー数
        <th>
    $forall Entity articleId article <- articles
        <tr>
            <td>
                #{articleTitle article}
            <td>
                #{show $ articlePublished article}
            <td>
                $maybe count <- articleViewCount article
                    #{count}
            <td>
                編集 削除

ここで一度実行してみます。まだデータがデータがないので、ヘッダー行だけしか表示されませんが、表示されればOKです。Linkは後々、修正と削除用のリンクを配置する予定で今はただの文字列です。

手でデータを入れれば表示が確認できます。

次回はデータ Articleデータの登録を行います。

Handlerの作成

前回まででデータの取得、表示ができました。(データがないので未確認ですが。)今回はデータを登録をできるようにしてみます。これがうまく実装できれば、前回実装したデータの表示についても確認できます。

まずはHandlerの追加からです。今回は Article というHandlerにデータの更新処理を任せます。

$ stack exec -- yesod add-handler
Name of route (without trailing R): Article
Enter route pattern (ex: /entry/#EntryId): /Article
Enter space-separated list of methods (ex: GET POST): GET POST

これでHandlerが完成しました。まずは新規登録処理から実装していきます。

Handler/Article.hs

module Handler.Article where

import Import

-- フォーム取得
articleForm :: Maybe Article -> Html -> MForm Handler (FormResult Article, Widget)
articleForm article extra = do
	(titleResult, titleView)            <- mreq textField "タイトル" (articleTitle <$> article)
	(publishedResult, publishedView)    <- mreq dayField "公開日" (articlePublished <$> article)
	(viewCountResult, viewCountView)    <- mopt intField "ビュー数" (articleViewCount <$> article)
	let result = Article
			<$> titleResult
			<*> publishedResult
			<*> viewCountResult
		widget = $(widgetFile "article-editor-form")
	return (result, widget)

-- 新規登録
getArticleR :: Handler Html
getArticleR = do
	let
		header = "Article新規登録" :: Text
	(widget, enctype) <- generateFormPost $ articleForm Nothing
	defaultLayout $(widgetFile "article")

-- 新規登録処理のポスト
postArticleR :: Handler Html
postArticleR = do
	((result, widget),enctype) <- runFormPost $ articleForm Nothing
	let
		header = "Article新規登録" :: Text
	case result of
		FormSuccess article -> do
			-- Postされたデータが正常な場合
			articleId <- runDB $ insert article
			redirect ArticleListR
		FormFailure _ -> do
			-- 不正な入力値のデータが送信された場合(必須項目が未入力等)
			setMessage "不正なデータが送信されました。"
			defaultLayout $(widgetFile "article")
		FormMissing -> defaultLayout [whamlet|データが送信されませんでした。 |]
		_ -> undefined

これでデータの新規登録処理は完成です。順番に処理を見ていきます。

articleFormについて(フォーム取得関数)

まずはフォームについてです。フォームにはいくつかの実装方法がありますが、今回は MonadicForm を使いました。参考にしたのはこちらのサイトと公式のページです。 ApplicativeForm だとフォームの生成をYesod側で行うので、ある程度自由にレイアウトを行うためには MonadicForm のほうがいいのかと思いまして。

まずフォームを取得する関数では引数に Maybe Article型 と Html型の値を取ります。Maybe Article型の引数にはフォームに表示させるモデルのデータです。新規登録時には Nothing を渡し、修正時には修正対象のデータを渡す予定です。Html型の extra は必要なおまじないとしておきます。

フォームの定義の記述について。

  • mreq/moptで、それぞれ必須入力/任意入力を指定します。
  • xxxFieldで入力フィールの種類を指定します。指定できるのはたぶんここにある分。だいたいInputタグのtype属性の内容みたいです。
  • 次の文字列でフィールドに紐づくラベルに表示させる内容を指定します。
  • 最後のカッコ内の内容が初期表示させる値です。

これらで定義されたフォーム内の個々のフィールドの内容を受け取るのが、(入力値, レンダリング情報)のタプルです。次の result はフォームで入力された内容(「入力値」)を元に定義された Articleモデルのデータです。widget はフォームのデザインを定義した別ファイル(templates/article-editor-form)を呼び出し、フォームデザインを定義します。

フォームのデザインは次のような感じです。

temlates/article-editor-form.hamlet

#{extra}
<table>
    <tr>
        <th>#{fvLabel titleView}
        <td>^{fvInput titleView}
    <tr>
        <th>#{fvLabel publishedView}
        <td>^{fvInput publishedView}
    <tr>
        <th>#{fvLabel viewCountView}
        <td>^{fvInput viewCountView}

#{extra}は必要なものです。fvLabelはHandler側で定義したフィールドのラベルの文字列です。fvInputが定義されたフィールドで、適した入力用フィールドが生成されます。

getArticleRについて

getArticleRではフォームのWidgetを取得し、画面に表示させています。

  • generateFormPost はHamletファイルで表示させる widget と enctype を作成します。
  • そしてフォームの widget を表示させる Hamlet ファイルを呼び出して終わりです。

    templates/article.hamlet

    <h1>#{header}
    <form action=@{ArticleR} method="POST" enctype="#{enctype}">
    	^{widget}
    	<input type="submit" value="送信">

    ^{widget}部分が上で作成した、フォーム用のViewが埋め込まれる個所です。

  • フォームの中で widget をレンダリングするようにしてサブミットボタンを追加してます。これでフォームで入力された内容をPOST先(postArticleR)で受け取ります。

postArticleRについて

では最後に入力値を受け取ってDBに登録してみます。

  • runFormPost では、POSTされたデータの取得を行います。 result は文字通り取得結果です。widget, enctype は generateFormPost と同様です。
  • Form の送信に対する結果はいくつかあります。 FormSuccess は送信データが正常の時。FormFailure は不正な送信データの時。Formmissingは送信に失敗した時?
  • 正しいデータが送信(POST)された場合、その入力結果の から生成された Article を DB へ Insert しています。insert を実行すると、値を挿入し、発行されたIdを返します。
  • 登録後、一覧画面にリダイレクトしてます。
  • FormFailure の場合には、setMessage でエラーメッセージを表示させています。その後再度 登録画面を表示させています。入力されたデータは再表示されます。

実行して確認

以上でDBへの登録が実装出来たので、動かしてみましょう。前回実装した一覧画面から呼び出すようにしてあげます。

templates/articleList.hamlet

<h1>Article一覧画面
<a href="@{ArticleR}">新規登録
<table border="1px">
	<tr>
		<th>タイトル
		<th>公開日
		<th>ビュー数
		<th>
	$forall Entity articleId article <- articles
		<tr>
			<td>
				#{articleTitle article}
			<td>
				#{show $ articlePublished article}
			<td>
				$maybe count <- articleViewCount article
					#{count}
			<td>編集 削除

適当な場所に上のリンクを追加して一覧画面から登録画面へリンクさせます。これで登録した内容が一覧画面で表示されているのが確認できる様になりました。次回は登録された内容を修正できるようにします。

参考URL

Haskellカテゴリの最新記事