Help us understand the problem. What is going on with this article?

Haskell・Servant+Persistent/Esqueletoで作る実用WebAPI (14) Esqueleto:INNER JOINを使ったSELECT(その2)

はじめに

またまた前の記事から間があいてしまいましたが、「Advent Calendarに投稿する」というところで腰をあげてみました(と書き始めたものの、登録日には間に合わなかった...)。

今回はINNER JOINの続きです。selectで取得したデータの取り扱いとINNER JOINのバリエーションについて書きます。

INNER JOINありの場合のselectの戻り値を処理する

前回の続きなので前置きなしで始めますが、SQL処理部分のHaskellコードは再掲しますね。

 box_list <- select $ from $ \(b `InnerJoin` a) -> do
    on $ b a BoxAccountId ==. a ^. AccountId
    where_ $ a ^. AccountName ==. val "Account-1"
    return (a, b)

joinをしないselectで取得できるデータは、今回の例でいくと[Entity Account]な型(Entity Accountのリスト)でしたが、joinをしてさらにreturnでタプルを指定すると、戻り値が[(Entity Account, Entity Box)]というようなタプルになります。「joinするとタプルになる」のではなく、「returnでタプルを指定するとタプルになる」です。仮にこの部分を

 box_list <- select $ from $ \(b `InnerJoin` a) -> do
    on $ b ^. BoxAccountId ==. a ^. AccountId
    where_ $ a ^. AccountName ==. val "Account-1"
    return b

とすれば、戻り値の型は[Entity Box]となります。この実装に続く処理にてEntity Accountの情報が不要な場合には、このような実装を使うこともあります。

話を戻して、戻り値が[(Entity Account, Entity Box)]という型であった場合、この型を引数にとり、例えばAPIの戻り値となる型に変換する関数を実装する場面が多いかと思います。APIの戻り値として「Boxの情報にAccountの情報をちょっと添えたもの」を想定してみます。

data ApiBox = ApiBox
  { apiBoxId = BoxId
  , apiBoxName = Text
  , apiAccountId = AccountId
  , apiAccountName = Text
  }

本来はAccountの情報を一通り所持している「ApiAccount」なる型を定義して、それをApiBoxとは別にセットする、というのが汎用的ではあるのですが、こういったショートカットをすることもあるかと思います。この(Entity Account, Entity Box)から、このように定義されたApiBoxへの変換関数toApiBoxFEは下記のようになります。

toApiBoxFE :: (Entity Account, Entity Box) -> ApiBox
toApiBoxFE (Entity account_id account, Entity box_id box) =
  ApiBox
  { apiBoxId = box_id
  , apiBoxName = boxName box
  , apiAccountId = account_id
  , apiAccountName = accountName account
  }

toApiBoxの仮引数はEntity型のタプルですが、2行目のようにコンストラクタを記述してやることで、タプルやEntityのkey, valueをばらすような記述は不要となります。便利ですね。あとはApiBoxの各メンバーを淡々と並べていけばOKです。

NULLなカラムに対するINNER JOIN

この節では、INNER JOINのバリエーションについて扱います。先の例では、INNER JOINの結合先が「NULL不可」なカラムでしたが、NULL可なカラムに対してINNER JOINしたくなる場合もあります。前回の記事で出した例では、BoxテーブルにあるfolderIdが「NULL可」のカラムとしています。この状況で「Boxテーブルをselectしつつ、Folderのデータもとってくる」とする場合の書き方を説明します。

 box_list <- select $ from $ \(b `InnerJoin` f) -> do
    on $ b ^. BoxFolderId ==. just (f ^. FolderId)
    where_ $ b ^. BoxName ==. val "Box-1-2"
    return (b, f)

NULL不可のカラムに対するINNER JOINでの実装との違いは、on句のところにjustが入っていることです。on句にある「b ^. BoxFolerId」の型は「Maybe FolderId」なのに対して、「f ^. FolderId」の型は「FolderId」なため、これをMaybe型にしてやる必要があります。just関数はこのようにMaybeの型をあわせるために使います。もしjustを入れ忘れた場合には、コンパイル時に「型が合わないよ!」エラーとなりますので、エラーメッセージに対応して修正すればOKです。うっかりしていても、実行時エラーが起こるようなことにならないのが、Haskellのいいところです。

この実装で生成されるSQLは下記のようになります。on句のMaybe aにまつわる型合わせの箇所は、特にSQLには表れず、Haskell実装部分としての処理となります。

SELECT `box`.`id`, `box`.`name`, `box`.`account_id`, `box`.`folder_id`, `box`.`start_time`,
`box`.`end_time`, `folder`.`id`, `folder`.`name`, `folder`.`start_time`, `folder`.`end_time`
FROM `box` INNER JOIN `folder` ON `box`.`folder_id` = `folder`.`id`
WHERE `box`.`name` = ?
; [PersistText "Box-1-2"]

返ってくる値はこんな感じです。Haskellで「return (b, f)」としているので、Entity BoxとEntity Folderのタプルが返ってきているのがわかるかと思います。INNER JOINなので、Entity FolderもMaybe型ではありません。

[(Entity {entityKey = BoxKey {unBoxKey = SqlBackendKey {unSqlBackendKey = 2}}
, entityVal = Box {boxName = "Box-1-2", boxAccountId = AccountKey 
{unAccountKey = SqlBackendKey {unSqlBackendKey = 1}}
, boxFolderId = Just (FolderKey {unFolderKey = SqlBackendKey {unSqlBackendKey = 1}})
, boxStartTime = 2018-12-16 22:57:13 UTC, boxEndTime = Nothing}}
, Entity {entityKey = FolderKey {unFolderKey = SqlBackendKey {unSqlBackendKey = 1}}
, entityVal = Folder {folderName = "Group-1", folderStartTime = 2018-12-16 22:57:08 UTC
, folderEndTime = Nothing}})]

この話はEsqueletoというよりSQLの話題なんですが、JOIN先のテーブルのレコードがないようなSQLを作成すると、戻り値自体がなくなります。例えば、box.id=1のレコードはfolder_idがnullであるため(前回の記事での前提を参照ください)、このレコードを検索対象とするような下記のHaskell実装の場合、

 box_list <- select $ from $ \(b `InnerJoin` f) -> do
    on $ b ^. BoxFolderId ==. just (f ^. FolderId)
    where_ $ b ^. BoxName ==. val "Box-1-1"
    return (b, f)

生成されるSQLは基本的には同じですが、戻り値は

[]

このように空リストとなります。単純にこのようなSQLを実行すると「Empty set」と返ってくるだけなので、「SQLとしてどういう処理をしたいのか」という側の問題となります。

まとめ

INNER JOINはOUTER JOINに比べてシンプルなので、この記事で終わりかな...と思っていたら、3テーブルJOINの話を忘れていました。これはこれで要注意な点があるので、次の記事では3テーブルJOINの話を書きます。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away