読者です 読者をやめる 読者になる 読者になる

yunomuのブログ

酒とゲームと上から目線

haskell-relational-recordとMySQLの識別子の大文字小文字

haskell-relational-record(hrr)とMySQLを使ってプログラムを書いてみようと思った。

書き方自体はこのあたり。
https://github.com/khibino/haskell-relational-record
https://github.com/krdlab/haskell-relational-record-driver-mysql

元々haskell-relational-recordはPostgreSQLのために作られているので(訂正:IBM-DB2 + ODBCで動かしていたのが最初だそうです)、MySQLから使おうとするとちょっと詰まるのかもしれない。
まずdefineTableFromDBが動かなかったんですけど、それはちょっと原因がよくわからないので保留。

そもそもの問題が適当に作られたテーブルの形式だったりするわけなんですが、おおむねこういう感じになっている。

mysql> connect myapp
Connection id:    126780
Current database: myapp
mysql> desc USER;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| NAME         | varchar(32)  | NO   | PRI | NULL    |       |
| MAIL_ADDRESS | varchar(128) | YES  |     | NULL    |       |
+--------------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

MySQLではスキーマとデータベースの区別が無いというか、同じ物を指すっぽいので、データベース名もスキーマ名も同じ"myapp"になっている。そして、テーブル名とフィールド名を大文字のスネークケース。どこの常識だこれはという感じ。まあこれはそういうものなので仕方ないものとする。

これを元にテーブル定義を作るとこうなる。defineTableFromDBが動かないので自分で定義する。

{-# LANGUAGE TemplateHaskell, MultiParamTypeClasses, FlexibleInstances #-}

module User where

import Database.HDBC.Query.TH (defineTableDefault')
import Database.Record.TH (derivingShow)
import Language.Haskell.TH
import DataSource

defineTableDefault' "myapp" "USER"
    [ ("NAME", conT ''String)
    , ("MAIL_ADDRESS", conT ''String)
    ] [derivingShow]

でき上がるテーブルとクエリの定義はこうなる。

*Main> :i USER
data USER
  = USER {nAME :: !String,
          mAILADDRESS :: !String}
     -- Defined at User.hs:10:1
instance Show USER -- Defined at User.hs:10:1
instance TableDerivable USER -- Defined at User.hs:10:1
instance ProductConstructor (String -> String -> USER)
  -- Defined at User.hs:10:1
*Main> uSER
SELECT NAME, MAIL_ADDRESS FROM MYAPP.user

とてもきもちわるい。その上この時点で既に破綻している。

  • データ型名が全部大文字で気持悪い
  • フィールド名の'_'が消えて先頭だけが小文字になっていて気持悪い
  • クエリの関数名が同様に"uSER"て何よ
  • クエリの中身のデータベース名が大文字に、テーブル名が小文字になっている

この、最後のやつが困る。他はまあ気持悪いだけなのでいいのだけど。
どう困るかというと、このクエリを発行するとそんなテーブル無いよと言われる。MySQLはテーブル名の大文字と小文字を区別することがある。

MySQLのドキュメントにはこういう事が書いてある。

5.2.2. 識別子の大文字小文字の区別
MySQL において、データベースはデータディレクトリ内のディレクトリに対応しています。データベース内の各テーブルも、データベースディレクトリ内の少なくとも 1 つ(記憶エンジンによってはそれ以上)のファイルに対応しています。トリガーもファイルに対応しています。そのため、ベースとなるオペレーティングシステムで大文字と小文字が区別される場合、データベース名とテーブル名でも大文字と小文字が区別されます。
MySQL :: MySQL 5.1 リファレンスマニュアル (オンラインヘルプ) :: 5.2.2 識別子の大文字小文字の区別

つまりMySQLのデータがWindowsファイルシステム上に置かれている場合はデータベース名やテーブル名の大文字小文字は区別しないけれど、UnixLinuxの多くのファイルシステム上に置かれていた場合は区別しますという事。なんじゃそりゃという感じだけども、これも仕方ない。

これをなんとかしてhrrを使うためには、ソースをいじる必要がある。
問題はテーブル定義と同時に作成されるクエリ関数なわけなので、それを定義している場所を探すと、tableSQLという関数が見つかる。場所はここ。
haskell-relational-record/relational-query/src/Database/Relational/Query/TH.hs at master · khibino/haskell-relational-record · GitHub

tableSQL :: String -> String -> String
tableSQL schema table = map toUpper schema ++ '.' : map toLower table

とてもわかりやすいですね。
これの"map toUpper"と"map toLower"を消してあげればいいのです。PostgreSQLでは基本的にテーブル名は小文字なのでこうなってるんだと思います。スキーマ名は忘れたけど大文字なんだっけ?

ということで、hrrはまだhackageに登録されていないので皆様各々githubからcloneして使っておられると思いますので、この辺りをいじる心理的障壁は比較的少ないんじゃないかと思います。やっちゃいましょう。branch作ってcommitしておくと本家が更新した時に追随が楽だと思います。いや検証してPull Request送れという話はもっともなんですが。

ともあれこれで一応クエリは利用可能になります。めでたしめでたし。

*Main> uSER
SELECT NAME, MAIL_ADDRESS FROM myapp.USER

ところで実はMySQLはデータベース名とテーブル名についてはなんか面倒な仕様があるんですが、フィールド名の大文字小文字は区別しないので、テーブル定義のところはフィールド名を小文字のスネークケースにすると少しだけ幸せになります。

defineTableDefault' "myapp" "USER"
    [ ("name", conT ''String)
    , ("mail_address", conT ''String)
    ] [derivingShow]

ごらんよ、焼け石に水って感じだけども。これでも普通に使えます。

*Main> :i USER
data USER
  = USER {name :: !String,
          mailAddress :: !String}
     -- Defined at User.hs:10:1
instance Show USER -- Defined at User.hs:10:1
instance TableDerivable USER -- Defined at User.hs:10:1
instance ProductConstructor (String -> String -> USER)
  -- Defined at User.hs:10:1
*Main> uSER
SELECT name, mail_address FROM myapp.USER