Creating a Simple Go Application for CRUD Operations with Neo4j and Docker
Tags: go, neo4j, docker, crud
Published: 2023-03-30 12:01:02

Hi everyone 👋, today we'll create a simple Go application that demonstrates basic CRUD operations using Neo4j 🏢, a popular graph database, and Docker 🐳. This tutorial will guide you through the steps to set up a local Neo4j instance using Docker, connect to it from your Go application using the Neo4j Go driver 🚗, and perform CRUD operations on a sample data set. By the end of this tutorial, you'll have a solid understanding of how to use Neo4j with Go and Docker to build scalable and efficient graph-based applications 🚀. So, let's get started!


Neo4j is a popular graph database that allows users to store, manage, and retrieve data using graph-based queries 📊. Go is a popular programming language for building scalable and efficient applications 🚀. In this tutorial, we'll learn how to create a simple Go project that uses Neo4j and Docker to perform CRUD operations.


## Prerequisites
Before we begin, please ensure that you have the following installed on your system:

- Docker 🐳
- Go (version 1.16 or higher)
- The neo4j-go-driver package (version 4 or higher)

## Setting up a Neo4j Database with Docker 🐳
First, we need to set up a Neo4j database using Docker 🐳. Open your terminal and execute the following command:

`docker run --name neo4j -p 7687:7687 -p 7474:7474 -e NEO4J_AUTH=neo4j/test1234 neo4j:latest
`
This command will create a new Docker container with a Neo4j database running on port 7687 (for Bolt) and 7474 (for HTTP)🚀. We're also setting the NEO4J_AUTH environment variable to neo4j/test1234 to specify the initial password for the neo4j user.


## Creating a New Go Project
Next, we need to create a new Go project. Create a new directory for the project and navigate into it:

```
mkdir my-neo4j-project
cd my-neo4j-project
```


Create a new Go module for the project:

```
go mod init my-neo4j-project
```
This will create a new go.mod file in the project directory.

Now, let's take a look at the code! 🔍
 
### Import statements

```
import (
	"fmt"
	"github.com/neo4j/neo4j-go-driver/v4/neo4j"
	"log"
)
```
The import statements include the necessary packages to interact with Neo4j database and log errors.

###  Create a person struct

```
type Person struct {
	ID   int64
	Name string
	Age  int
}
```

The Person struct represents the structure of a "Person" node in the Neo4j database 🧑. It has an ID, name, and age field.


### Write the Main function

```
func main() {
	// create a new Neo4j driver
	driver, err := neo4j.NewDriver("bolt://localhost:7687", neo4j.BasicAuth("neo4j", "test1234", ""))
	if err != nil {
		log.Fatalf("Failed to create Neo4j driver: %v", err)
	}
	defer driver.Close()
```

The main function is the entry point of the application. It creates a new Neo4j driver instance by providing the bolt URL and authentication credentials 🔐.


### Create a Person

Let's start by creating a new person. We define a createPerson function that takes in the Neo4j driver instance, the person's name, and age as arguments. Inside the function, we create a new session using the driver and then execute a Cypher query to create a new Person node with the given name and age. We then extract the ID of the created node from the result and return a new Person struct with the ID, name, and age.

```
func createPerson(driver neo4j.Driver, name string, age int) (*Person, error) {
	session := driver.NewSession(neo4j.SessionConfig{})
	defer session.Close()

	result, err := session.Run(
		"CREATE (p:Person {name: $name, age: $age}) RETURN id(p)",
		map[string]interface{}{"name": name, "age": age},
	)
	if err != nil {
		return nil, err
	}

	record, err := result.Single()
	if err != nil {
		return nil, err
	}

	id, ok := record.Values[0].(int64)
	if !ok {
		return nil, fmt.Errorf("invalid ID type")
	}

	return &Person{ID: id, Name: name, Age: age}, nil
}
```

### Get a Person by Name
Next, we define a getPersonByName function that takes in the Neo4j driver instance and a person's name as arguments. Inside the function, we create a new session using the driver and then execute a Cypher query to find a Person node with the given name. We limit the result to one record and extract the ID and age of the node from the result. We then return a new Person struct with the ID, name, and age.

```
func getPersonByName(driver neo4j.Driver, name string) (*Person, error) {
	session := driver.NewSession(neo4j.SessionConfig{})
	defer session.Close()

	result, err := session.Run(
		"MATCH (p:Person) WHERE p.name = $name RETURN id(p), p.age LIMIT 1",
		map[string]interface{}{"name": name},
	)
	if err != nil {
		return nil, err
	}

	record, err := result.Single()
	if err != nil {
		return nil, err
	}

	id, ok := record.Values[0].(int64)
	if !ok {
		return nil, fmt.Errorf("invalid ID type")
	}

	age, ok := record.Values[1].(int64)
	if !ok {
		return nil, fmt.Errorf("invalid age type")
	}

	return &Person{ID: id, Name: name, Age: int(age)}, nil
}
```


### Get a Person by ID
We also define a getPersonByID function that takes in the Neo4j driver instance and a person's ID as arguments. Inside the function, we create a new session using the driver and then execute a Cypher query to find a Person node with the given ID. We extract the name and age of the node from the result and return a new Person struct with the ID, name, and age.

```
func getPersonByID(driver neo4j.Driver, id int64) (*Person, error) {
    session := driver.NewSession(neo4j.SessionConfig{})
    defer session.Close()

    result, err := session.Run(
        "MATCH (p:Person) WHERE id(p) = $id RETURN p.name, p.age",
        map[string]interface{}{"id

```
### Update person age
The updatePersonAge function takes in a Neo4j driver object, the ID of the person whose age needs to be updated, and the new age. It then creates a new session using the driver and executes a Cypher query that updates the person's age based on the provided ID. The Cypher query starts with the MATCH keyword, which finds the person node with the given ID. The SET keyword is used to set the person node's age property to the new age provided. Finally, the RETURN keyword is used to retrieve the person node's name and updated age.

Once the Cypher query has executed successfully, the function constructs a Person struct containing the updated ID, name, and age. This Person struct is then returned by the function.

```
func updatePersonAge(driver neo4j.Driver, id int64, age int) (*Person, error) {
	session := driver.NewSession(neo4j.SessionConfig{})
	defer session.Close()

	result, err := session.Run(
		"MATCH (p:Person) WHERE id(p) = $id SET p.age = $age RETURN p.name, p.age",
		map[string]interface{}{"id": id, "age": age},
	)
	if err != nil {
		return nil, err
	}

	record, err := result.Single()
	if err != nil {
		return nil, err
	}

	name, ok := record.Values[0].(string)
	if !ok {
		return nil, fmt.Errorf("invalid name type")
	}

	newAge, ok := record.Values[1].(int64)
	if !ok {
		return nil, fmt.Errorf("invalid age type")
	}

	return &Person{ID: id, Name: name, Age: int(newAge)}, nil
}
```
### Delete person
The deletePerson function takes in a Neo4j driver object and the ID of the person to be deleted. It creates a new session using the driver and executes a Cypher query that deletes the person node based on the provided ID. The Cypher query starts with the MATCH keyword, which finds the person node with the given ID. The DELETE keyword is used to delete the person node.

If the deletion is successful, the function returns nil. Otherwise, it returns an error indicating that the deletion was unsuccessful.

```
func deletePerson(driver neo4j.Driver, id int64) error {
	session := driver.NewSession(neo4j.SessionConfig{})
	defer session.Close()

	_, err := session.Run(
		"MATCH (p:Person) WHERE id(p) = $id DELETE p",
		map[string]interface{}{"id": id},
	)
	if err != nil {
		return err
	}

	return nil
}

```
### Putting it All Together
Now that we have defined the necessary functions for CRUD operations, let's put them together in the main function. The main function demonstrates how to create, get, update, and delete a Person node in the Neo4j database.

```
func main() {
	// create a new Neo4j driver
	driver, err := neo4j.NewDriver("bolt://localhost:7687", neo4j.BasicAuth("neo4j", "test1234", ""))
	if err != nil {
		log.Fatalf("Failed to create Neo4j driver: %v", err)
	}
	defer driver.Close()

	// create a new person
	person, err := createPerson(driver, "Alice", 30)
	if err != nil {
		log.Fatalf("Failed to create person: %v", err)
	}
	log.Printf("Created person: %+v\n", person)

	// get the person by name
	personByName, err := getPersonByName(driver, "Alice")
	if err != nil {
		log.Fatalf("Failed to get person by name: %v", err)
	}
	log.Printf("Found person by name: %+v\n", personByName)

	// get the person by ID
	personByID, err := getPersonByID(driver, person.ID)
	if err != nil {
		log.Fatalf("Failed to get person by ID: %v", err)
	}
	log.Printf("Found person by ID: %+v\n", personByID)

	// update the person's age
	updatedPerson, err := updatePersonAge(driver, person.ID, 35)
	if err != nil {
		log.Fatalf("Failed to update person's age: %v", err)
	}
	log.Printf("Updated person: %+v\n", updatedPerson)

	// delete the person
	err = deletePerson(driver, person.ID)
	if err != nil {
		log.Fatalf("Failed to delete person: %v", err)
	}
	log.Printf("Deleted person with ID %d\n", person.ID)
}

```
The main function first creates a new Neo4j driver instance and then creates a new person node with the name "Alice" and age 30. It then retrieves the person node by name and ID and prints the results. The function then updates the person's age to 35 and prints the updated person. Finally, it deletes the person node and prints a message indicating that the person was deleted.


## Conclusion
In this tutorial, we learned how to create a simple Go application that uses Neo4j and Docker to perform CRUD operations on a sample data set. We first set up a Neo4j database using Docker and then defined the necessary functions for creating, getting, updating, and deleting a Person node in the Neo4j database. We then put these functions together in the main function to demonstrate how to perform CRUD operations on a sample data set.

By the end of this tutorial, you should have a solid understanding of how to use Neo4j with Go and Docker to build scalable and efficient graph-based applications. I hope you found this tutorial helpful, and please feel free to leave any comments or questions below! 👋