Add SQL query cacheing

This commit is contained in:
Vikram Rangnekar 2019-04-20 00:35:57 -04:00
parent 652b31ce38
commit b666876064
9 changed files with 117 additions and 43 deletions

View File

@ -2,6 +2,8 @@ app_name: "Super Graph Development"
host_port: 0.0.0.0:8080 host_port: 0.0.0.0:8080
web_ui: true web_ui: true
debug_level: 1 debug_level: 1
# enabling tracing also disables query caching
enable_tracing: true enable_tracing: true
# Throw a 401 on auth failure for queries that need auth # Throw a 401 on auth failure for queries that need auth

View File

@ -2,6 +2,8 @@ app_name: "Super Graph Production"
host_port: 0.0.0.0:8080 host_port: 0.0.0.0:8080
web_ui: false web_ui: false
debug_level: 0 debug_level: 0
# disabled tracing enables query caching
enable_tracing: false enable_tracing: false
# Throw a 401 on auth failure for queries that need auth # Throw a 401 on auth failure for queries that need auth

View File

@ -1,5 +1,5 @@
query { query {
product(id: 15) { product(id: $PRODUCT_ID, where: { price: { eq: $PRODUCT_PRICE } }) {
id id
name name
} }

View File

@ -1,21 +1,6 @@
query { query {
products( product(id: 15) {
# returns only 30 items
limit: 30,
# starts from item 10, commented out for now
# offset: 10,
# orders the response items by highest price
order_by: { price: desc },
# no duplicate prices returned
distinct: [ price ]
# only items with an id >= 30 and < 30 are returned
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }) {
id id
name name
price
} }
} }

View File

@ -240,6 +240,29 @@ query {
} }
``` ```
### Using variables
Variables and their values can be passed along side the GraphQL query. Using variables makes for better client side code as well as improved server side
caching. The build-in web ui also hass support for variables. Not having to manupilate your GraphQL query string to insert values into it makes for cleaner
and better client side code.
```javascript
// define the request object keeping the query and the variables seperate
var req = {
query: '{ product(id: $product_id) { name } }' ,
variables: { "product_id": 5 }
}
// use the fetch api to make the query
fetch('http://localhost:8080/api/v1/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req),
})
.then(res => res.json())
.then(res => console.log(res.data));
```
### Full text search ### Full text search
Every app these days needs search. Enought his often means reaching for something heavy like Solr. While this will work why add complexity to your infrastructure when Postgres has really great Every app these days needs search. Enought his often means reaching for something heavy like Solr. While this will work why add complexity to your infrastructure when Postgres has really great
@ -412,11 +435,13 @@ Configuration files can either be in YAML or JSON their names are derived from t
We're tried to ensure that the config file is self documenting and easy to work with. We're tried to ensure that the config file is self documenting and easy to work with.
```yaml ```yaml
title: Super Graph Development app_name: "Super Graph Development"
host_port: 0.0.0.0:8080 host_port: 0.0.0.0:8080
web_ui: true web_ui: true
debug_level: 1 debug_level: 1
enable_tracing: false
# enabling tracing also disables query caching
enable_tracing: true
# Throw a 401 on auth failure for queries that need auth # Throw a 401 on auth failure for queries that need auth
# valid values: always, per_query, never # valid values: always, per_query, never

1
go.mod
View File

@ -3,6 +3,7 @@ module github.com/dosco/super-graph
require ( require (
github.com/Masterminds/semver v1.4.2 github.com/Masterminds/semver v1.4.2
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3 github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3
github.com/allegro/bigcache v1.2.0
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/garyburd/redigo v1.6.0 github.com/garyburd/redigo v1.6.0

2
go.sum
View File

@ -4,6 +4,8 @@ github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITg
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3 h1:+qz9Ga6l6lKw6fgvk5RMV5HQznSLvI8Zxajwdj4FhFg= github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3 h1:+qz9Ga6l6lKw6fgvk5RMV5HQznSLvI8Zxajwdj4FhFg=
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3/go.mod h1:FlkD11RtgMTYjVuBnb7cxoHmQGqvPpCsr2atC88nl/M= github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3/go.mod h1:FlkD11RtgMTYjVuBnb7cxoHmQGqvPpCsr2atC88nl/M=
github.com/allegro/bigcache v1.2.0 h1:qDaE0QoF29wKBb3+pXFrJFy1ihe5OT9OiXhg1t85SxM=
github.com/allegro/bigcache v1.2.0/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 h1:rRISKWyXfVxvoa702s91Zl5oREZTrR3yv+tXrrX7G/g= github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 h1:rRISKWyXfVxvoa702s91Zl5oREZTrR3yv+tXrrX7G/g=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=

View File

@ -425,6 +425,26 @@ func aggFunctionWithFilter(t *testing.T) {
} }
} }
func queryWithVariables(t *testing.T) {
gql := `query {
product(id: $PRODUCT_ID, where: { price: { eq: $PRODUCT_PRICE } }) {
id
name
}
}`
sql := `SELECT json_object_agg('product', products) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "products" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((("products"."price") > (0)) AND (("products"."price") < (8)) AND (("products"."price") = ('{{PRODUCT_PRICE}}')) AND (("id") = ('{{PRODUCT_ID}}'))) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "done_1337";`
resSQL, err := compileGQLToPSQL(gql)
if err != nil {
t.Fatal(err)
}
if resSQL != sql {
t.Fatal(errNotExpected)
}
}
func syntheticTables(t *testing.T) { func syntheticTables(t *testing.T) {
gql := `query { gql := `query {
me { me {
@ -457,6 +477,7 @@ func TestCompileGQL(t *testing.T) {
t.Run("manyToManyReverse", manyToManyReverse) t.Run("manyToManyReverse", manyToManyReverse)
t.Run("aggFunction", aggFunction) t.Run("aggFunction", aggFunction)
t.Run("aggFunctionWithFilter", aggFunctionWithFilter) t.Run("aggFunctionWithFilter", aggFunctionWithFilter)
t.Run("queryWithVariables", queryWithVariables)
t.Run("syntheticTables", syntheticTables) t.Run("syntheticTables", syntheticTables)
} }

View File

@ -2,44 +2,74 @@ package serv
import ( import (
"context" "context"
"crypto/sha1"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"strings" "strings"
"time" "time"
"github.com/allegro/bigcache"
"github.com/dosco/super-graph/qcode"
"github.com/go-pg/pg" "github.com/go-pg/pg"
"github.com/valyala/fasttemplate" "github.com/valyala/fasttemplate"
) )
var (
cache, _ = bigcache.NewBigCache(bigcache.DefaultConfig(24 * time.Hour))
)
func handleReq(ctx context.Context, w io.Writer, req *gqlReq) error { func handleReq(ctx context.Context, w io.Writer, req *gqlReq) error {
qc, err := qcompile.CompileQuery(req.Query) var key, finalSQL string
if err != nil { var qc *qcode.QCode
var entry []byte
var err error
cacheEnabled := (conf.EnableTracing == false)
if cacheEnabled {
k := sha1.Sum([]byte(req.Query))
key = string(k[:])
entry, err = cache.Get(key)
}
if len(entry) == 0 || err == bigcache.ErrEntryNotFound {
qc, err = qcompile.CompileQuery(req.Query)
if err != nil {
return err
}
var sqlStmt strings.Builder
if err := pcompile.Compile(&sqlStmt, qc); err != nil {
return err
}
t := fasttemplate.New(sqlStmt.String(), openVar, closeVar)
sqlStmt.Reset()
_, err = t.Execute(&sqlStmt, varMap(ctx, req.Vars))
if err == errNoUserID &&
authFailBlock == authFailBlockPerQuery &&
authCheck(ctx) == false {
return errUnauthorized
}
if err != nil {
return err
}
finalSQL = sqlStmt.String()
} else if err != nil {
return err return err
} else {
finalSQL = string(entry)
} }
var sqlStmt strings.Builder
if err := pcompile.Compile(&sqlStmt, qc); err != nil {
return err
}
t := fasttemplate.New(sqlStmt.String(), openVar, closeVar)
sqlStmt.Reset()
_, err = t.Execute(&sqlStmt, varMap(ctx, req.Vars))
if err == errNoUserID &&
authFailBlock == authFailBlockPerQuery &&
authCheck(ctx) == false {
return errUnauthorized
}
if err != nil {
return err
}
finalSQL := sqlStmt.String()
if conf.DebugLevel > 0 { if conf.DebugLevel > 0 {
fmt.Println(finalSQL) fmt.Println(finalSQL)
} }
@ -55,6 +85,12 @@ func handleReq(ctx context.Context, w io.Writer, req *gqlReq) error {
et := time.Now() et := time.Now()
resp := gqlResp{Data: json.RawMessage(root)} resp := gqlResp{Data: json.RawMessage(root)}
if cacheEnabled {
if err = cache.Set(key, []byte(finalSQL)); err != nil {
return err
}
}
if conf.EnableTracing { if conf.EnableTracing {
resp.Extensions = &extensions{newTrace(st, et, qc)} resp.Extensions = &extensions{newTrace(st, et, qc)}
} }