Repository
Recommended Approach
The repository pattern is well known and there are good resources available to learn about it.
Arrower believes the repository represents actions from the domain
=> FindByLoginName()
It is strongly recommended you implement your repositories yourself in whatever technology you like!
Convenience Helpers
If you're using the repository pattern it is cumbersome to always implement an in memory copy of the repository (for testing) and the real one.
The approach arrower is taking:
- as repository has to be implemented each time there is a helper:
- the following interfaces are provided as in memory and pg implementation
- extend and overwrite them to fit them to your domain
The provided implementations are convenience helpers and
assume you know what you're doing!
They are not an ORM and very simplistic on purpose!
Use the Arrower provided repository only for simple CRUD
and throwaway prototypes.
Don't limit yourself to the methods offered out of the box and
implement your own custom method and behaviour as you need!
There is nothing more powerful than a well crafted SQL query
behind your domain focussed repository method.
As simple repositories share a repeating set of methods, Arrower offers commonly used methods ready out of the box. In general, it is good practise to keep your own repository methods to a minimum. Arrower offers a lot for your convenience, so you have a buffet to choose from, not as a recommendation to use all of them at all times!
- Entity Repository
- Tenant Repository
type Repository[E any, ID id] interface {
NextID(ctx context.Context) (ID, error)
Create(ctx context.Context, entity E) error
Read(ctx context.Context, id ID) (E, error)
Update(ctx context.Context, entity E) error
Delete(ctx context.Context, entity E) error
All(ctx context.Context) ([]E, error)
AllByIDs(ctx context.Context, ids []ID) ([]E, error)
FindAll(ctx context.Context) ([]E, error)
FindByID(ctx context.Context, id ID) (E, error)
FindByIDs(ctx context.Context, ids []ID) ([]E, error)
Exists(ctx context.Context, id ID) (bool, error)
ExistsByID(ctx context.Context, id ID) (bool, error)
ExistByIDs(ctx context.Context, ids []ID) (bool, error)
ExistAll(ctx context.Context, ids []ID) (bool, error)
Contains(ctx context.Context, id ID) (bool, error)
ContainsID(ctx context.Context, id ID) (bool, error)
ContainsIDs(ctx context.Context, ids []ID) (bool, error)
ContainsAll(ctx context.Context, ids []ID) (bool, error)
CreateAll(ctx context.Context, entities []E) error
Save(ctx context.Context, entity E) error
SaveAll(ctx context.Context, entities []E) error
UpdateAll(ctx context.Context, entities []E) error
Add(ctx context.Context, entity E) error
AddAll(ctx context.Context, entities []E) error
Count(ctx context.Context) (int, error)
Length(ctx context.Context) (int, error)
DeleteByID(ctx context.Context, id ID) error
DeleteByIDs(ctx context.Context, ids []ID) error
DeleteAll(ctx context.Context) error
Clear(ctx context.Context) error
AllIter(ctx context.Context) Iterator[E, ID]
FindAllIter(ctx context.Context) Iterator[E, ID]
}
type TenantRepository[T any, tID id, E any, eID id] interface {
NextID(ctx context.Context, tenantID tID) (eID, error)
Create(ctx context.Context, tenantID tID, entity E) error
Read(ctx context.Context, tenantID tID, id eID) (E, error)
Update(ctx context.Context, tenantID tID, entity E) error
Delete(ctx context.Context, tenantID tID, entity E) error
All(ctx context.Context) ([]E, error)
AllOfTenant(ctx context.Context, tenantID tID) ([]E, error)
AllByIDs(ctx context.Context, tenantID tID, ids []eID) ([]E, error)
FindAll(ctx context.Context) ([]E, error)
FindAllOfTenant(ctx context.Context, tenantID tID) ([]E, error)
FindByID(ctx context.Context, tenantID tID, id eID) (E, error)
FindByIDs(ctx context.Context, tenantID tID, ids []eID) ([]E, error)
Exists(ctx context.Context, tenantID tID, id eID) (bool, error)
ExistsByID(ctx context.Context, tenantID tID, id eID) (bool, error)
ExistByIDs(ctx context.Context, tenantID tID, ids []eID) (bool, error)
ExistAll(ctx context.Context, tenantID tID, ids []eID) (bool, error)
Contains(ctx context.Context, tenantID tID, id eID) (bool, error)
ContainsID(ctx context.Context, tenantID tID, id eID) (bool, error)
ContainsIDs(ctx context.Context, tenantID tID, ids []eID) (bool, error)
ContainsAll(ctx context.Context, tenantID tID, ids []eID) (bool, error)
Save(ctx context.Context, tenantID tID, entity E) error
SaveAll(ctx context.Context, tenantID tID, entities []E) error
UpdateAll(ctx context.Context, tenantID tID, entities []E) error
Add(ctx context.Context, tenantID tID, entity E) error
AddAll(ctx context.Context, tenantID tID, entities []E) error
Count(ctx context.Context) (int, error)
CountOfTenant(ctx context.Context, tenantID tID) (int, error)
Length(ctx context.Context) (int, error)
LengthOfTenant(ctx context.Context, tenantID tID) (int, error)
Empty(ctx context.Context) (bool, error)
EmptyTenant(ctx context.Context, tenantID tID) (bool, error)
IsEmpty(ctx context.Context) (bool, error)
IsEmptyTenant(ctx context.Context, tenantID tID) (bool, error)
DeleteByID(ctx context.Context, tenantID tID, id eID) error
DeleteByIDs(ctx context.Context, tenantID tID, ids []eID) error
DeleteAll(ctx context.Context) error
DeleteAllOfTenant(ctx context.Context, tenantID tID) error
Clear(ctx context.Context) error
ClearTenant(ctx context.Context, tenantID tID) error
}
var repo YourRepositoryType = repository.NewMemoryRepository[Entity, EntityID]()
It is implicitly assumed that the entity has a field named ID
with an underlying type of string
or int
.
That field will be used as the primary key.
You can change the field name:
repo := repository.NewMemoryRepository[E, I](
repository.WithIDField("YourPKField"),
)
The repository will probably not match all your needs, see how to extend and overwrite or fine tune it, so it fits all your applications needs.
// define the repository in the domain
type UserRepository interface {
Save(ctx context.Context, user User) error
FindByID(ctx context.Context, id UserID) (User, error)
Delete(ctx context.Context, user User) error
}
// usage in your application
var repo UserRepository = repository.NewMemoryRepository[User, UserID](),
// define the repository in the domain
type UserRepository interface {
Save(ctx context.Context, user User) error
FindByID(ctx context.Context, id UserID) (User, error)
Delete(ctx context.Context, user User) error
}
// implement the repository in the interfaces layer
func NewInMemoryUserRepository() *InMemoryUserRepository {
return &InMemoryUserRepository{
MemoryRepository: repository.NewMemoryRepository[User, UserID](),
}
}
var _ UserRepository = (*InMemoryUserRepository)(nil)
type InMemoryUserRepository struct {
*repository.MemoryRepository[User, UserID]
}
// usage in your application
var repo UserRepository = NewInMemoryUserRepository()
Testing
- Classical way
repo := repository.NewMemoryRepository[testdata.Entity, testdata.EntityID]()
c, _ := repo.Count(ctx)
assert.Equal(t, 0, c, "repo should be empty")
- Build in assertions in the default repository
repo := repository.Test[testdata.Entity, testdata.EntityID](t)
repo.Empty()
- Assertion helper for any / custom repositories
repo := NewMyCustomRepository[Entity, EntityID]()
rassert := repository.TestAssert[Entity, EntityID](t, repo)
rassert.Empty()
- Embed assertions into custom repository
repo := NewMyCustomTestRepository(t)
repo.Emtpy()
func NewMyCustomTestRepository(t *testing.T) *MyCustomRepository {
repo := repository.NewMemoryRepository[Entity, EntityID]()
return &MyCustomRepository{
MemoryRepository: repo,
TestAssertions: repository.TestAssert(t, repo),
}
}
type MyCustomRepository struct {
*repository.MemoryRepository[Entity, EntityID]
*repository.TestAssertions[Entity, EntityID]
}