go-rel / go-rel/rel

Add Entity manager

Open
#278 9 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
786
Forks
61
Avg merge
4d 11h
Merged PRs (30d)
2

Description

The entity manager is an implementation of the Facade pattern based on `rel.Changeset`.

All entities have 3 states:
- `new`: base state for new entity - not managed by the entity manager.
- `managed`: entity in `new` state passed to the `Persist` method of the entity manager or loaded from the database using method from the entity manager like `Find`
- `removed`: Entity must by removed after flush

Method `Flush` - commits changes for all `managed` entities in transaction by `rel.Changeset`, and reset `rel.Changeset` state.

Additionally:
- `Find` method with id criteria (find by id) should check if entity is already loaded and return it if is true (do not make a new request)
- Transactional write-behind allows to replace many `Insert` in code to one `InsertAll`
- Lazy users can wrap in middleware with automatic call `flush`
- Use one time `ctx context.Conext`

## API

### Persist(record interface{})

Make an entity managed and persistent.
The record will be inserted into the database as a result of the `Flush` operation.
Only for new entities

### Remove(record interface{})

Removes an entity.
A removed entity will be removed from the database as a result of the `Flush` operation.

### Refresh(ctx context.Context, record interface{}) error

Refreshes the persistent state of an entity from the database, overriding any local changes that have not yet been persisted.

### Flush() error

Flushes all changes to an entities that have been queued up to now to the database.

### Select methods from `rel.Repository`

Retrieve the records, persist them in the entity manager, and return to the user.

API may by changed like:

```go
Find[T any](ctx context.Context, queriers ...Querier) (*T, error)
```

## Example

Current way:

```go
func (t Todos) MoveToBasket(ctx context.Context, repo rel.Repository, id int) {
var todo todos.Todo
if err := repo.Find(ctx, &todo, where.Eq("id", id)); err != nil {
panic(err)
}
changesTodo = rel.NewChangeset(&todo)

var basket todos.Category
if err := repo.Find(ctx, &basket, where.Eq("name", "basket")); err != nil {
panic(err)
}
changesBasket = rel.NewChangeset(&basket)

// domain code
todo.Category = basket
basket.Total += 1

repo.Transaction(ctx, func(ctx context.Context) error {
err := repo.Update(c, &todo, changesTodo)
if err != nil {
return err
}

err := repo.Update(c, &basket, changesBasket)
if err != nil {
return err
}

return nil
})
if err != nil {
render(ctx, "Server error", 500)
}

render(ctx, todo, 200)
}
```

If you don't want to get a problems, you should always call `Update` in the same method where entity fetched. Very often it turns out the controller. Very often this place is the controller

Entity manager:

```go
func (t Todos) DoSomething(ctx context.Context, em rel.EntityManager, id int) {
var todo todos.Todo
if err := em.Find(ctx, &todo, where.Eq("id", id)); err != nil {
panic(err)
}

var basket todos.Category
if err := em.Find(ctx, &basket, where.Eq("name", "basket")); err != nil {
panic(err)
}

// domain code
todo.Category = basket
basket.Total += 1

render(ctx, todo, 200)
}

func MiddlewareForLazyUsers(ctx iris.Context, repo rel.Repository) {
em := rel.NewEntityManager(repo)

// request dependency
ctx.RegisterDependency(em)

ctx.Next()

err := em.Flush(ctx.Context())
if err != nil {
render(ctx, "Server error", 500)
}
}
```

## Improvements

1. Dedicated repositories for entity, this will improve the user experience and allow use of generics:

- `rel.Repository`: Universal repository(current implementation)
- `rel.EntityRepository[T any]`: Repository for entity. Similar to `rel.Repository`, but with generics and managed by entity manager.
- `rel.EntityManager`: Entity manager with methods:
- `Persist(record interface{})`
- `Remove(record interface{})`
- `Refresh(ctx context.Context, record interface{}) error`
- `Flush() error`
- `repository(...) any`: internal method for `rel.Repository[T any]`.

```go
func (t Todos) DoSomething(ctx context.Context, em rel.EntityManager, id int) {
todo, err := rel.Repository[todos.Todo](em).Find(ctx, where.Eq("id", id))
if err != nil {
panic(err)
}

basket, err := rel.Repository[todos.Category](em).Find(ctx, where.Eq("name", "basket"))
if err != nil {
panic(err)
}

// domain code
todo.Category = basket
basket.Total += 1

render(ctx, todo, 200)
}
```

2. User can implement own repository and register than in entity manager.
3. Association managed by entity manager
- `when the primary key (id) is not zero.` does not work for client side id (like UUID). Entity manager resolve this issue.

## Summary

Entity Manager allows you to keep the domain clean, and removes the complexity of working with the database

Contributor guide

Open the contributing guide

Research direction

Start by reading rel.Changeset and rel.Repository, then map the proposed EntityManager API to the entity states and Flush behavior described here. Done means supporting Persist, Remove, Refresh, Flush, managed lookups, and repository integration while preserving transactional write-behind and the example usage patterns.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend, databases
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.