Open benmai opened 6 years ago
@benmai Really good points covered.
I've had some experience implementing some Query engines and I came up with the following interfaces:
// Query defines query parameters.
type Query interface {
QueryID() string
}
// QueryHandler handles a query.
type QueryHandler interface {
// Handle handles the given query and returns the report.
Handle(ctx context.Context, q Query, report interface{}) error
}
The typical usage example may look as follows:
// query
type GetAccountShortInfo struct {
Number string
}
func (r GetAccountShortInfo) QueryID() string {
return "GetAccountShortInfo"
}
// report (read side model)
// Account An Account representation.
type Account struct {
Number string
Balance money.Money
Ledgers []Ledger
}
// somewhere in the application service where results are needed
func ShowAccount {
accountReport := &report.Account{}
err := h.queryBus.Handle(context.Background(), query.GetAccountShortInfo{Number: number}, accountReport)
// accountReport will have all the query results
}
Obviously the query structure can hold all the fields needed for the request: limit, filters, offset, etc... You may for example implement some CriteriaQuery and an SQLQueryHandler which would satisfy the interfaces above :)
You may look at a simplified examples I've made.
Follow-up of this Slack conversation.
In the process of working on a new repo driver, I’ve run up against some of the limits of the
ReadRepo
interface. Namely:eventhorizon
has its own ad-hoc methods. For example, the Mongo driver hasFindCustom
andFindCustomIter
, which take Mongo-specific arguments. The problems with this are two-fold:local
).ReadRepo
functionality, which makes it hard to implement new drivers that follow the conventions of existing drivers and are tested with the same level of rigor.Find
andFindAll
alone don’t fulfill the query needs of a fully-featured application. It’s missing paging (a must-have for a large number of records), selecting by any kind of condition, making aggregation queries (e.g. count of records which satisfy a condition), sorting, and probably some other things I'm not thinking of. The MongoDB repo solves this in an ad-hoc way by exposing the ability to execute MongoDB queries directly. Also, from what I can surmise theParent()
method onReadRepo
exists only to access this MongoDB-specific functionality at the moment.Find
andFindAll
methods, seems more informed by MongoDB than CQRS. This is a completely semantic argument, but to me naming something after the MongoDB API makes it marginally more difficult to map the functionality to CQRS principles.Based on these limitations, I would like to propose the following replacement for
ReadRepo
:And then an example of how an implementation of
Querier
might handleQueryOption
s:A few notes about this:
QueryOption
idea is heavily inspired by gRPCDialOption
s, which are also passed as variadic arguments.query.go
, perhaps) can expose some generalQueryOption
s itself. The implementations ofQuerier
can choose to support them or not, and the implementations may also expose their ownQueryOption
s. This way, implementations ofQuerier
may be extensible without breaking the interface API, so we can write tests for the way our transport layer (e.g., RPC or HTTP) uses theQuerier
using a mocked interface. As an example, we may achieve backwards compatibility (probably) with any applications using the MongoDB driver withFindCustom
currently by exposing aqueryOptionMongo
that contains afunc(*mgo.Collection) *mgo.Query
, which is whatFindCustom
does anyway.Find
is gone), but combiningNewQueryOptionID
with a check for an empty slice of entities returned fromQuery
can achieve the same functionality. This keeps the interface very simple.group by
), but potentially that could be solved by a separateAggregator
interface. That also may just need to be very custom.This is all just stuff I’ve thought about in the last two days or so, and I am very open to any and all feedback. Everything I’m trying to do here is in service of making it easier to work within the Event Horizon framework so that when using it we’re thinking more about how to do CQRS well rather than how to do something or other with any specific database, so I’m especially interested to hear suggestions about how we can get closer to that ideal.
Also thanks to @hryx, with whom I’m working on all of these ideas. Please chime in if I've missed anything.