Skip to content

Latest commit

 

History

History
646 lines (521 loc) · 18.3 KB

index.md

File metadata and controls

646 lines (521 loc) · 18.3 KB

+++ date = "2017-03-20T19:35:35+11:00" title = "Clients" +++

Implementation

{{% notice "note" %}} All mutations and queries run within the context of a transaction. This differs significantly from the interaction model pre v0.9. {{% /notice %}}

Clients can communicate with the server in two different ways:

  • Via gRPC. Internally this uses Protocol Buffers (the proto file used by Dgraph is located at api.proto).

  • Via HTTP. There are various endpoints, each accepting and returning JSON. There is a one to one correspondence between the HTTP endpoints and the gRPC service methods.

It's possible to interface with dgraph directly via gRPC or HTTP. However, if a client library exists for you language, this will be an easier option.

{{% notice "tip" %}} For multi-node setups, predicates are assigned to the group that first sees that predicate. Dgraph also automatically moves predicate data to different groups in order to make the predicate distribution more even. This occurs automatically every 10 minutes. It's possible for clients to aid this process by by communicating with all dgraph instances. For the Go client, this means passing in one *grpc.ClientConn per dgraph instance. Mutations will be made in a round robin fashion, resulting in an initially semi random predicate distribution. {{% /notice %}}

Go

GoDoc

The go client communicates with the server on the grpc port (default value 9080).

The client can be obtained in the usual way via go get:

go get -u -v github.com/dgraph-io/dgo

The full GoDoc contains documentation for the client API along with examples showing how to use it.

Create the client

To create a client, dial a connection to Dgraph's external Grpc port (typically 9080). The following code snippet shows just one connection. You can connect to multiple Dgraph servers to distribute the workload evenly.

func newClient() *dgo.Dgraph {
	// Dial a gRPC connection. The address to dial to can be configured when
	// setting up the dgraph cluster.
	d, err := grpc.Dial("localhost:9080", grpc.WithInsecure())
	if err != nil {
		log.Fatal(err)
	}

	return dgo.NewDgraphClient(
		api.NewDgraphClient(d),
	)
}

Alter the database

To set the schema, set it on a api.Operation object, and pass it down to the Alter method.

func setup(c *dgo.Dgraph) {
	// Install a schema into dgraph. Accounts have a `name` and a `balance`.
	err := c.Alter(context.Background(), &api.Operation{
		Schema: `
			name: string @index(term) .
			balance: int .
		`,
	})
}

api.Operation contains other fields as well, including drop predicate and drop all. Drop all is useful if you wish to discard all the data, and start from a clean slate, without bringing the instance down.

	// Drop all data including schema from the dgraph instance. This is useful
	// for small examples such as this, since it puts dgraph into a clean
	// state.
	err := c.Alter(context.Background(), &api.Operation{DropAll: true})

Create a transaction

Dgraph v0.9 supports running distributed ACID transactions. To create a transaction, just call c.NewTxn(). This operation incurs no network call. Typically, you'd also want to call a defer txn.Discard() to let it automatically rollback in case of errors. Calling Discard after Commit would be a no-op.

func runTxn(c *dgo.Dgraph) {
	txn := c.NewTxn()
	defer txn.Discard()
	...
}

Run a query

You can run a query by calling txn.Query. The response would contain a JSON field, which has the JSON encoded result. You can unmarshal it into Go struct via json.Unmarshal.

	// Query the balance for Alice and Bob.
	const q = `
		{
			all(func: anyofterms(name, "Alice Bob")) {
				uid
				balance
			}
		}
	`
	resp, err := txn.Query(context.Background(), q)
	if err != nil {
		log.Fatal(err)
	}

	// After we get the balances, we have to decode them into structs so that
	// we can manipulate the data.
	var decode struct {
		All []struct {
			Uid     string
			Balance int
		}
	}
	if err := json.Unmarshal(resp.GetJson(), &decode); err != nil {
		log.Fatal(err)
	}

Run a mutation

txn.Mutate would run the mutation. It takes in a api.Mutation object, which provides two main ways to set data: JSON and RDF N-Quad. You can choose whichever way is convenient.

We're going to continue using JSON. You could modify the Go structs parsed from the query, and marshal them back into JSON.

	// Move $5 between the two accounts.
	decode.All[0].Bal += 5
	decode.All[1].Bal -= 5

	out, err := json.Marshal(decode.All)
	if err != nil {
		log.Fatal(err)
	}

	_, err := txn.Mutate(context.Background(), &api.Mutation{SetJson: out})

Sometimes, you only want to commit mutation, without querying anything further. In such cases, you can use a CommitNow field in api.Mutation to indicate that the mutation must be immediately committed.

Commit the transaction

Once all the queries and mutations are done, you can commit the transaction. It returns an error in case the transaction could not be committed.

	// Finally, we can commit the transactions. An error will be returned if
	// other transactions running concurrently modify the same data that was
	// modified in this transaction. It is up to the library user to retry
	// transactions when they fail.

	err := txn.Commit(context.Background())

Complete Example

This is an example from the GoDoc. It shows how to to create a Node with name Alice, while also creating her relationships with other nodes. Note loc predicate is of type geo and can be easily marshalled and unmarshalled into a Go struct. More such examples are present as part of the GoDoc.

type School struct {
	Name string `json:"name,omitempty"`
}

type loc struct {
	Type   string    `json:"type,omitempty"`
	Coords []float64 `json:"coordinates,omitempty"`
}

// If omitempty is not set, then edges with empty values (0 for int/float, "" for string, false
// for bool) would be created for values not specified explicitly.

type Person struct {
		Uid      string     `json:"uid,omitempty"`
		Name     string     `json:"name,omitempty"`
		Age      int        `json:"age,omitempty"`
		Dob      *time.Time `json:"dob,omitempty"`
		Married  bool       `json:"married,omitempty"`
		Raw      []byte     `json:"raw_bytes",omitempty`
		Friends  []Person   `json:"friend,omitempty"`
		Location loc        `json:"loc,omitempty"`
		School   []School   `json:"school,omitempty"`
}

conn, err := grpc.Dial("127.0.0.1:9080", grpc.WithInsecure())
if err != nil {
	log.Fatal("While trying to dial gRPC")
}
defer conn.Close()

dc := api.NewDgraphClient(conn)
dg := dgo.NewDgraphClient(dc)

op := &api.Operation{}
op.Schema = `
	name: string @index(exact) .
	age: int .
	married: bool .
	loc: geo .
	dob: datetime .
`

ctx := context.Background()
err = dg.Alter(ctx, op)
if err != nil {
	log.Fatal(err)
}

dob := time.Date(1980, 01, 01, 23, 0, 0, 0, time.UTC)
// While setting an object if a struct has a Uid then its properties in the graph are updated
// else a new node is created.
// In the example below new nodes for Alice, Bob and Charlie and school are created (since they
// dont have a Uid).
p := Person{
	Name:    "Alice",
	Age:     26,
	Married: true,
	Location: loc{
		Type:   "Point",
		Coords: []float64{1.1, 2},
	},
	Dob: &dob,
	Raw: []byte("raw_bytes"),
	Friends: []Person{{
		Name: "Bob",
		Age:  24,
	}, {
		Name: "Charlie",
		Age:  29,
	}},
	School: []School{{
		Name: "Crown Public School",
	}},
}

mu := &api.Mutation{
	CommitNow: true,
}
pb, err := json.Marshal(p)
if err != nil {
	log.Fatal(err)
}

mu.SetJson = pb
assigned, err := dg.NewTxn().Mutate(ctx, mu)
if err != nil {
	log.Fatal(err)
}

// Assigned uids for nodes which were created would be returned in the resp.AssignedUids map.
variables := map[string]string{"$id": assigned.Uids["blank-0"]}
q := `query Me($id: string){
	me(func: uid($id)) {
		name
		dob
		age
		loc
		raw_bytes
		married
		friend @filter(eq(name, "Bob")){
			name
			age
		}
		school {
			name
		}
	}
}`

resp, err := dg.NewTxn().QueryWithVars(ctx, q, variables)
if err != nil {
	log.Fatal(err)
}

type Root struct {
	Me []Person `json:"me"`
}

var r Root
err = json.Unmarshal(resp.Json, &r)
if err != nil {
	log.Fatal(err)
}
// fmt.Printf("Me: %+v\n", r.Me)
// R.Me would be same as the person that we set above.

fmt.Println(string(resp.Json))
// Output: {"me":[{"name":"Alice","dob":"1980-01-01T23:00:00Z","age":26,"loc":{"type":"Point","coordinates":[1.1,2]},"raw_bytes":"cmF3X2J5dGVz","married":true,"friend":[{"name":"Bob","age":24}],"school":[{"name":"Crown Public School"}]}]}

Java

The official Java client can be found here and it fully supports Dgraph v0.9.0. Follow the instructions in the README to get it up and running.

We also have a DgraphJavaSample project, which contains an end-to-end working example of how to use the Java client.

Javascript

The official Javascript client can be found here and it fully supports Dgraph v0.9.4. Follow the instructions in the README to get it up and running.

We also have a simple example project, which contains an end-to-end working example of how to use the Javascript client, for Node.js >= v6.

Python

The official Python client can be found here and it fully supports Dgraph v1.0.0 and Python versions >= 2.7 and >= 3.5. Follow the instructions in the README to get it up and running.

We also have a simple example project, which contains an end-to-end working example of how to use the Python client.

Raw HTTP

{{% notice "warning" %}} Raw HTTP needs more chops to use than our language clients. We wrote this to be a guide to help you build Dgraph client in a new language. {{% /notice %}}

It's also possible to interact with dgraph directly via its HTTP endpoints. This allows clients to be built for languages that don't have access to a working gRPC implementation.

In the examples shown here, regular command line tools such as curl and jq are used. However, the real intention here is to show other programmers how they could implement a client in their language on top of the HTTP API.

Similar to the Go client example, we use a bank account transfer example.

Create the Client

A client built on top of the HTTP API will need to track state at two different levels:

  1. Per client. Each client will need to keep a linearized reads (lin_read) map. This is a map from dgraph group id to proposal id. This will be needed for the system as a whole (client + server) to have linearizability. Whenever a lin_read map is received in a server response (for any transaction), the client should update its version of the map by merging the two maps together. The merge operation is simple - the new map gets all key/value pairs from the parent maps. Where a key exists in both maps, the max value is taken. The client's initial lin_read is should be an empty map.

  2. Per transaction. There are three pieces of state that need to be maintained for each transaction.

    1. Each transaction needs its own lin_read (updated independently of the client level lin_read). Any lin_read maps received in server responses associated with the transaction should be merged into the transactions lin_read map.

    2. A start timestamp (start_ts). This uniquely identifies a transaction, and doesn't change over the transaction lifecycle.

    3. The set of keys modified by the transaction (keys). This aids in transaction conflict detection.

{{% notice "note" %}} On a dgraph set up with no replication, there is no need to track lin_read. It can be ignored in responses received from dgraph and doesn't need to be sent in any requests. {{% /notice %}}

Alter the database

The /alter endpoint is used to create or change the schema. Here, the predicate name is the name of an account. It's indexed so that we can look up accounts based on their name.

curl -X POST localhost:8080/alter -d 'name: string @index(term) .'

If all goes well, the response should be {"code":"Success","message":"Done"}.

Other operations can be performed via the /alter endpoint as well. A specific predicate or the entire database can be dropped.

E.g. to drop the predicate name:

curl -X POST localhost:8080/alter -d '{"drop_attr": "name"}'

To drop all data and schema:

curl -X POST localhost:8080/alter -d '{"drop_all": true}'

Start a transaction

Assume some initial accounts with balances have been populated. We now want to transfer money from one account to the other. This is done in four steps:

  1. Create a new transaction.

  2. Inside the transaction, run a query to determine the current balances.

  3. Perform a mutation to update the balances.

  4. Commit the transaction.

Starting a transaction doesn't require any interaction with dgraph itself. Some state needs to be set up for the transaction to use. The transaction's lin_read is initialized by copying the client's lin_read. The start_ts can initially be set to 0. keys can start as an empty set.

For both query and mutation if the start_ts is provided as a path parameter, then the operation is performed as part of the ongoing transaction else a new transaction is initiated.

Run a query

To query the database, the /query endpoint is used. We need to use the transaction scoped lin_read. Assume that lin_read is {"1": 12}.

To get the balances for both accounts:

curl -X POST -H 'X-Dgraph-LinRead: {"1": 12}' localhost:8080/query -d $'
{
  balances(func: anyofterms(name, "Alice Bob")) {
    uid
    name
    balance
  }
}' | jq

The result should look like this:

{
  "data": {
    "balances": [
      {
        "uid": "0x1",
        "name": "Alice",
        "balance": "100"
      },
      {
        "uid": "0x2",
        "name": "Bob",
        "balance": "70"
      }
    ]
  },
  "extensions": {
    "server_latency": {
      "parsing_ns": 70494,
      "processing_ns": 697140,
      "encoding_ns": 1560151
    },
    "txn": {
      "start_ts": 4,
      "lin_read": {
        "ids": {
          "1": 14
        }
      }
    }
  }
}

Notice that along with the query result under the data field, there is some additional data in the extensions -> txn field. This data will have to be tracked by the client.

First, there is a start_ts in the response. This start_ts will need to be used in all subsequent interactions with dgraph for this transaction, and so should become part of the transaction state.

Second, there is a new lin_read map. The lin_read map should be merged with both the client scoped and transaction scoped lin_read maps. Recall that both the transaction scoped and client scoped lin_read maps are {"1": 12}. The lin_read in the response is {"1": 14}. The merged result is {"1": 14}, since we take the max all of the keys.

Run a Mutation

Now that we have the current balances, we need to send a mutation to dgraph with the updated balances. If Bob transfers $10 to Alice, then the RDFs to send are:

<0x1> <balance> "110" .
<0x2> <balance> "60" .

Note that we have to to refer to the Alice and Bob nodes by UID in the RDF format.

We now send the mutations via the /mutate endpoint. We need to provide our transaction start timestamp as a path parameter, so that dgraph knows which transaction the mutation should be part of.

curl -X POST localhost:8080/mutate/4 -d $'
{
  set {
    <0x1> <balance> "110" .
    <0x2> <balance> "60" .
  }
}
' | jq

The result:

{
  "data": {
    "code": "Success",
    "message": "Done",
    "uids": {}
  },
  "extensions": {
    "txn": {
      "start_ts": 4,
      "keys": [
        "AAALX3ByZWRpY2F0ZV8AAAAAAAAAAAI=",
        "AAAHYmFsYW5jZQAAAAAAAAAAAg==",
        "AAALX3ByZWRpY2F0ZV8AAAAAAAAAAAE=",
        "AAAHYmFsYW5jZQAAAAAAAAAAAQ=="
      ],
      "lin_read": {
        "ids": {
          "1": 17
        }
      }
    }
  }
}

We get another lin_read map, which needs to be merged (the new lin_read map for both the client and transaction becomes {"1": 17}). We also get some keys. These should be added to the set of keys stored in the transaction state.

Committing the transaction

{{% notice "note" %}} It's possible to commit immediately after a mutation is made (without requiring to use the /commit endpoint as explained in this section). To do this, add the X-Dgraph-CommitNow: true header to the final /mutate call. {{% /notice %}}

Finally, we can commit the transaction using the /commit endpoint. We need the start_ts we've been using for the transaction along with the keys. If we had performed multiple mutations in the transaction instead of the just the one, then the keys provided during the commit would be the union of all keys returned in the responses from the /mutate endpoint.

curl -X POST localhost:8080/commit/4 -d $'
  [
    "AAALX3ByZWRpY2F0ZV8AAAAAAAAAAAI=",
    "AAAHYmFsYW5jZQAAAAAAAAAAAg==",
    "AAALX3ByZWRpY2F0ZV8AAAAAAAAAAAE=",
    "AAAHYmFsYW5jZQAAAAAAAAAAAQ=="
  ]' | jq
{
  "data": {
    "code": "Success",
    "message": "Done"
  },
  "extensions": {
    "txn": {
      "start_ts": 4,
      "commit_ts": 5
    }
  }
}

The transaction is now complete.

If another client were to perform another transaction concurrently affecting the same keys, then it's possible that the transaction would not be successful. This is indicated in the response when the commit is attempted.

{
  "errors": [
    {
      "code": "Error",
      "message": "Transaction aborted"
    }
  ]
}

In this case, it should be up to the user of the client to decide if they wish to retry the transaction.