turash/dev_guides/02_neo4j_driver.md
Damir Mukimov 4a2fda96cd
Initial commit: Repository setup with .gitignore, golangci-lint v2.6.0, and code quality checks
- Initialize git repository
- Add comprehensive .gitignore for Go projects
- Install golangci-lint v2.6.0 (latest v2) globally
- Configure .golangci.yml with appropriate linters and formatters
- Fix all formatting issues (gofmt)
- Fix all errcheck issues (unchecked errors)
- Adjust complexity threshold for validation functions
- All checks passing: build, test, vet, lint
2025-11-01 07:36:22 +01:00

490 lines
12 KiB
Markdown

# Neo4j Go Driver Development Guide
**Library**: `github.com/neo4j/neo4j-go-driver/v5`
**Used In**: MVP - Graph database for resource matching
**Purpose**: Connect to Neo4j and execute Cypher queries
---
## Where It's Used
- Store graph structure (Business → Site → ResourceFlow)
- Query matches between ResourceFlows
- Create and manage relationships
- Graph traversal for matching algorithm
---
## Official Documentation
- **GitHub**: https://github.com/neo4j/neo4j-go-driver
- **Official Docs**: https://neo4j.com/docs/driver-manual/current/drivers/go/
- **GoDoc**: https://pkg.go.dev/github.com/neo4j/neo4j-go-driver/v5
- **Cypher Query Language**: https://neo4j.com/docs/cypher-manual/current/
---
## Installation
```bash
go get github.com/neo4j/neo4j-go-driver/v5
```
---
## Key Concepts
### 1. Driver Initialization
```go
import (
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"context"
)
func NewNeo4jDriver(uri, username, password string) (neo4j.DriverWithContext, error) {
driver, err := neo4j.NewDriverWithContext(
uri, // e.g., "neo4j://localhost:7687"
neo4j.BasicAuth(username, password, ""),
)
if err != nil {
return nil, err
}
// Verify connectivity
ctx := context.Background()
if err := driver.VerifyConnectivity(ctx); err != nil {
driver.Close(ctx)
return nil, err
}
return driver, nil
}
```
### 2. Session Management
```go
// Create session
session := driver.NewSession(ctx, neo4j.SessionConfig{
AccessMode: neo4j.AccessModeWrite, // or AccessModeRead
DatabaseName: "neo4j", // or your database name
})
defer session.Close(ctx)
// Execute query
result, err := session.Run(ctx, cypher, params)
```
### 3. Basic Cypher Queries
```go
// Create node
cypher := "CREATE (b:Business {id: $id, name: $name, email: $email}) RETURN b"
params := map[string]interface{}{
"id": uuid.New().String(),
"name": "Factory A",
"email": "contact@factorya.com",
}
result, err := session.Run(ctx, cypher, params)
if err != nil {
return err
}
// Process single record
record, err := result.Single(ctx)
if err != nil {
return err
}
node, _ := record.Get("b")
businessNode := node.(neo4j.Node)
```
### 4. Create Relationships
```go
// Match nodes and create relationship
cypher := `
MATCH (b:Business {id: $businessID})
MATCH (s:Site {id: $siteID})
CREATE (b)-[:OPERATES_AT]->(s)
RETURN b, s
`
params := map[string]interface{}{
"businessID": businessID,
"siteID": siteID,
}
_, err := session.Run(ctx, cypher, params)
```
### 5. Query with Results
```go
// Query multiple records
cypher := `
MATCH (b:Business)-[:OPERATES_AT]->(s:Site)
WHERE b.id = $businessID
RETURN b, s
`
params := map[string]interface{}{
"businessID": businessID,
}
result, err := session.Run(ctx, cypher, params)
if err != nil {
return err
}
var sites []Site
for result.Next(ctx) {
record := result.Record()
siteNode, _ := record.Get("s")
site := parseSiteNode(siteNode.(neo4j.Node))
sites = append(sites, site)
}
if err := result.Err(); err != nil {
return err
}
```
### 6. Transactions
```go
// Transaction with automatic commit/rollback
_, err = session.ExecuteWrite(ctx, func(tx neo4j.ManagedTransaction) (interface{}, error) {
// Create business
result, err := tx.Run(ctx,
"CREATE (b:Business {id: $id, name: $name}) RETURN b.id",
map[string]interface{}{"id": id, "name": name},
)
if err != nil {
return nil, err
}
record, err := result.Single(ctx)
if err != nil {
return nil, err
}
businessID, _ := record.Get("b.id")
// Create site in same transaction
_, err = tx.Run(ctx,
"MATCH (b:Business {id: $businessID}) CREATE (s:Site {id: $siteID}) CREATE (b)-[:OPERATES_AT]->(s)",
map[string]interface{}{"businessID": businessID, "siteID": siteID},
)
return businessID, err
})
```
### 7. Parameter Binding
```go
// Using struct for parameters
type BusinessParams struct {
ID string
Name string
Email string
}
params := BusinessParams{
ID: uuid.New().String(),
Name: "Factory A",
Email: "contact@factorya.com",
}
cypher := `
CREATE (b:Business {
id: $id,
name: $name,
email: $email
})
RETURN b
`
// Neo4j driver automatically converts struct to map
result, err := session.Run(ctx, cypher, params)
```
### 8. Extracting Values from Records
```go
record, _ := result.Single(ctx)
// Get by key
node, _ := record.Get("b")
relationship, _ := record.Get("r")
// Type assertions
businessNode := node.(neo4j.Node)
props := businessNode.Props
// Extract properties
id, _ := props["id"].(string)
name, _ := props["name"].(string)
// Or use helper function
func getString(record neo4j.Record, key string) string {
val, ok := record.Get(key)
if !ok {
return ""
}
str, _ := val.(string)
return str
}
```
---
## MVP-Specific Patterns
### Resource Flow Service
```go
type Neo4jService struct {
driver neo4j.DriverWithContext
}
func (s *Neo4jService) CreateResourceFlow(ctx context.Context, flow ResourceFlow) error {
session := s.driver.NewSession(ctx, neo4j.SessionConfig{
AccessMode: neo4j.AccessModeWrite,
})
defer session.Close(ctx)
cypher := `
MATCH (site:Site {id: $siteID})
CREATE (flow:ResourceFlow {
id: $id,
direction: $direction,
type: $type,
temperature_celsius: $temperature,
quantity_kwh_per_month: $quantity,
cost_per_kwh_euro: $cost
})
CREATE (site)-[:HOSTS]->(flow)
RETURN flow.id
`
params := map[string]interface{}{
"id": flow.ID,
"siteID": flow.SiteID,
"direction": flow.Direction,
"type": flow.Type,
"temperature": flow.TemperatureCelsius,
"quantity": flow.QuantityKwhPerMonth,
"cost": flow.CostPerKwhEuro,
}
_, err := session.Run(ctx, cypher, params)
return err
}
```
### Matching Query
```go
func (s *Neo4jService) FindMatches(ctx context.Context, flowID string, maxDistanceKm float64) ([]Match, error) {
session := s.driver.NewSession(ctx, neo4j.SessionConfig{
AccessMode: neo4j.AccessModeRead,
})
defer session.Close(ctx)
cypher := `
MATCH (sourceFlow:ResourceFlow {id: $flowID})-[:HOSTS]->(sourceSite:Site),
(targetFlow:ResourceFlow)-[:HOSTS]->(targetSite:Site)
WHERE sourceFlow.direction = 'output'
AND targetFlow.direction = 'input'
AND sourceFlow.type = 'heat'
AND targetFlow.type = 'heat'
AND ABS(sourceFlow.temperature_celsius - targetFlow.temperature_celsius) <= 10
WITH sourceFlow, targetFlow, sourceSite, targetSite,
point.distance(
point({longitude: sourceSite.longitude, latitude: sourceSite.latitude}),
point({longitude: targetSite.longitude, latitude: targetSite.latitude})
) / 1000 AS distance_km
WHERE distance_km <= $maxDistance
RETURN targetFlow.id AS target_flow_id,
targetFlow.temperature_celsius AS target_temp,
targetFlow.quantity_kwh_per_month AS target_quantity,
distance_km
ORDER BY distance_km ASC
LIMIT 20
`
params := map[string]interface{}{
"flowID": flowID,
"maxDistance": maxDistanceKm,
}
result, err := session.Run(ctx, cypher, params)
if err != nil {
return nil, err
}
var matches []Match
for result.Next(ctx) {
record := result.Record()
match := Match{
TargetFlowID: getString(record, "target_flow_id"),
TargetTemp: getFloat(record, "target_temp"),
TargetQuantity: getFloat(record, "target_quantity"),
DistanceKm: getFloat(record, "distance_km"),
}
matches = append(matches, match)
}
return matches, result.Err()
}
```
### Connection Pooling
```go
// Driver automatically manages connection pool
// Configure during driver creation
driver, err := neo4j.NewDriverWithContext(
uri,
neo4j.BasicAuth(username, password, ""),
func(config *neo4j.Config) {
config.MaxConnectionPoolSize = 50
config.ConnectionAcquisitionTimeout = 30 * time.Second
config.MaxTransactionRetryTime = 30 * time.Second
},
)
```
---
## Error Handling
```go
result, err := session.Run(ctx, cypher, params)
if err != nil {
// Check for specific Neo4j errors
if neo4jErr, ok := err.(*neo4j.Neo4jError); ok {
switch neo4jErr.Code {
case "Neo.ClientError.Statement.SyntaxError":
// Handle syntax error
case "Neo.ClientError.Security.Unauthorized":
// Handle auth error
default:
// Handle other errors
}
}
return err
}
// Check result errors
if err := result.Err(); err != nil {
return err
}
```
---
## Performance Tips
1. **Reuse sessions** - create session per request/operation, not per query
2. **Use transactions** - batch operations in single transaction
3. **Parameterize queries** - always use parameters, never string concatenation
4. **Create indexes** - for frequently queried properties
5. **Use LIMIT** - always limit query results
6. **Profile queries** - use `EXPLAIN` and `PROFILE` to optimize Cypher
---
## Indexes
```go
// Create indexes for better performance
indexes := []string{
"CREATE INDEX business_id IF NOT EXISTS FOR (b:Business) ON (b.id)",
"CREATE INDEX site_id IF NOT EXISTS FOR (s:Site) ON (s.id)",
"CREATE INDEX resource_flow_direction IF NOT EXISTS FOR (r:ResourceFlow) ON (r.direction)",
"CREATE INDEX resource_flow_type IF NOT EXISTS FOR (r:ResourceFlow) ON (r.type)",
}
for _, index := range indexes {
_, err := session.Run(ctx, index, nil)
if err != nil {
log.Printf("Failed to create index: %v", err)
}
}
```
---
## Tutorials & Resources
- **Neo4j Go Driver Examples**: https://github.com/neo4j/neo4j-go-driver/tree/5.0/examples
- **Getting Started Tutorial**: https://neo4j.com/developer/go/
- **Cypher Manual**: https://neo4j.com/docs/cypher-manual/current/
- **Best Practices**: https://neo4j.com/developer/go-driver/#_best_practices
---
## Common Patterns
### Repository Pattern
```go
type BusinessRepository struct {
driver neo4j.DriverWithContext
}
func (r *BusinessRepository) FindByID(ctx context.Context, id string) (*Business, error) {
session := r.driver.NewSession(ctx, neo4j.SessionConfig{
AccessMode: neo4j.AccessModeRead,
})
defer session.Close(ctx)
// ... query logic ...
}
func (r *BusinessRepository) Create(ctx context.Context, business *Business) error {
session := r.driver.NewSession(ctx, neo4j.SessionConfig{
AccessMode: neo4j.AccessModeWrite,
})
defer session.Close(ctx)
// ... create logic ...
}
```
### Helper Functions
```go
func parseBusinessNode(node neo4j.Node) *Business {
props := node.Props
return &Business{
ID: getString(props, "id"),
Name: getString(props, "name"),
Email: getString(props, "email"),
}
}
func getString(m map[string]interface{}, key string) string {
if val, ok := m[key]; ok {
if str, ok := val.(string); ok {
return str
}
}
return ""
}
func getFloat(m map[string]interface{}, key string) float64 {
if val, ok := m[key]; ok {
if f, ok := val.(float64); ok {
return f
}
}
return 0
}
```