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
.
MATCH (person:Person)-[actedIn:ACTED_IN]->(movie:Movie {title: $title})
RETURN person, actedIn, movie
The returned value can be represented in a 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()
.
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.
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:
-
An execution context
-
A
neo4j.Result
-
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.
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.
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:
-
The casted value, or an empty value
-
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) -
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 |
---|---|---|---|---|
|
Node is returned |
An instance of |
|
|
|
No node is returned (e.g. Using |
an empty |
|
|
|
Not the specified type |
an empty |
|
an |
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.
// 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
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
andnil
-
❏
neo4j.Record
andnil
-
❏ 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.
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.