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).
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.
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: "[email protected]",
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()
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.
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.
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.
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.
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).