32

Tôi thấy rất phổ biến khi muốn mô hình hóa dữ liệu quan hệ trong các chương trình chức năng của mình. Ví dụ, khi phát triển một trang web tôi có thể muốn có cấu trúc dữ liệu sau đây để lưu trữ thông tin về người dùng của tôi:Mô hình hóa an toàn dữ liệu quan hệ trong Haskell

data User = User 
    { name :: String 
    , birthDate :: Date 
    } 

Tiếp theo, tôi muốn để lưu trữ dữ liệu về thư mà người dùng đăng trên trang web của tôi:

data Message = Message 
    { user :: User 
    , timestamp :: Date 
    , content :: String 
    } 

có nhiều vấn đề liên quan đến cấu trúc dữ liệu này:

  • Chúng tôi không có bất kỳ cách nào phân biệt người dùng với tên tương tự và ngày sinh.
  • Dữ liệu người dùng sẽ được sao chép khi tuần tự hóa/deserialisation
  • So sánh người dùng yêu cầu so sánh dữ liệu của họ có thể là một hoạt động tốn kém.
  • Nội dung cập nhật cho các trường của User là mong manh - bạn có thể quên cập nhật tất cả các lần xuất hiện của User trong cấu trúc dữ liệu của bạn.

Các vấn đề này có thể quản lý được trong khi dữ liệu của chúng tôi có thể được biểu diễn dưới dạng cây. Ví dụ, bạn có thể cấu trúc như thế này:

data User = User 
    { name :: String 
    , birthDate :: Date 
    , messages :: [(String, Date)] -- you get the idea 
    } 

Tuy nhiên, nó có thể có dữ liệu của bạn có hình dạng như một DAG (tưởng tượng bất kỳ mối quan hệ nhiều-nhiều), hoặc thậm chí là một đồ thị nói chung (OK, có lẽ không phải). Trong trường hợp này, tôi có xu hướng để mô phỏng các cơ sở dữ liệu quan hệ bằng cách lưu trữ dữ liệu của tôi trong Map s:

newtype Id a = Id Integer 
type Table a = Map (Id a) a 

Đây là loại công trình, nhưng không an toàn và xấu xí cho nhiều lý do:

  • Bạn chỉ là một Id constructor gọi đi từ tra cứu vô nghĩa.
  • Khi tra cứu bạn nhận được Maybe a, nhưng thường cơ sở dữ liệu có cấu trúc đảm bảo rằng có giá trị.
  • Đó là vụng về.
  • Thật khó để đảm bảo tính toàn vẹn tham chiếu của dữ liệu của bạn.
  • Quản lý các chỉ mục (rất cần thiết cho hiệu suất) và đảm bảo tính toàn vẹn của chúng thậm chí còn khó hơn và vụng về hơn.

Hiện có công việc khắc phục những sự cố này không?

Dường như mẫu Haskell có thể giải quyết chúng (như thường lệ), nhưng tôi không muốn phát minh lại bánh xe.

Trả lời

21

Thư viện ixset sẽ giúp bạn với điều này. Đó là thư viện sao lưu phần quan hệ của acid-state, cũng xử lý việc tuần tự hóa phiên bản dữ liệu của bạn và/hoặc đảm bảo đồng thời, trong trường hợp bạn cần.

Điều về ixset là nó tự động quản lý "khóa" cho mục nhập dữ liệu của bạn.

Ví dụ của bạn, người ta sẽ tạo ra một-nhiều mối quan hệ với nhiều loại dữ liệu của bạn như thế này:

data User = 
    User 
    { name :: String 
    , birthDate :: Date 
    } deriving (Ord, Typeable) 

data Message = 
    Message 
    { user :: User 
    , timestamp :: Date 
    , content :: String 
    } deriving (Ord, Typeable) 

instance Indexable Message where 
    empty = ixSet [ ixGen (Proxy :: Proxy User) ] 

Sau đó bạn có thể tìm thấy những thông điệp của một người dùng cụ thể. Nếu bạn đã xây dựng lên một IxSet như thế này:

user1 = User "John Doe" undefined 
user2 = User "John Smith" undefined 

messageSet = 
    foldr insert empty 
    [ Message user1 undefined "bla" 
    , Message user2 undefined "blu" 
    ] 

... sau đó bạn có thể tìm thấy các thông điệp bằng user1 với:

user1Messages = toList $ messageSet @= user1 

Nếu bạn cần phải tìm người dùng của một tin nhắn, chỉ cần sử dụng user hoạt động như bình thường. Mô hình này là mối quan hệ một-nhiều.

Bây giờ, đối với nhiều-nhiều mối quan hệ, với một tình huống như thế này:

data User = 
    User 
    { name :: String 
    , birthDate :: Date 
    , messages :: [Message] 
    } deriving (Ord, Typeable) 

data Message = 
    Message 
    { users :: [User] 
    , timestamp :: Date 
    , content :: String 
    } deriving (Ord, Typeable) 

... bạn tạo một chỉ mục với ixFun, có thể được sử dụng với danh sách các chỉ mục.Cũng giống như vậy:

instance Indexable Message where 
    empty = ixSet [ ixFun users ] 

instance Indexable User where 
    empty = ixSet [ ixFun messages ] 

Để tìm tất cả các tin nhắn của một người khác, bạn vẫn sử dụng các chức năng tương tự:

user1Messages = toList $ messageSet @= user1 

Bên cạnh đó, với điều kiện bạn có một chỉ số của người sử dụng:

userSet = 
    foldr insert empty 
    [ User "John Doe" undefined [ messageFoo, messageBar ] 
    , User "John Smith" undefined [ messageBar ] 
    ] 

... bạn có thể tìm tất cả người dùng cho một tin nhắn:

messageFooUsers = toList $ userSet @= messageFoo 

Nếu bạn không muốn cập nhật người dùng thư hoặc tin nhắn của người dùng khi thêm người dùng/thư mới, bạn nên tạo loại dữ liệu trung gian để mô hình mối quan hệ giữa người dùng và thư, giống như trong SQL (và loại bỏ các lĩnh vực usersmessages):

data UserMessage = UserMessage { umUser :: User, umMessage :: Message } 

instance Indexable UserMessage where 
    empty = ixSet [ ixGen (Proxy :: Proxy User), ixGen (Proxy :: Proxy Message) ] 

Tạo một bộ các mối quan hệ sau đó sẽ cho phép bạn truy vấn cho người dùng bằng tin nhắn và các thông báo cho người dùng mà không cần phải cập nhật bất cứ điều gì.

Thư viện có giao diện rất đơn giản xem xét giao diện của nó!

CHỈNH SỬA: Về "dữ liệu chi phí cần so sánh": ixset chỉ so sánh các trường bạn chỉ định trong chỉ mục của mình (để tìm tất cả thư của người dùng trong ví dụ đầu tiên, nó so sánh " toàn bộ người dùng ").

Bạn điều chỉnh những phần nào của trường được lập chỉ mục mà nó so sánh bằng cách thay đổi cá thể Ord. Vì vậy, nếu so sánh người dùng là tốn kém cho bạn, bạn có thể thêm trường userId và sửa đổi instance Ord User để chỉ so sánh trường này, chẳng hạn.

Điều này cũng có thể được sử dụng để giải quyết vấn đề về trứng và trứng: nếu bạn có id, nhưng không phải là số User, cũng không phải là Message?

Sau đó bạn có thể chỉ cần tạo chỉ mục rõ ràng cho id, tìm người dùng theo id đó (với userSet @= (12423 :: Id)) và sau đó thực hiện tìm kiếm.

+0

Không những one-to-many mô hình đưa ra ở đây chia sẻ tất cả những nhược điểm của mô hình ban đầu từ câu hỏi? – ehird

+0

@ehird, tôi thực hiện khoảng 10 lần chỉnh sửa mỗi phút, vì vậy tôi nghĩ rằng tôi đã trả lời mối quan tâm của bạn trên đường đi. – dflemstr

+0

Vâng, thực sự. (BTW, trạng thái axit không thực sự phụ thuộc vào ixset; chúng chỉ được thiết kế để sử dụng cùng nhau.) – ehird

3

Tôi không có giải pháp hoàn chỉnh, nhưng tôi khuyên bạn nên xem gói ixset; nó cung cấp một loại thiết lập với một số tùy ý các chỉ mục mà tra cứu có thể được thực hiện với. (Nó được thiết kế để sử dụng với acid-state cho sự bền bỉ.)

Bạn vẫn cần phải tự duy trì một "khóa chính" cho mỗi bảng, nhưng bạn có thể làm cho nó dễ dàng hơn đáng kể trong một vài cách sau:

  1. Thêm một tham số kiểu để Id, do đó, ví dụ: User chứa Id User thay vì chỉ là Id. Điều này đảm bảo bạn không trộn lẫn Id giây cho các loại riêng biệt.

  2. Lập Id loại trừu tượng, và cung cấp một giao diện an toàn để tạo ra những cái mới trong một số ngữ cảnh (giống như một đơn nguyên State mà theo dõi những liên quan IxSet và dòng điện cao nhất Id).

  3. Viết chức năng wrapper cho phép bạn, ví dụ, cung cấp một User nơi một Id User dự kiến ​​trong các truy vấn, và điều đó thực thi bất biến (ví dụ, nếu mỗi Message giữ một chìa khóa cho một giá trị User, nó có thể cho phép bạn tra cứu User tương ứng mà không cần xử lý giá trị Maybe; "unsafety" được chứa trong hàm trợ giúp này).

Lưu ý bổ sung, bạn thực sự không cần cấu trúc cây cho các loại dữ liệu thông thường để hoạt động vì chúng có thể biểu thị các đồ thị tùy ý; tuy nhiên, điều này làm cho các hoạt động đơn giản như không thể cập nhật tên người dùng.

5

Một cách tiếp cận hoàn toàn khác để biểu diễn dữ liệu quan hệ được sử dụng bởi gói cơ sở dữ liệu haskelldb. Nó không hoạt động giống như các kiểu bạn mô tả trong ví dụ của bạn, nhưng nó được thiết kế để cho phép một giao diện kiểu an toàn cho các truy vấn SQL. Nó có các công cụ để tạo các kiểu dữ liệu từ lược đồ cơ sở dữ liệu và ngược lại. Các kiểu dữ liệu như các kiểu dữ liệu bạn mô tả hoạt động tốt nếu bạn luôn muốn làm việc với toàn bộ các hàng. Nhưng chúng không hoạt động trong các tình huống mà bạn muốn tối ưu hóa truy vấn của mình bằng cách chỉ chọn các cột nhất định. Đây là cách tiếp cận HaskellDB có thể hữu ích.

+0

Ngày nay tôi sẽ đề nghị Opaleye thay vì HaskellDB (http://hackage.haskell.org/package/opaleye) nhưng Tôi thiên vị vì tôi đã viết nó :) –

+0

@TomEllis: Bạn có cân nhắc viết câu trả lời ngắn gọn cho câu hỏi này bằng cách sử dụng Opaleye không? –

+0

Chắc chắn, thông tin cho bạn đây: http://stackoverflow.com/a/28307896/997606 –

5

IxSet là vé. Để giúp những người khác có thể vấp ngã trên bài đăng này dưới đây là ví dụ được thể hiện đầy đủ hơn,

{-# LANGUAGE OverloadedStrings, DeriveDataTypeable, TypeFamilies, TemplateHaskell #-} 

module Main (main) where 

import Data.Int 
import Data.Data 
import Data.IxSet 
import Data.Typeable 

-- use newtype for everything on which you want to query; 
-- IxSet only distinguishes indexes by type 
data User = User 
    { userId :: UserId 
    , userName :: UserName } 
    deriving (Eq, Typeable, Show, Data) 
newtype UserId = UserId Int64 
    deriving (Eq, Ord, Typeable, Show, Data) 
newtype UserName = UserName String 
    deriving (Eq, Ord, Typeable, Show, Data) 

-- define the indexes, each of a distinct type 
instance Indexable User where 
    empty = ixSet 
     [ ixFun $ \ u -> [userId u] 
     , ixFun $ \ u -> [userName u] 
     ] 

-- this effectively defines userId as the PK 
instance Ord User where 
    compare p q = compare (userId p) (userId q) 

-- make a user set 
userSet :: IxSet User 
userSet = foldr insert empty $ fmap (\ (i,n) -> User (UserId i) (UserName n)) $ 
    zip [1..] ["Bob", "Carol", "Ted", "Alice"] 

main :: IO() 
main = do 
    -- Here, it's obvious why IxSet needs distinct types. 
    showMe "user 1" $ userSet @= (UserId 1) 
    showMe "user Carol" $ userSet @= (UserName "Carol") 
    showMe "users with ids > 2" $ userSet @> (UserId 2) 
    where 
    showMe :: (Show a, Ord a) => String -> IxSet a -> IO() 
    showMe msg items = do 
    putStr $ "-- " ++ msg 
    let xs = toList items 
    putStrLn $ " [" ++ (show $ length xs) ++ "]" 
    sequence_ $ fmap (putStrLn . show) xs 
5

Tôi đã được yêu cầu viết câu trả lời bằng Opaleye. Trong thực tế, không có nhiều điều để nói, vì mã Opaleye khá chuẩn khi bạn có một lược đồ cơ sở dữ liệu. Dù sao, ở đây là, giả sử có một user_table với các cột user_id, namebirthdatemessage_table với các cột user_id, time_stampcontent.

Loại thiết kế này được giải thích chi tiết hơn trong the Opaleye Basic Tutorial.

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

import Opaleye 
import Data.Profunctor.Product (p2, p3) 
import Data.Profunctor.Product.TH (makeAdaptorAndInstance) 
import Control.Arrow (returnA) 

data UserId a = UserId { unUserId :: a } 
$(makeAdaptorAndInstance "pUserId" ''UserId) 

data User' a b c = User { userId :: a 
         , name  :: b 
         , birthDate :: c } 
$(makeAdaptorAndInstance "pUser" ''User') 

type User = User' (UserId (Column PGInt4)) 
        (Column PGText) 
        (Column PGDate) 

data Message' a b c = Message { user  :: a 
           , timestamp :: b 
           , content :: c } 
$(makeAdaptorAndInstance "pMessage" ''Message') 

type Message = Message' (UserId (Column PGInt4)) 
         (Column PGDate) 
         (Column PGText) 


userTable :: Table User User 
userTable = Table "user_table" (pUser User 
    { userId = pUserId (UserId (required "user_id")) 
    , name  = required "name" 
    , birthDate = required "birthdate" }) 

messageTable :: Table Message Message 
messageTable = Table "message_table" (pMessage Message 
    { user  = pUserId (UserId (required "user_id")) 
    , timestamp = required "timestamp" 
    , content = required "content" }) 

Một truy vấn ví dụ mà gia nhập bảng người dùng vào bảng tin nhắn trên các lĩnh vực user_id:

usersJoinMessages :: Query (User, Message) 
usersJoinMessages = proc() -> do 
    aUser <- queryTable userTable -<() 
    aMessage <- queryTable messageTable -<() 

    restrict -< unUserId (userId aUser) .== unUserId (user aMessage) 

    returnA -< (aUser, aMessage)