使用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