If you are using gqlgen to generate your GraphQL endpoint and sqlx or some other kind of ORM for your database, you can benefit from using one type or struct in both directions. You create one type User struct
for GraphQL and it maps to your db schema. Now if this user contains more complex types, things become interesting…
Requirements
- PostgreSQL database (or e.g. CockroachDB)
- Using gqlgen to generate a GraphQL endpoint
- sqlx to communicate with the db benefitting from scan to struct
Situation
We have a struct with a string slice type:
type User struct {
Friends []string `db:"friends"`
}
This works quite well with gqlgen as it translates straight forward to a [String]
in GraphQL (quick warning here: If the struct doesn’t match the graphql schema, gqlgen just doesn’t generate the code leaving you with a partially implemented interface of your endpoint).
Now let’s have a look at the database side of things. The schema representing our users will look like this:
CREATE TABLE IF NOT EXISTS users (
friends STRING[]
)
Notice the friends column having the type STRING[]
which is equivalent to STRING ARRAY
.
If if we now select from that table and want to scan into our User
struct, we are greeted with:
Error: sql: Scan error on column index 1, name “friends”: unsupported Scan, storing driver.Value type []uint8 into type *[]string
Apparently the driver returns a byte array.
Solution
This seems like a problem someone else had before, so a quick search suggest pq.StringArray
as the matching type. This leaves us with a new struct definition:
type User struct {
Friends pq.StringArray `db:"friends"`
}
This solves the issue with scanning the column into the struct. Reason for this is the .Value
and .Scan
methods on the StringArray
type instructing sql how to scan the data from the database. More details on this can be found in the sqlx documentation.
Now when regenerating the GraphQL endpoint, our User
suddenly disappears. From our gqlgen configuration we know that we provide the model for the User
:
models:
User:
model: github.com/user/package/model.User
so when we generate the GraphQL endpoint, gqlgen fails as the pq.StringArray
doesn’t match the [String]
in the schema definition. In order to fix this, we have to update our GraphQL schema:
type User {
friends: StringArray
}
StringArray
is a custom scalar which we can define as scalar StringArray
in the schema. StringArray
is not defined yet so we point gqlgen to the correct place via the configuration:
StringArray:
model: github.com/user/package/gql.StringArray
Where qgl
is my package containing everything gqlgen. Types are sorted now and we get back our User
in the GraphQL schema. The next problem is that gqlgen doesn’t know how to serialize from or to the StringArray
type. This problem can be solved by providing an external marshaler which is very well described in the gqlgen documentation.
func MarshalStringArray(a pq.StringArray) graphql.Marshaler {
return graphql.WriterFunc(func(w io.Writer) {
data, _ := json.Marshal(a)
io.WriteString(w, string(data))
})
}
func UnmarshalStringArray(v interface{}) (pq.StringArray, error) {
a, ok := v.(pq.StringArray)
if !ok {
return nil, errors.New("failed to cast to pq.StringArray")
}
return a, nil
}
The data, _ := json.Marshal(a)
line is for convenience and asks for a more sophisticated solution.
Now all those steps together will provide you with a single model which can be used by gqlgen to generate the GraphQL endpoint and by sqlx to write and scan.