Type Safety

In the previous lesson, we looked at how Neo4j types are used in Go. The driver provides a set of type-safe functions which guarantee the type of value returned by the function. These functions protect against unwanted side effects when dealing with the data returned by the database.

Golang Generics

You can learn more about Generics in Go in the go.dev docs.

Neo4j Types to Go Types

Let’s start with the following Cypher statement which finds all people who have acted in a movie. The Cypher statement returns two neo4j.Node types, person and movie, and one neo4j.Relationship.

cypher
Actors for a Movie
MATCH (person:Person)-[actedIn:ACTED_IN]->(movie:Movie {title: $title})
RETURN person, actedIn, movie

The returned value can be represented in a struct.

go
Representing Results in a Go struct
type personActedInMovie struct {
	person  neo4j.Node
	actedIn neo4j.Relationship
	movie   neo4j.Node
}

This struct can then be used to define value returned by session.Run(), neo4j.ExecuteRead() and neo4j.ExecuteWrite().

go
Using the struct
people, err := neo4j.ExecuteRead(
	ctx,
	session,
	func(tx neo4j.ManagedTransaction) ([]personActedInMovie, error) {
		result, err := tx.Run(ctx, cypher, params)
		if err != nil {
			return nil, err
		}
		return neo4j.CollectTWithContext(
			ctx,
			result,
			func(record *neo4j.Record) (personActedInMovie, error) {
				person, isNil, err := neo4j.GetRecordValue[neo4j.Node](record, "person")

				if isNil {
					fmt.Println("person value is nil")
				}

				if err != nil {
					return personActedInMovie{}, err
				}

				actedIn, _, err := neo4j.GetRecordValue[neo4j.Relationship](record, "actedIn")
				if err != nil {
					return personActedInMovie{}, err
				}

				movie, _, err := neo4j.GetRecordValue[neo4j.Node](record, "movie")
				if err != nil {
					return personActedInMovie{}, err
				}

				return personActedInMovie{person, actedIn, movie}, nil
			},
		)
	},
)

In the code sample above, the neo4j.CollectTWithContext() function is used to return a slice of personActedInMovie structs. The code uses the neo4j.GetRecordValue() helper function to extract a value from each record.

Let’s look at the functions used in this sample in more detail.

CollectTWithContext

The neo4j.CollectTWithContext() function is used to iterate through the records held in a neo4j.Result object and abstract any required values.

go
Using CollectTWithContext
neo4j.CollectTWithContext(
	ctx, // (1)
	result, // (2)
	func(record *neo4j.Record) (T any, error) { // (3)
		// Use `neo4j.GetRecordValue` to access values
	},
)

The function accepts three arguments:

  1. An execution context

  2. A neo4j.Result

  3. A callback function that is called once per record in the buffer. The function is passed one argument, a neo4j.Record which is used to access the values in each record.

In the code sample, T represents the type of value that the function will return.

The output of the function is a slice of values returned by the callback function.

SingleTWithContext

Alternatively, if you expect one result, you can use the neo4j.SingleTWithContext() function. The function signature is similar, but instead the function will return a single value instead of a slice.

go
Using SingleTWithContext
neo4j.SingleTWithContext(

	ctx, // (1)
	result, // (2)
	func(record *neo4j.Record) (T any, error) { // (3)
		// Use `neo4j.GetRecordValue` to access values
	},

)

For Single Results Only

If the result contains zero or more than one result, the function will return an error.

GetRecordValue

The neo4j.GetRecordValue() function allows you to access a value from a neo4j.Record by its key and guarantee the type of value returned.

go
Using GetRecordValue
person, isNil, err := neo4j.GetRecordValue[neo4j.Node](record, "person")

The function above extracts the person value from the record variable, cast as a neo4j.Node.

The function returns three values:

  1. The casted value, or an empty value

  2. A boolean to represent whether the returned value is included in the record or not. This value should be used to disambiguate from any returned default values such as 0 or empty strings)

  3. An error will be returned if the value does not exist on the record or the value cannot be cast as the requested type.

The following table illustrates what will be returned depending on whether the value has been successfully cast, is nil or cannot be cast as the requested type.

Returned value Outcome person isNil err

(:Person {name: "Arya"})

Node is returned

An instance of neo4j.Node

false

nil

nil

No node is returned (e.g. Using OPTIONAL MATCH)

an empty neo4j.Node

true

nil

"string"

Not the specified type

an empty neo4j.Node

false

an error

GetProperty

The code sample at the start of this lesson returns a personActedInMovie struct, which contains neo4j.Node and neo4j.Relationship types. Properties of both nodes and relationships can be accessed using the neo4j.GetProperty functions.

go
Accessing Node & Relationship Properties
// Get a Node Property
name, err := neo4j.GetProperty[string](node, "name")
fmt.Println("Actor name is ", name) // Actor name is Tom Hanks

// Get a Relationship Property
roles, err := neo4j.GetProperty[[]any](rel, "roles")
fmt.Println("They play ", roles) // They Play ["Woody"]

If the property exists and adheres to the type specification, the value is returned. If the property does not exist or doesn’t adhere to the type specification, an error is returned.

A Working Example

Expand to show a full working example
go
Full CollectTWithContext example
func collectTExample() {
	// Create a Driver Instance
	ctx := context.Background()
	driver, err := neo4j.NewDriverWithContext(
		"neo4j+s://dbhash.databases.neo4j.io",    // (1)
		neo4j.BasicAuth("neo4j", "letmein!", ""), // (2)
	)
	PanicOnErr(err)
	defer PanicOnClosureError(ctx, driver)

	// Open a new Session
	session := driver.NewSession(ctx, neo4j.SessionConfig{})
	defer PanicOnClosureError(ctx, session)

	// Define Query
	cypher := `
		MATCH (person:Person)-[actedIn:ACTED_IN]->(movie:Movie {title: $title})
		RETURN person, actedIn, movie
	`
	params := map[string]any{"title": "The Matrix"}

	people, err := neo4j.ExecuteRead(
		ctx,
		session,
		func(tx neo4j.ManagedTransaction) ([]personActedInMovie, error) {
			result, err := tx.Run(ctx, cypher, params)
			if err != nil {
				return nil, err
			}
			return neo4j.CollectTWithContext(
				ctx,
				result,
				func(record *neo4j.Record) (personActedInMovie, error) {
					person, isNil, err := neo4j.GetRecordValue[neo4j.Node](record, "person")

					if isNil {
						fmt.Println("person value is nil")
					}

					if err != nil {
						return personActedInMovie{}, err
					}

					actedIn, _, err := neo4j.GetRecordValue[neo4j.Relationship](record, "actedIn")
					if err != nil {
						return personActedInMovie{}, err
					}

					movie, _, err := neo4j.GetRecordValue[neo4j.Node](record, "movie")
					if err != nil {
						return personActedInMovie{}, err
					}

					return personActedInMovie{person, actedIn, movie}, nil
				},
			)
		},
	)

	fmt.Println(people)

	// First Row
	first := people[0]

	node := first.person
	rel := first.actedIn

	// Get a Node Property
	name, err := neo4j.GetProperty[string](node, "name")
	fmt.Println("Actor name is ", name) // Actor name is Tom Hanks

	// Get a Relationship Property
	roles, err := neo4j.GetProperty[[]any](rel, "roles")
	fmt.Println("They play ", roles) // They Play ["Woody"]
}

Check Your Understanding

Using SingleTWithContext

If a neo4j.Result with more than one row is passed to the neo4j.SingleTWithContext(), what values will be returned?

  • ✓ a zero-value and error

  • nil and nil

  • neo4j.Record and nil

  • ❏ None, the application will exit

Hint

The neo4j.SingleTWithContext() function expects a result with exactly one row passed to it. If zero or more than one rows are contained in the result, an error will be returned.

Solution

Two things will happen. The first record from the result set will be processed and returned as the first value. As the function expects exactly one row, an error will be returned as the second value.

Complete The Code

Assuming that the movie variable is a type of neo4j.Node, which type should be used to extract the title property? The title property on a Movie node is a string.

Select the correct type from the dropdown to complete the code sample.

go
title, err := neo4j.GetProperty[/*select:string*/](movie, "title")
  • ❏ int

  • ✓ string

  • ❏ neo4j.Node

  • ❏ neo4j.String

Hint

Strings in Neo4j are directly mapped to the Go string type.

Solution

The correct answer is string.

Lesson Summary

In this challenge, you used your knowledge to create a driver instance and execute a Cypher statement.

In the next Challenge, you will modify the repository to read from the database.