Queries & QueryHandler
QueryHandlers serve the purpose of answering user queries without altering the state of aggregates. They are inherently read-only and can only invoke read-only methods.
From a technical standpoint, implementing QueryHandlers involves adhering to the QueryHandler
interface. This interface defines the methods GetQueryDataList
and HandleQuery
. The GetQueryDataList
returns a list of all known QueryData
this handler is able to process. The HandleQuery
method processes the query itself.
Here, we present the best practices for implementing a QueryHandler
that handles requests and returns a response of type Response
. Similar to handling Commands, Events, and Aggregates, create a new package folder named query and then create the file domain/simple/query/handler.go
within that folder.
// domain/simple/query/handler.go
package query
import (
"fmt"
"my/app/domain/simple/readmodel"
"github.com/gradientzero/es-cqrs-base/base"
)
type queryHandler struct {
SimpleReadmodel readmodel.ReadModel
}
// Make sure it implements interface
var _ base.QueryHandler = (*queryHandler)(nil)
func NewQueryHandler(
SimpleReadmodel readmodel.ReadModel,
) base.QueryHandler {
qh := &queryHandler{}
qh.SimpleReadmodel = SimpleReadmodel
return qh
}
// fullfilling base.QueryHandler interface
func (qh *queryHandler) GetQueryDataList() []base.QueryData {
return []base.QueryData{
&SimpleQueryModel{},
// ... &SimpleQueryList{},
}
}
func (qh *queryHandler) HandleQuery(qry base.Query) (*base.Response, error) {
// optional: custom logic like authorization
// ...
if qry.GetData() != nil {
return qry.GetData().HandleQueryData(qh, qry)
} else {
return nil, fmt.Errorf("could not handle query, unknown type %s", qry.GetDataType())
}
}
Unlike EventHandlers, Queries are processed by a single QueryHandler
that has added the corresponding QueryData
(in this case, SimpleQueryModel
) to the list. Within the HandleQuery
method, operations applicable to all queries can be performed. Subsequently, HandleQueryData
method of the underlying QueryData
of the provided Query
is called.
By convention, each QueryData
is written in its own file. In this example, the QueryData SimpleQueryModel
will be stored in domain/simple/query/qry.model.go
. This QueryData is designed to simply return the internal model of a specific aggregate.
// domain/simple/query/qry.model.go
package query
import (
"fmt"
"github.com/gradientzero/es-cqrs-base/base"
)
// SimpleQueryModel is the QueryData
type SimpleQueryModel struct {
AggregateUuid string `json:"aggregateUuid,omitempty"`
SomeOtherParams string `json:"someOtherParams,omitempty"`
}
// Make sure it implements interface
var _ base.QueryData = (*SimpleQueryModel)(nil)
// fullfilling base.QueryData interface
func (qryData *SimpleQueryModel) HandleQueryData(_qryHandler base.QueryHandler, qry base.Query) (*base.Response, error) {
// retrieve query handler
qryHandler, ok := _qryHandler.(*queryHandler)
if !ok {
return nil, fmt.Errorf("%s failed - could not cast abstract aggregate to concrete type", base.GetTypeName(_qryHandler))
}
// retrieve item read model
model, err := qryHandler.SimpleReadModel.GetModelByAggregateUuid(qryData.AggregateUuid)
if err != nil {
return nil, err
}
// generate response
response := &base.Response{}
response.Item = model
// return response
return response, nil
}
First, we define our QueryData
(SimpleQueryModel
) and implement the HandleQueryData
method, as required by the base.QueryData
interface. Then, we cast the QueryHandler to our specific QueryHandler
so that we can access the ReadModel
. Next, we simply query the ReadModel for this aggregate, and the ReadModel provides the internal model of the aggregate. This model is then written into the Response
object and returned.
If we want to return a list, we should use Items
instead of Item
and write the list into response.Items
.