Getting Started with ScalarDB GraphQL
ScalarDB GraphQL is an interface layer that allows client applications to communicate with a ScalarDB database with GraphQL.
In this Getting Started guide, you will run a GraphQL server on your local machine.
Prerequisites
We assume you have already installed Docker and have access to a ScalarDB-supported database such as Cassandra. Please configure them first by following Getting Started with ScalarDB if you have not set them up yet.
You need a Personal Access Token (PAT) to access the Docker image of ScalarDB GraphQL in GitHub Container registry since the image is private. Ask a person in charge to get your account ready. Please read the official document for more detail.
Set up a database schema
We use the following simple example schema.
emoney.json
{
"emoney.account": {
"transaction": true,
"partition-key": [
"id"
],
"clustering-key": [],
"columns": {
"id": "TEXT",
"balance": "INT"
}
}
}
To apply the schema to your database, download the Schema Loader that matches the version you use from scalardb releases, and run the following command to load the schema.
$ java -jar scalardb-schema-loader-<version>.jar --config /path/to/database.properties -f emoney.json --coordinator
Docker login
docker login
is required to start the ScalarDB GraphQL Docker image as described in the Prerequisites section.
# read:packages scope needs to be selected in a personal access token to login
$ export CR_PAT=YOUR_PERSONAL_ACCESS_TOKEN
$ echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin
Configure the GraphQL Server
Add the following properties to your database.properties
file. You can change the values as needed. Especially, make sure to set namespaces
property. The tables in the specified namespaces (it can be a comma-separated list) will be exposed by the GraphQL server.
scalar.db.graphql.port=8080
scalar.db.graphql.path=/graphql
scalar.db.graphql.namespaces=emoney
scalar.db.graphql.graphiql=true
Start up the GraphQL Server
The following command starts up the ScalarDB GraphQL server. The first time you run the command, it will download the Docker image from GitHub Container Registry.
$ docker run -v /path/to/database.properties:/scalardb-graphql/database.properties.tmpl -p 8080:8080 ghcr.io/scalar-labs/scalardb-graphql:<version>
At this point, the server reads the tables in the specified namespaces and generates a GraphQL schema to perform CRUD operations on the them.
Run operations from GraphiQL
If the server is configured with a property scalar.db.graphql.graphiql=true
(true by default), GraphiQL IDE will be available. When the above example properties are used, the endpoint URL of GraphiQL IDE is http://localhost:8080/graphql
. Opening that URL with your web browser will take you to the GraphiQL screen.
Let's insert the first record. In the left pane, paste the following mutation, then push the triangle-shaped Execute Query
button at the top of the window.
mutation PutUser1 {
account_put(put: {key: {id: "user1"}, values: {balance: 1000}})
}
ScalarDB GraphQL always runs queries with transactions. The above query starts a new transaction, executes a ScalarDB Put command, and commits the transaction at the end of the execution.
The following response from the GraphQL server will appear in the right pane.
{
"data": {
"account_put": true
},
"extensions": {
"transaction": {
"id": "d6b3ea78-f147-4950-969c-7b989b4168bc"
}
}
}
The "data"
field contains the result of the execution. This response shows the account_put
field of the mutation was successful. The result type of mutations is Boolean!
, which indicates whether the operation succeeded or not.
The "extensions"
field is used to return a transaction ID in which an operation is executed. We will discuss transaction IDs later in this document.
Next, let's get the record you just inserted. Paste the following query next to the previous mutation in the left pane, and click the Execute Query
button. Since you don't delete the mutation PutUser1
above, a pull-down will appear below the button, and you can choose which operation should be executed. Choose GetUser1
.
query GetUser1 {
account_get(get: {key: {id: "user1"}}) {
account {
id
balance
}
}
}
You should get the following result in the right pane.
{
"data": {
"account_get": {
"account": {
"id": "user1",
"balance": 1000
}
}
},
"extensions": {
"transaction": {
"id": "6917c4f5-54b1-405f-9b93-29dc41e41858"
}
}
}
Mappings between GraphQL API and ScalarDB Java API
The automatically generated GraphQL schema defines queries, mutations, and object types for input/output to allow you to run CRUD operations for all the tables in the specified namespaces. These operations are designed to match the ScalarDB APIs defined in the DistributedTransaction
interface.
Assuming you have an account
table in a namespace, the following queries and mutations will be generated.
ScalarDB API | GraphQL root type | GraphQL field |
---|---|---|
get(Get get) | Query | account_get(get: account_GetInput!): account_GetPayload |
scan(Scan scan) | Query | account_scan(scan: account_ScanInput!): account_ScanPayload |
put(Put put) | Mutation | account_put(put: account_PutInput!): Boolean! |
put(java.util.List<Put> puts) | Mutation | account_bulkPut(put: [account_PutInput!]!): Boolean! |
delete(Delete delete) | Mutation | account_delete(delete: account_DeleteInput!): Boolean! |
delete(java.util.List<Delete> deletes) | Mutation | account_bulkDelete(delete: [account_DeleteInput!]!): Boolean! |
mutate(java.util.List<? extends Mutation> mutations) | Mutation | account_mutate(put: [account_PutInput!]delete: [account_DeleteInput!]): Boolean! |
Note that the scan
field is not generated for a table with no clustering key. This is the reason why account_scan
field is not available in our emoney
example in this document.
You can see all generated GraphQL types in GraphiQL's Documentation Explorer (the < Docs
link at the top-right corner).
Transaction across multiple requests
This section describes how to run a transaction that spans multiple GraphQL requests.
The generated schema provides the @transaction
directive that allows you to identify transactions. This directive can be used with both queries and mutations.
Start a transaction before running an operation
Adding a @transaction
directive with no arguments to a query or a mutation directs the execution to start a new transaction.
query GetAccounts @transaction {
user1: account_get(get: {key: {id: "user1"}}) {
account { balance }
}
user2: account_get(get: {key: {id: "user2"}}) {
account { balance }
}
}
You will get a result with a transaction ID in the extensions
field. The id
value in the extensions is the transaction ID in which the operation in the request was run. In this case, this is the new ID of the transaction just started by the request.
{
"data": {
"user1": {
"account": {
"balance": 1000
}
},
"user2": {
"account": {
"balance": 1000
}
}
},
"extensions": {
"transaction": {
"id": "c88da8a6-a13f-4857-82fe-45f1ab4150f9"
}
}
}
Run an operation in a continued transaction
To run the next queries or mutations in the transaction you started, specify the transaction ID as the id
argument of the @transaction
. The following example will update two accounts you got in the previous example in the same transaction. This represents a transfer of balance from user1's account to user2's account.
mutation Transfer @transaction(id: "c88da8a6-a13f-4857-82fe-45f1ab4150f9") {
user1: account_put(put: {key: {id: "user1"}, values: {balance: 750}})
user2: account_put(put: {key: {id: "user2"}, values: {balance: 1250}})
}
Note that a transaction started with GraphQL has a timeout of 1 minute and will be aborted automatically when it exceeds the timeout.
Commit a transaction
To commit the continued transaction, specify both the id
and the commit: true
flag as arguments of the @transaction
directive.
query GetAndCommit @transaction(id: "c88da8a6-a13f-4857-82fe-45f1ab4150f9", commit: true) {
user1: account_get(get: {key: {id: "user1"}}) {
account { balance }
}
user2: account_get(get: {key: {id: "user2"}}) {
account { balance }
}
}
Note: If you specify a commit: true
flag without an id
argument like @transaction(commit: true)
, a new transaction is started and committed just for one operation. This is exactly the same as not specifying the @transaction
directive, as seen in the above examples using GraphiQL. In other words, you can omit the directive itself when it is @transaction(commit: true)
.
Abort a transaction
When you need to abort a transaction explicitly, you can use the abort
mutation field. Note that you cannot mix it with any other operations, so you must specify it alone.
mutation AbortTx @transaction(id: "c88da8a6-a13f-4857-82fe-45f1ab4150f9") {
abort
}