Skip to main content

Repository

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()

tip

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
warning

The provided implementations are convenience helpers and assume you know what you're doing!
They are not an ORM and very simplistic on purpose!

tip

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!

repository.go
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]
}
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.

Direct use of generic repository
// 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](),
Wrap the generic repository
// 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

  1. Classical way
repo := repository.NewMemoryRepository[testdata.Entity, testdata.EntityID]()
c, _ := repo.Count(ctx)
assert.Equal(t, 0, c, "repo should be empty")
  1. Build in assertions in the default repository
repo := repository.Test[testdata.Entity, testdata.EntityID](t)
repo.Empty()
  1. Assertion helper for any / custom repositories
repo := NewMyCustomRepository[Entity, EntityID]()
rassert := repository.TestAssert[Entity, EntityID](t, repo)
rassert.Empty()
  1. 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]
}