使用Haskell';处理数据完整性的最佳实践是什么;什么是持久库?

使用Haskell';处理数据完整性的最佳实践是什么;什么是持久库?,haskell,persistent,data-integrity,Haskell,Persistent,Data Integrity,假设我的应用程序中有一个处理用户的基本模型: User username Text password Text email Text 基于这些类型,我可以将我喜欢的任何内容(只要是Text)存储到这些字段中的任何一个。比如说,我想开始实施一些关于哪些可以和哪些不能被视为电子邮件的规则。我创建此电子邮件模块: {-# LANGUAGE GeneralizedNewtypeDeriving #-} module Model.EmailAddress ( EmailAddress

假设我的应用程序中有一个处理用户的基本模型:

User
  username Text
  password Text
  email Text
基于这些类型,我可以将我喜欢的任何内容(只要是
Text
)存储到这些字段中的任何一个。比如说,我想开始实施一些关于哪些可以和哪些不能被视为电子邮件的规则。我创建此电子邮件模块:

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Model.EmailAddress
  ( EmailAddress
  , parseEmailAddress
  ) where

import           Data.Aeson
import           Data.Text (Text)
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TL
import           Database.Persist
import           Database.Persist.Sql
import           Text.Shakespeare.Text (ToText (..))

newtype EmailAddress = EmailAddress (Either InvalidEmailAddress ValidEmailAddress)
  deriving (Show, Eq)

instance ToJSON EmailAddress where
  toJSON = String . toStrictText

instance FromJSON EmailAddress where
  parseJSON = withText "EmailAddress" (pure . parseEmailAddress)

instance ToText EmailAddress where
  toText (EmailAddress e) = either toText toText e

newtype ValidEmailAddress = ValidEmailAddress Text
  deriving (Show, Eq)

instance ToText ValidEmailAddress where
  toText (ValidEmailAddress t) = toText t

data InvalidEmailAddress = InvalidEmailAddress
  { invalidEmailAddressValue :: Text
  , invalidEmailAddressReason :: Text
  } deriving (Show, Eq)

instance ToText InvalidEmailAddress where
  toText = toText . invalidEmailAddressValue

parseEmailAddress :: Text -> EmailAddress
parseEmailAddress t = 
  -- super basic validation example
  EmailAddress $ case T.count "@" t of
    0 -> Left (InvalidEmailAddress t "Must contain @ symbol")
    1 -> Right (ValidEmailAddress t)
    _ -> Left (InvalidEmailAddress t "Contains more than one @ symbol")

instance PersistFieldSql EmailAddress where
   sqlType _ = SqlString

instance PersistField EmailAddress where
  fromPersistValue =
    decodePersistTextValue parseEmailAddress

  toPersistValue =
    PersistText . toStrictText

decodePersistTextValue :: PersistField a => (Text -> a) -> PersistValue -> Either Text a
decodePersistTextValue f = \case
  PersistByteString bs ->
    fromPersistValue $ PersistText (T.decodeUtf8 bs)
  v ->
    f <$> fromPersistValue v

toStrictText :: ToText t => t -> Text
toStrictText = builderToStrictText . toText

builderToStrictText :: TL.Builder -> Text
builderToStrictText = TL.toStrict . TL.toLazyText
但是,这并不能阻止我向数据库写入错误的电子邮件地址。如果构成有效电子邮件地址的规则发生变化,我仍然可以从数据库中读取它们,以前有效的电子邮件将以无效的形式从数据库中出来,这很好,我喜欢这样。但是,我无法强制执行(无论如何在持久级别)防止向数据库写入无效电子邮件,因为
toPersistValue
的类型为
a->PersistValue
,我想我必须强制执行这一层(当数据进入应用程序、表单解码器、json解码器等时)要确保出现
左无效邮件地址
时,用户会收到错误信息

这让我想到,这是在使用持久化时强制执行数据完整性的最佳实践吗?有更好的方法吗?难道我们根本就不应该参与吗?ie该字段保持为
文本
,在访问这些字段时,是否由应用程序转换这些字段


我真的很想以一种类型安全的方式将我的实体写入数据库,允许我确定在使用无效电子邮件尝试保存我的
用户
类型的情况下该怎么做,我可以在
toPersistValue
实现中抛出一个
错误
,但这对我来说有点脏,而且不是很Haskell'y。

我想说,使用Persistent的模板Haskell系统已经不是很Haskell-y了,因为您已经放弃了对类型定义的控制。据我所知,您不能在模板系统之外声明
User
type,然后将其传递给Persistent以自动执行所有需要的类型类。我认为这是一种权衡。它很方便,因为它为您生成了很多东西,但您放弃了一定数量的控制。我不确定这是否真的会有什么不同,您仍然需要定义自己的
PersistField
实例,这会遇到同样的问题。我自己也不是TH的最大粉丝,但我不认为这是问题所在。我想我只是想让你不会因为做了一些非Haskell-y的事情而感到难过,呵呵,但是如果我理解这个问题,我认为有一个好的解决方案。您可以定义一个
EmailAddress
类型,它只是
Text
上的一个新类型包装,隐藏构造函数,然后导出一个自定义构造函数
mkEmail::Text->可能是EmailAddress
,该构造函数负责确保它是有效的电子邮件。然后,您可以确保
EmailAddress
的所有值都是有效的电子邮件,您不必担心它在PersistValue中的传入和传出。我要说的是,使用persist的模板haskell系统已经不是很haskell-y了,因为您已经放弃了对类型定义的控制。据我所知,您不能在模板系统之外声明
User
type,然后将其传递给Persistent以自动执行所有需要的类型类。我认为这是一种权衡。它很方便,因为它为您生成了很多东西,但您放弃了一定数量的控制。我不确定这是否真的会有什么不同,您仍然需要定义自己的
PersistField
实例,这会遇到同样的问题。我自己也不是TH的最大粉丝,但我不认为这是问题所在。我想我只是想让你不会因为做了一些非Haskell-y的事情而感到难过,呵呵,但是如果我理解这个问题,我认为有一个好的解决方案。您可以定义一个
EmailAddress
类型,它只是
Text
上的一个新类型包装,隐藏构造函数,然后导出一个自定义构造函数
mkEmail::Text->可能是EmailAddress
,该构造函数负责确保它是有效的电子邮件。然后,您可以确保
EmailAddress
的所有值都是有效的电子邮件,并且您不必担心它在值的往返中。
User
  username Text
  password Text
  email EmailAddress