Haskell: ต่อ MySQL ด้วย persistent library
Tags: haskell
Published: 2021-05-11 01:57:34

คุยกับเพื่อนๆในกลุ่ม Rust Telegram แล้วอยากเห็นว่า Haskell นั่นต่อ DB ยังไงเมื่อเทียบกับ Rust ที่ใช้ proc macro ช่วยให้โค้ดดูง่ายขึ้น

ค้นๆดูแล้วนั้น Haskell ใช้ library ที่ชื่อว่า [persistent](https://www.stackage.org/haddock/lts-17.11/persistent-2.11.0.4) ซึ่งตัวนี้ทำหน้าที่สร้าง datatype ที่ map กับ table ให้ด้วยผ่านฟีเจอร์ของ [GHC](https://www.haskell.org/ghc/) (compiler haskell ที่รองรับฟีเจอร์มากกว่า standard กำหนดผ่านทาง language extension) ที่ชื่อว่า TemplateHaskell

ลองเขียนต่อ MySQL เพื่อ query ง่ายๆออกมาได้แบบนี้

```haskell
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
module Main where

import           Database.Persist
import           Database.Persist.TH
import           Database.Persist.Sql
import           Control.Monad.IO.Class
import           Control.Monad.Reader
import           Database.Persist.MySQL
import           Control.Monad.Logger

share [mkPersist sqlSettings] [persistLowerCase|
Person
    name String
    age Int Maybe
    deriving Show
|]

listPerson :: MonadIO m => SqlPersistT m [Entity Person]
listPerson = selectList [] []

conn :: ConnectInfo
conn = defaultConnectInfo {
    connectPassword = "password",
    connectHost = "0.0.0.0",
    connectDatabase = "learning_sql"
  }

main :: IO ()
main = runStderrLoggingT $ withMySQLPool conn 1 $ runSqlPool $ do
  me <- (entityVal.head) <$> listPerson
  p $ "Name: " <> (personName me)
  case personAge me of
    Just age -> p $ "Age: " <> (show age)
    _ -> return ()
  where
    p = liftIO . putStrLn
```

จะเห็นว่าต้อง enable language extension เยอะพอสมควรเลย :D 

โค้ดตรงที่ใช้ TemplateHaskell ช่วยสร้าง datatype ให้เองคือตรงนี้

```haskell
share [mkPersist sqlSettings] [persistLowerCase|
Person
    name String
    age Int Maybe
    deriving Show
|]
```

จะสร้าง datatype แบบนี้ให้

```haskell
data Person
  = Person {personName :: !String, personAge :: !(Maybe Int)}
```

ซึ่งมันจะไป map กับ table `person` ที่มี columns แบบนี้

```
MariaDB [learning_sql]> desc person;
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | int(11)      | NO   | PRI | NULL    | auto_increment |
| name  | varchar(255) | YES  |     | NULL    |                |
| age   | int(11)      | YES  |     | NULL    |                |
+-------+--------------+------+-----+---------+----------------+
```

ส่วนที่เขียน query คือฟังก์ชัน `listPerson`

```haskell
listPerson :: MonadIO m => SqlPersistT m [Entity Person]
listPerson = selectList [] []
```

ซึ่ง context ของคำสั่งในการ query ต่างๆจะอยู่ภายใต้ reader monad transformer `SqlPersistT m`

สำหรับ connection เราใส่รายละเอียดใน type `ConnectInfo` โดยเอาค่าจาก `defaultConnectInfo` มาปรับนิดหน่อย

```haskell
conn :: ConnectInfo
conn = defaultConnectInfo {
    connectPassword = "password",
    connectHost = "0.0.0.0",
    connectDatabase = "learning_sql"
  }
```

ส่วนใน main ก็แค่ลองเรียกแล้วเอาค่าแรกออกมาดู เราก็เขียนโค้ด query ต่างๆใน context ของ `SqlPersistT m` เสร็จแล้วพอจะประกอบร่างก็ต้อง runwrap monad transformer กันก่อน โดยใช้ runSqlPool จากนั้นเราก็เรียก `withMySQLPool` เพื่อสร้าง connection ไปรับ query สุดท้ายเนื่องจาก `withMySQLPool` ได้ผลลัพธ์ออกมาเป็น `MonadLogger` เราก็เลยต้องใช้ `runStderrLoggingT` เพื่อเลือกให้มัน log ค่าต่างๆออก standard error พร้อมกับ unwrap ค่าที่ได้ออกมาเป็น IO ปกติเพื่อกำหนดให้เป็น main ฟังก์ชันได้

```haskell
main :: IO ()
main = runStderrLoggingT $ withMySQLPool conn 1 $ runSqlPool $ do
  me <- (entityVal.head) <$> listPerson
  p $ "Name: " <> (personName me)
  case personAge me of
    Just age -> p $ "Age: " <> (show age)
    _ -> return ()
  where
    p = liftIO . putStrLn
```

โค้ดก็ประมาณนี้ ใครไม่เคยเขียน Haskell หรือไม่เข้าใจ Monad Transformer มาก่อนอาจจะงงๆ แต่ใช้งานจริงๆก็ไม่ยากมาก ตรง TemplateHaskell ช่วยได้เยอะเลยในการลดโค้ดที่ต้องเขียนเองสำหรับการแปลงไปมาระหว่าง column กับ datatype ใน Haskell

## ขอฝาก Buy Me a Coffee
สำหรับท่านใดที่อ่านแล้วชอบโพสต์ต่างๆของผมที่นี่ ต้องการสนับสนุนค่ากาแฟเล็กๆน้อยๆ สามารถสนับสนุนผมได้ผ่านทาง Buy Me a Coffee คลิ๊กที่รูปด้านล่างนี้ได้เลยครับ

<a href="https://www.buymeacoffee.com/weerasak" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>

ส่วนท่านใดไม่สะดวกใช้บัตรเครดิต หรือ Paypal สามารถสนับสนุนผมได้ผ่านทาง PromptPay โดยดู QR Code ได้จากโพสต์ที่พินเอาไว้ได้ที่ Page DevDose ครับ https://web.facebook.com/devdoseth