1711-Games / Entita2

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Entita2

Entita2 is a simple ORM for working with NoSQL DBs. This particular package contains basic abstractions (without any particular DB connections). Currently the only concrete DB implementation is Entita2FDB which works with FoundationDB (a highly distributed transactional NoSQL DB by Apple).

Entity definition

import struct Foundation
import Entita2

let storage: SomeStorage = someStorageReference

final class User: E2Entity {
    public typealias Identifier = E2.UUID

    public static var format: E2.Format = .JSON
    public static var IDKey: KeyPath<User, E2.UUID> = \.ID
    public static var storage: some Entita2Storage = storage

    public let ID: E2.UUID

    public var username: String
    public var password: String
    public var email: String
    public var dateSignup: Date
    public var dateLogin: Date?

    public init(
        username: String,
        password: String,
        email: String,
        dateSignup: Date = Date(),
        dateLogin: Date? = nil
    ) {
        self.ID = .init()
        self.username = username
        self.password = password
        self.email = email
        self.dateSignup = dateSignup
        self.dateLogin = dateLogin
    }
}

NB: Every Entita2-prefixed definition has a E2-prefixed typealias: Entita2 >> E2, Entita2Entity >> E2Entity etc.

This snippet defines an entity User with an UUID identifier (identifiers can be anything, including Int, of course) and a few more properties. Under the hood Entita2 utilizes Codable protocol, so every property must conform to it. The entity is packed using JSON format (other option is MessagePack). ID property can be named anything, and therefore a KeyPath should be provided to this property.

CRUD

Loading a record from DB:

try await User.load(by: E2.UUID("9C0FDD1C-FE56-4598-A037-177362DBD3D2")!)

Creating a new record:

let newUser = User(
    username: "17:11 Teo",
    password: "706c656173652073656e642068656c70".hashedOfCourse,
    email: "teo@1711.games",
    dateSignup: Date()
)
try await newUser.insert(storage: storageInstance)

Updating an existing record:

user.dateLogin = Date()

try await user.save()

Deleting a record:

try await user.delete()

Pre/after operation hooks

If you want to perform some actions before or after any CRUD operation, you may define a method of a following signature in your entity:

func afterLoad(within transaction: AnyTransaction?) async throws

There are seven methods of such kind (including afterLoad, but not beforeLoad, because it's nonsense):

beforeInsert
afterInsert

beforeSave
afterSave

beforeDelete
afterDelete

You may define any of theese methods, but you should not execute them manualy.

Additionally, there are siblings of these methods with 0 suffix (beforeInsert0 etc), which are for Entita2 extensions like Entita2FDB, you should not define (nor execute) them in your entities.

Order of execution of these methods is as follows:

beforeInsert0
beforeInsert
save0 // actual IO operation
afterInsert0
afterInsert

Almost all CRUD methods also have 0-suffix siblings: insert0, save0, delete0 which schedule IO operations and communicate with storage. Those methods should not be defined or executed directly, unless you work on a new DB implementation, and there is no other way.

Storage

If you want to use Entita2 with your custom storage, your storage class have to adopt Entita2Storage protocol. It requires four methods:

/// Begins a transaction if `Storage` is transactional
func begin() async throws -> AnyTransaction

/// Tries to load bytes from storage for given key within a transaction
func load(by key: Bytes, within transaction: AnyTransaction?) async throws -> Bytes?

/// Saves given bytes at given key within a transaction
func save(
    bytes: Bytes,
    by key: Bytes,
    within transaction: AnyTransaction?
) async throws

/// Deletes a given key (and value) from storage within a transaction
func delete(by key: Bytes, within transaction: AnyTransaction?) async throws

AnyTransaction is a protocol for a transaction, it has to have just one method:

func commit() async throws

If your DB is not transactional, create a dummy commit method that would do nothing.

Transactions

If your DB is transactional, you might want to utilize transactions. First you create a transaction with storage.begin(), then you pass it to every CRUD method (otherwise transaction will be started automatically for any CRUD operation). The rest is up to DB.

FAQ

Entita2? Where is Entita1 then?

There is indeed a package called Entita (without a number) with similar functionality which is heavily used in LGNC engine. However, it's quite low-level and is probably not of much use for broad public.

Why is Storage injected into static class instead of passing to individual dynamic CRUD methods?

We have considered this [Fluent-inspired] approach, but upon thorough production testing we decided that it is too cumbersome. We understand that community prefers more safe approach with dependency container being passed (like request in Vapor) to each request, but we weren't aiming for a specific framework while developing this ORM (actually we were at some point, but we've committed some serious efforts to make it framework-agnostic).

About

License:MIT License


Languages

Language:Swift 100.0%