-
-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Entity manager #278
Comments
what do you mean by this? do you mean if I understand correctly, I think the proposal sounds interesting, but I'm not sure about the interest from other users. would you like to start with PoC first? I could give you access to a blank repository in this organization |
No,
Yes!
I will try to do this (I will start in a personal repository), but due to the situation in my country, I can't give an estimate of the time. |
Great, Thank you Hope you always safe and the situation can get better soon 🙏 |
Sounds both interesting and frighting. With generics comes the power and responsibility. All these entity managers/generic repos come from Java/Net world. Such tools as Entity Framework or Hibernate have been honed for years and include ridiculous amount of work. Lots of optimisations, lots features. Golang on the other side has always strived to be a very straightforward and simple language. With all these new features people tend to bring the re-invented heavy enterprise wheel into the go world. Proper transaction handling in Golang when more than one |
Implemented as UOW in one big project - everything is fine. Prototype: type UnitOfWork interface {
Context() context.Context
// Persist make the entities managed and persistent.
// An entity will be inserted into the database as a result of the
// Flush operation.
Persist(entities ...any)
// Removes the entities.
// A removed entity will be removed from the database as a result of the
// Flush operation.
Remove(entities ...any)
// Attach existing entities.
// An entity will be updated into the database as a result of the
// Flush operation.
Attach(entities ...any)
// Detach entities from UOW, so Flush operation do nothing with this
// entities.
Detach(entities ...any)
// Flushes all changes to an entities that have been queued up to now to
// the database.
Flush() error
}
type UnitOfWorkFactory func(ctx context.Context) UnitOfWork Transaction handling is not a problem because they are created at N levels (1 - base level: http request/other entry point, 2... - domain): func (c *Controller) action(ctx *fiber.Ctx) error {
uow := c.uowFactory(ctx.UserContext())
c.anyService1.RunMethod(uow, args...)
c.anyService2.RunMethod(uow, args...)
if err := uow.Flush(); err != nil { // first level transaction
return internal server error
}
}` |
Hi @qRoC, |
No, but implementation has less than 200 lines of code. The only thing is that you need a patch for rel - master...qRoC:rel:master (details: #308).
You can change signature from Separated objects: func (s *Server) RegisterUser(ctx context.Context) {
uow := createUOW()
s.userService.RegisterUser(ctx, uow, email, password) // too many arguments :(
if err := uow.Flush(ctx); err != nil {
return s.internalServerError()
}
}
func (s *UserService) RegisterUser(ctx context.Context, uow persistence.UnitOfWork, email, password string) (User, error) {
user, err := s.repo.findByEmail(ctx, uow, email) // too many arguments :(
if err != nil {
return nil, err
}
if user != nil {
return user, ErrUserExists
}
uow.Persist(user)
}
func (s *RelUserRepository) findByEmail(ctx context.Context, uow persistence.UnitOfWork, email string) (domain.User, error) {
user := new(domain.User)
if err := repo.inner.Find(ctx, user, rel.Eq("email", email)); err != nil {
if errors.Is(err, rel.ErrNotFound) {
return nil, nil
}
return nil, err
}
uow.Attach(user)
return user, nil
} context in UOW: func (s *Server) RegisterUser(ctx context.Context) {
uow := createUOW()
s.userService.RegisterUser(uow, email, password)
if err := uow.Flush(); err != nil {
return s.internalServerError()
}
}
func (s *UserService) RegisterUser(uow persistence.UnitOfWork, email, password string) (User, error) {
user, err := s.repo.findByEmail(uow, email)
if err != nil {
return nil, err
}
if user != nil {
return user, ErrUserExists
}
uow.Persist(user)
}
func (s *RelUserRepository) findByEmail(uow persistence.UnitOfWork, email string) (domain.User, error) {
user := new(domain.User)
if err := repo.inner.Find(uow.Context(), user, rel.Eq("email", email)); err != nil {
if errors.Is(err, rel.ErrNotFound) {
return nil, nil
}
return nil, err
}
uow.Attach(user)
return user, nil
} |
I can provide a basic implementation of UOW, but a full-fledged one will depend on your tasks. For example, I use an additional abstraction for counter fields: type NumberOp = uint8
const (
// NumberOpNothing means that a value is not changed.
NumberOpNothing NumberOp = 0
// NumberOpSet means that a value must be set.
NumberOpSet NumberOp = 1
// NumberOpIncrement means that a value must be incremented by value.
NumberOpIncrement NumberOp = 2
)
// Implementation of a counter with data to be able to persist in custom
// service (like Redis, Postgres).
type Number[T constraints.Number] struct {
initValue T
currentValue T
isIncrement bool
}
func NewNumber[T constraints.Number](value T) Number[T] {
return Number[T]{
initValue: value,
currentValue: value,
isIncrement: true,
}
}
func (number *Number[T]) Set(value T) {
number.currentValue = value
number.isIncrement = false
}
func (number *Number[T]) Increment(value T) {
number.currentValue += value
}
func (number Number[T]) Get() T {
return number.currentValue
}
func (number Number[T]) PersistInfo() (NumberOp, T) {
if number.initValue == number.currentValue {
return NumberOpNothing, number.currentValue
}
if number.isIncrement {
return NumberOpIncrement, number.currentValue - number.initValue
}
return NumberOpSet, number.currentValue
} type Post struct {
Views Number[uint64]
}
func (s *BlogService) ShowPost(uow persistence.UnitOfWork, id identity.Value) (Post, error) {
post, err := s.repo.findByID(uow, id)
if err != nil {
return nil, err
}
if post != nil {
post.Views.Increment(1)
// attached to UOW in `findByID`, so `Persist` is not needed.
}
return post, nil
} |
Hey @qRoC, thanks for your replies. If I got it correctly, your proposal about
is very similar to what DbContext from
It would be great to have it as part of When it comes to your examples, there is more than one way to skin a cat and to implement a Unit of work in conjunction with a Repository pattern. |
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 innew
state passed to thePersist
method of the entity manager or loaded from the database using method from the entity manager likeFind
removed
: Entity must by removed after flushMethod
Flush
- commits changes for allmanaged
entities in transaction byrel.Changeset
, and resetrel.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)Insert
in code to oneInsertAll
flush
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:
Example
Current way:
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 controllerEntity manager:
Improvements
rel.Repository
: Universal repository(current implementation)rel.EntityRepository[T any]
: Repository for entity. Similar torel.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 forrel.Repository[T any]
.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
The text was updated successfully, but these errors were encountered: