DEV Community

loading...

Haskell: ต่อ MySQL ด้วย persistent library

Weerasak Chongnguluam
Software Developer/Love to code/Teaching to code
・2 min read

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

ค้นๆดูแล้วนั้น Haskell ใช้ library ที่ชื่อว่า persistent ซึ่งตัวนี้ทำหน้าที่สร้าง datatype ที่ map กับ table ให้ด้วยผ่านฟีเจอร์ของ GHC (compiler haskell ที่รองรับฟีเจอร์มากกว่า standard กำหนดผ่านทาง language extension) ที่ชื่อว่า TemplateHaskell

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

{-# 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
Enter fullscreen mode Exit fullscreen mode

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

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

share [mkPersist sqlSettings] [persistLowerCase|
Person
    name String
    age Int Maybe
    deriving Show
|]
Enter fullscreen mode Exit fullscreen mode

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

data Person
  = Person {personName :: !String, personAge :: !(Maybe Int)}
Enter fullscreen mode Exit fullscreen mode

ซึ่งมันจะไป 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    |                |
+-------+--------------+------+-----+---------+----------------+
Enter fullscreen mode Exit fullscreen mode

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

listPerson :: MonadIO m => SqlPersistT m [Entity Person]
listPerson = selectList [] []
Enter fullscreen mode Exit fullscreen mode

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

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

conn :: ConnectInfo
conn = defaultConnectInfo {
    connectPassword = "password",
    connectHost = "0.0.0.0",
    connectDatabase = "learning_sql"
  }
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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

ขอฝาก Buy Me a Coffee

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

Buy Me A Coffee

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

Discussion (0)