Skip to content

Commit

Permalink
init data loaders
Browse files Browse the repository at this point in the history
  • Loading branch information
thanhhaudev committed Nov 7, 2024
1 parent 651c018 commit df04760
Show file tree
Hide file tree
Showing 16 changed files with 251 additions and 4 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/urfave/cli/v2 v2.27.4 // indirect
github.com/vikstrous/dataloadgen v0.0.6 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.opentelemetry.io/otel v1.11.1 // indirect
go.opentelemetry.io/otel/trace v1.11.1 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/sync v0.8.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,14 @@ github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
github.com/vektah/gqlparser/v2 v2.5.17 h1:9At7WblLV7/36nulgekUgIaqHZWn5hxqluxrxGUhOmI=
github.com/vektah/gqlparser/v2 v2.5.17/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww=
github.com/vikstrous/dataloadgen v0.0.6 h1:A7s/fI3QNnH80CA9vdNbWK7AsbLjIxNHpZnV+VnOT1s=
github.com/vikstrous/dataloadgen v0.0.6/go.mod h1:8vuQVpBH0ODbMKAPUdCAPcOGezoTIhgAjgex51t4vbg=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
go.opentelemetry.io/otel v1.11.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4=
go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE=
go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
Expand Down
2 changes: 1 addition & 1 deletion src/datastore/postgres/author.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (a *authorRepository) FindByID(ctx context.Context, id int) (*model.Author,

func (a *authorRepository) FindByIDs(ctx context.Context, ids []int) ([]*model.Author, error) {
var authors []*model.Author
if err := a.db.WithContext(ctx).Where("id IN (?)", ids).Find(&authors).Error; err != nil {
if err := a.db.WithContext(ctx).Preload("Books").Where("id IN (?)", ids).Find(&authors).Error; err != nil {
return nil, err
}

Expand Down
9 changes: 9 additions & 0 deletions src/datastore/postgres/book.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ type bookRepository struct {
db *gorm.DB
}

func (b *bookRepository) FindByIDs(ctx context.Context, ids []int) ([]*model.Book, error) {
var books []*model.Book
if err := b.db.WithContext(ctx).Preload("Authors").Find(&books, ids).Error; err != nil {
return nil, err
}

return books, nil
}

func (b *bookRepository) FindBooksByAuthorID(ctx context.Context, authorID int) ([]*model.Book, error) {
var author model.Author
if err := b.db.WithContext(ctx).Preload("Books").First(&author, authorID).Error; err != nil {
Expand Down
9 changes: 9 additions & 0 deletions src/datastore/postgres/borrower.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ func (b *borrowerRepository) FindByID(ctx context.Context, id int) (*model.Borro
return &borrower, nil
}

func (b *borrowerRepository) FindByIDs(ctx context.Context, ids []int) ([]*model.Borrower, error) {
var borrowers []*model.Borrower
if err := b.db.WithContext(ctx).Find(&borrowers, ids).Error; err != nil {
return nil, err
}

return borrowers, nil
}

func (b *borrowerRepository) FindBorrowerBooksByID(ctx context.Context, borrowerID int) ([]*model.BorrowerBook, error) {
var borrowerBooks []*model.BorrowerBook
if err := b.db.WithContext(ctx).
Expand Down
6 changes: 5 additions & 1 deletion src/graph/model/author.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
type Author struct {
ID int `gorm:"primaryKey"`
Name string `json:"name"`
Books []*Book `gorm:"many2many:authors_books" json:"books"`
Books []*Book `gorm:"many2many:authors_books;foreignKey:ID;joinForeignKey:AuthorID;References:ID;JoinReferences:BookID"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Expand All @@ -20,6 +20,10 @@ func (a Author) TableName() string {
return "authors"
}

func (a *Author) GetID() int {
return a.ID
}

type CreateAuthorInput struct {
Name string `json:"name"`
Books []int `json:"books"`
Expand Down
4 changes: 4 additions & 0 deletions src/graph/model/book.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ func (b Book) TableName() string {
return "books"
}

func (b *Book) GetID() int {
return b.ID
}

func (b *Book) SetAuthors(authors []*Author) {
b.Authors = authors
}
Expand Down
4 changes: 4 additions & 0 deletions src/graph/model/borrower.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,7 @@ type Borrower struct {
func (b Borrower) TableName() string {
return "borrowers"
}

func (b *Borrower) GetID() int {
return b.ID
}
2 changes: 2 additions & 0 deletions src/graph/resolver/author.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/graph/resolver/book.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions src/loader/author.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package loader

import (
"context"
"github.com/thanhhaudev/go-graphql/src/graph/model"
"github.com/thanhhaudev/go-graphql/src/repository"
)

type authorLoader struct {
authorRepository repository.AuthorRepository
}

// getAuthors implements a batch function that loads authors by IDs
func (a *authorLoader) getAuthors(ctx context.Context, ids []int) ([]*model.Author, []error) {
authors, err := a.authorRepository.FindByIDs(ctx, ids)
if err != nil {
return nil, []error{err}
}

return mapping(authors, ids), nil
}

func newAuthorLoader(authorRepository repository.AuthorRepository) *authorLoader {
return &authorLoader{
authorRepository: authorRepository,
}
}
28 changes: 28 additions & 0 deletions src/loader/book.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package loader

import (
"context"

"github.com/thanhhaudev/go-graphql/src/graph/model"
"github.com/thanhhaudev/go-graphql/src/repository"
)

type bookLoader struct {
bookRepository repository.BookRepository
}

// getBooks implements a batch function that loads books by IDs
func (b *bookLoader) getBooks(ctx context.Context, ids []int) ([]*model.Book, []error) {
books, err := b.bookRepository.GetByIDs(ctx, ids)
if err != nil {
return nil, []error{err}
}

return mapping(books, ids), nil
}

func newBookLoader(bookRepository repository.BookRepository) *bookLoader {
return &bookLoader{
bookRepository: bookRepository,
}
}
28 changes: 28 additions & 0 deletions src/loader/borrower.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package loader

import (
"context"

"github.com/thanhhaudev/go-graphql/src/graph/model"
"github.com/thanhhaudev/go-graphql/src/repository"
)

type borrowerLoader struct {
borrowerRepository repository.BorrowerRepository
}

// getBorrowers implements a batch function that loads borrowers by IDs
func (b *borrowerLoader) getBorrowers(ctx context.Context, ids []int) ([]*model.Borrower, []error) {
borrowers, err := b.borrowerRepository.FindByIDs(ctx, ids)
if err != nil {
return nil, []error{err}
}

return mapping(borrowers, ids), nil
}

func newBorrowerLoader(borrowerRepository repository.BorrowerRepository) *borrowerLoader {
return &borrowerLoader{
borrowerRepository: borrowerRepository,
}
}
113 changes: 113 additions & 0 deletions src/loader/loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package loader

import (
"context"
"net/http"
"time"

"github.com/thanhhaudev/go-graphql/src/graph/model"
"github.com/thanhhaudev/go-graphql/src/repository"
"github.com/vikstrous/dataloadgen"
)

type ctxKey string

type Model interface {
GetID() int
}

const (
loadersKey = ctxKey("dataLoaders")
)

// Loaders wrap your data loaders to inject via middleware
type Loaders struct {
AuthorLoader *dataloadgen.Loader[int, *model.Author]
BookLoader *dataloadgen.Loader[int, *model.Book]
BorrowerLoader *dataloadgen.Loader[int, *model.Borrower]
}

// NewLoaders instantiates data loaders for the middleware
func NewLoaders(
authorRepository repository.AuthorRepository,
bookRepository repository.BookRepository,
borrowerRepository repository.BorrowerRepository,
) *Loaders {
al := newAuthorLoader(authorRepository)
bl := newBookLoader(bookRepository)
brl := newBorrowerLoader(borrowerRepository)
w := dataloadgen.WithWait(10 * time.Millisecond)

return &Loaders{
AuthorLoader: dataloadgen.NewLoader(al.getAuthors, w),
BookLoader: dataloadgen.NewLoader(bl.getBooks, w),
BorrowerLoader: dataloadgen.NewLoader(brl.getBorrowers, w),
}
}

// Middleware injects data loaders into the context
// This middleware likely injects data loaders into the request context, optimizing data fetching.
// This accessibly allows the resolvers to load data in a batch, reducing the number of queries to the database.
func Middleware(
authorRepository repository.AuthorRepository,
bookRepository repository.BookRepository,
borrowerRepository repository.BorrowerRepository,
next http.Handler,
) http.Handler {
// return a middleware that injects the loader to the request context
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
loader := NewLoaders(authorRepository, bookRepository, borrowerRepository)
r = r.WithContext(context.WithValue(r.Context(), loadersKey, loader))

next.ServeHTTP(w, r)
})
}

// mapping maps the models to the same order as the input IDs
func mapping[M Model](models []M, ids []int) []M {
// Create a map to easily find authors by ID
idMap := make(map[int]M)
for _, m := range models {
idMap[m.GetID()] = m
}

// Prepare the result slice in the same order as the input IDs
// This is important because the result will be used in the same order as the input IDs
result := make([]M, len(ids))
for i, id := range ids {
result[i] = idMap[id]
}

return result
}

// For returns the data loader for a given context
func For(ctx context.Context) *Loaders {
return ctx.Value(loadersKey).(*Loaders)
}

// FindAuthor loads an author by ID
func FindAuthor(ctx context.Context, id int) (*model.Author, error) {
return For(ctx).AuthorLoader.Load(ctx, id)
}

// GetAuthors loads authors by IDs
func GetAuthors(ctx context.Context, ids []int) ([]*model.Author, error) {
return For(ctx).AuthorLoader.LoadAll(ctx, ids)
}

func FindBook(ctx context.Context, id int) (*model.Book, error) {
return For(ctx).BookLoader.Load(ctx, id)
}

func GetBooks(ctx context.Context, ids []int) ([]*model.Book, error) {
return For(ctx).BookLoader.LoadAll(ctx, ids)
}

func FindBorrower(ctx context.Context, id int) (*model.Borrower, error) {
return For(ctx).BorrowerLoader.Load(ctx, id)
}

func GetBorrowers(ctx context.Context, ids []int) ([]*model.Borrower, error) {
return For(ctx).BorrowerLoader.LoadAll(ctx, ids)
}
2 changes: 2 additions & 0 deletions src/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
type BookRepository interface {
GetAll(ctx context.Context) ([]*model.Book, error)
FindByID(ctx context.Context, id int) (*model.Book, error)
FindByIDs(ctx context.Context, ids []int) ([]*model.Book, error)
FindBooksByAuthorID(ctx context.Context, authorID int) ([]*model.Book, error)
Create(ctx context.Context, model *model.Book) error
Update(ctx context.Context, model *model.Book) error
Expand All @@ -27,6 +28,7 @@ type AuthorRepository interface {
type BorrowerRepository interface {
GetAll(ctx context.Context) ([]*model.Borrower, error)
FindByID(ctx context.Context, id int) (*model.Borrower, error)
FindByIDs(ctx context.Context, ids []int) ([]*model.Borrower, error)
FindByTelNumber(ctx context.Context, telNumber string) (*model.Borrower, error)
FindBorrowerBooksByID(ctx context.Context, borrowerID int) ([]*model.BorrowerBook, error)
Create(ctx context.Context, model *model.Borrower) (*model.Borrower, error)
Expand Down
9 changes: 8 additions & 1 deletion src/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/thanhhaudev/go-graphql/src/datastore/postgres"
"github.com/thanhhaudev/go-graphql/src/graph/generated"
"github.com/thanhhaudev/go-graphql/src/graph/resolver"
"github.com/thanhhaudev/go-graphql/src/loader"
"github.com/thanhhaudev/go-graphql/src/service"
)

Expand Down Expand Up @@ -46,10 +47,16 @@ func main() {

defer db.Close() // Close database connection when main function exits

srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{
// create the query handler
var srv http.Handler = handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{
Resolvers: resolver.NewResolver(authorService, bookService, borrowerService),
}))

// inject data loaders into the context
// wrap the srv handler with the middleware
srv = loader.Middleware(authorRepo, bookRepo, borrowerRepo, srv)

// route handlers
http.Handle("/", playground.Handler("GraphQL playground", "/query"))
http.Handle("/query", srv)

Expand Down

0 comments on commit df04760

Please sign in to comment.