Verwendung von GORM mit Golang-Microservices und PostgreSQL
Bedeutung von Mikrodiensten
In der modernen Softwareentwicklung ist die Microservices-Architektur entscheidend für die Erstellung skalierbarer und wartbarer Anwendungen. Sie zerlegt komplexe Anwendungen in kleinere, unabhängige Dienste, die von verschiedenen Teams separat entwickelt, bereitgestellt und skaliert werden können. Dies erhöht die Agilität und Widerstandsfähigkeit.
Mit diesem architektonischen Ansatz geht jedoch die Herausforderung einher, Daten über die verteilten Dienste hinweg effizient zu verwalten. Eine effektive Datenbankinteraktion ist entscheidend, um sicherzustellen, dass Microservices optimal funktionieren. Hier kommt GORM ins Spiel, ein leistungsstarker Object-Relational Mapper (ORM) für Go.
In diesem Beitrag zeigen wir, wie du GORM mit Golang-Microservices und PostgreSQL verwenden kannst. Wir geben praktische Beispiele und bewährte Methoden, um die Entwicklungsgeschwindigkeit, Skalierbarkeit und Wartbarkeit zu steigern. Egal, ob du neu im Bereich Microservices bist oder deine bestehende Konfiguration verbessern möchtest. Dieser Leitfaden wird dir helfen, das Beste aus GORM, Golang und PostgreSQL in der modernen Softwareentwicklung herauszuholen.
Um diesem Leitfaden leichter folgen zu können, überprüfe bitte den Beispielcode, der hier entwickelt wurde.
Was ist ein ORM?
Stell dir zwei Welten vor: In der einen Welt befindet sich dein Anwendungscode mit Objekten, Klassen und Methoden; in der anderen Welt sind die Datenbanken, in denen deine Daten in Tabellen mit Zeilen und Spalten gespeichert sind. Diese beiden Welten sprechen unterschiedliche Sprachen und haben unterschiedliche Weisen, Informationen zu organisieren.
Ein ORM (Object-Relational Mapping) fungiert als Übersetzer zwischen diesen beiden Welten. Es ermöglicht dir, mit deiner Datenbank in der Sprache und Syntax deiner Programmiersprache zu interagieren. Anstatt komplexe SQL-Abfragen zu schreiben und die Ergebnisse manuell in Objekte in deinem Code zu überführen, kannst du Klassen und Strukturen definieren, die den Tabellen und Zeilen in deiner Datenbank entsprechen.
GORM
In Go verwenden wir das GORM-Paket, eine benutzerfreundliche ORM-Bibliothek. (siehe
GORM vereinfacht die Datenbankinteraktionen, indem es eine benutzerfreundliche API für CRUD-Operationen (Create, Read, Update, Delete), komplexe Abfragen und Transaktionsmanagement bereitstellt, während es gleichzeitig den Großteil des Boilerplate-Codes abstrahiert.
Erste Schritte mit GORM
Dieser Leitfaden setzt voraus, dass du über Grundkenntnisse in Go und Microservices verfügst. Zudem wird davon ausgegangen, dass Go auf deinem System installiert ist. Er ist nicht als grundlegender Leitfaden zu diesen Themen gedacht.
Erstellung eines neuen Golang-Projekts
Der erste Schritt besteht darin, ein neues Projekt in Go zu erstellen:
go mod init <name>
Ich habe ein Projekt namens stellar_backend erstellt. Dieser Befehl generiert zwei wesentliche Dateien: go.mod und go.sum. Diese Dateien sind entscheidende Bestandteile des Go-Modulsystems und verwalten die Abhängigkeiten in Go-Projekten.
Als Nächstes müssen wir GORM und den PostgreSQL-Treiber installieren, was einfach mit dem Befehl go get
erledigt werden kann.
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
Da wir ein Modul erstellt haben, werden diese Befehle unsere go.sum- und go.mod-Dateien aktualisieren, um die erforderlichen Abhängigkeiten einzuschließen. Im Verlauf des Projekts werden wir auf weitere Abhängigkeiten stoßen, die installiert werden müssen. Achte darauf diejenigen, die nicht auf deinem System vorhanden sind mit dem Befehl go get
zu installieren.
Projektstrukturierung für Skalierbarkeit
Eine gut strukturierte Dateianordnung ist entscheidend für die Wartbarkeit und Skalierbarkeit von Microservices. Wir verwenden das folgende Layout, um unsere Microservices in Go zu strukturieren. Diese Struktur hilft uns, den Code lesbar und modular zu halten:
├── api/
│ │
│ └── server/
│ └── main.go
│
├── config/
│ └── config.go
├── internal/
│ ├── <module>/
│ │ └── service/
│ │ │ └── <module>_service.go
│ │ └── repositories/
│ │ │ └── <module>_repository.go
│ │ └── dtos/
│ │ │ └── <module>_dto.go
│ │ └── controller.go
│ └── db/
│ └── db.go
├── .env
├── go.mod
└── go.sum
Zusammenfassung der wichtigsten Teile:
api/:
server/main.go: Enthält den Einstiegspunkt unserer Anwendung.
internal/: Beinhaltet anwendungsspezifische interne Logik.
db/
db/db.go: Enthält die Logik für die Datenbankverbindung.
<module_name>/:
repositories/<module_name>_repository.go: Verwalte die Datenbankinteraktionen mit GORM.
dtos/<module_name>_dto.go: Definiert Modul-Datenübertragungsobjekte (DTOs).
routes.go: Definiert eine API zur Verwaltung der Moduloperationen und registriert die Modulpfade auf dem Server.
go.mod: Die vom Befehl go mod init erstellte Moduldurationsdatei.
go.sum: Die von Go-Modulen verwaltete Abhängigkeitssummenprüfdatei.
.env: Enthält die Umgebungsvariablen.
Bevor wir mit dem Code fortfahren, müssen wir eine Datenbank für unsere Abfragen einrichten.
Erstellung einer Datenbank
Die Verbindung zu einer Datenbank ist entscheidend für die Funktionsweise unserer Anwendung. Du kannst entweder eine neue Datenbank erstellen oder eine vorhandene nutzen. In diesem Fall werden wir Docker und Docker Compose verwenden, um eine Datenbank in einem Container einzurichten und zu verwalten.
Dazu haben wir eine docker-compose.yml-Datei mit folgendem Inhalt erstellt:
...
services:
db:
...
environment:
POSTGRES_PASSWORD: $POSTGRES_PASSWORD
POSTGRES_DB: $POSTGRES_DB
...
networks:
default:
name: stellar_default
Und in der .env-Datei haben wir die Variablen definiert, die in dieser Datei verwendet werden:
POSTGRES_DB=stellar_backend
POSTGRES_PASSWORD=admin
POSTGRES_USER=admin
Jetzt können wir den Befehl docker-compose up
ausführen, um den Container zu starten und die Datenbank zu erstellen. Wir sollten eine Ausgabe erhalten, die etwa wie folgt aussieht:
Jetzt versuchen wir eine Verbindung zu dieser neuen Datenbank mit einem PostgreSQL-Client herzustellen. Wir haben dazu psql
verwendet:
psql -h localhost -U admin -d stellar_backend
Es wird nach dem Passwort gefragt, das wir zuvor in der .env-Datei definiert haben. Wenn die Verbindung erfolgreich hergestellt wird solltest du eine Ausgabe sehen, die etwa wie folgt aussieht:

Jetzt verbinden wir uns mit unserer Datenbank mithilfe von Go.
Verbindung von GORM mit der Datenbank
Da wir unsere Datenbank erfolgreich eingerichtet haben ist der nächste Schritt die Verbindung zu dieser Datenbank über Go herzustellen. Entsprechend unserer Projektstruktur erstellen wir eine Datei namens db.go, um die gesamte Verbindungslogik zu verwalten.
In dieser Datei fügen wir unsere Verbindungslogik zur Datenbank ein:
...
dsn := "host=" + os.Getenv("POSTGRES_HOST") +
" user=" + os.Getenv("POSTGRES_USER") +
" password=" + os.Getenv("POSTGRES_PASSWORD") +
" dbname=" + os.Getenv("POSTGRES_DB") +
" port=" + os.Getenv("POSTGRES_PORT") +
" sslmode=" + os.Getenv("SSL_MODE") +
" TimeZone=" + os.Getenv("TIMEZONE")
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
...
fmt.Println("Connected to database! 🚀")
...
Im Moment sieht die Dateistruktur folgendermaßen aus:
my-microservice/
├── internal/
│ └── db/
│ └── db.go
├── .env
├── docker-compose.yml
├── go.mod
└── go.sum
Und wir müssen die neuen Variablen zur .env-Datei hinzufügen:
...
POSTGRES_PORT=5432
POSTGRES_DB=stellar_db
SSL_MODE=disable
TIMEZONE=UTC
Und um diese Variablen zu laden benötigen wir das Paket github.com/joho/godotenv
. Um es zu installieren führe einfach folgenden Befehl aus: go get -u github.com/joho/godotenv
Wenn wir diesen Code ausführen und alles reibungslos funktioniert sehen wir die folgende Nachricht:
Connected to database! 🚀
Der nächste Schritt besteht darin, eine Tabelle zur Speicherung von Daten zu erstellen. Dazu benötigen wir Modelle.
Erstellen eines Modells
Ein Modell stellt eine strukturierte Definition einer Datenbanktabelle dar und dient als Vorlage für die Interaktion mit dieser Tabelle. Es ermöglicht uns, Daten mit unserer Programmiersprache — in diesem Fall Go — zu definieren, zu manipulieren und abzufragen.
Die Modelle in Go und GORM werden auf folgende Weise definiert:
type <model_name> struct {
<property_name> <type> <struct_tags>
}
In Go verwenden wir eine struct
, um ein Modell zu definieren. Jede struct
repräsentiert eine Datenbanktabelle, und die Felder in der struct
entsprechen den Spalten in dieser Tabelle. Darüber hinaus können die Felder auch Beziehungen zwischen verschiedenen Tabellen darstellen. Jedes Feld hat einen spezifischen Datentyp und kann Tags für zusätzliche Attribute oder Regeln enthalten.
Zum Beispiel werden wir ein Modell erstellen, um planetarische Systeme darzustellen. Unser fiktiver Microservice wird eine API bereitstellen, um Informationen über diese Systeme (alles fiktiv) zu verwalten.
...
type PlanetarySystem struct {
gorm.Model
Name string `gorm:"not null;type:varchar(100)" json:"name"`
Planets []Planet `gorm:"foreignKey:PlanetarySystemID" json:"planets"`
Asteroids []Asteroid `gorm:"foreignKey:PlanetarySystemID" json:"asteroids"`
}
...
Um ein praktisches Beispiel zu nehmen: Unser
PlanetarySystem
-Modell hat die folgenden Eigenschaften:Name: Dies wird der Name des planetarischen Systems sein.
Das
struct
-Taggorm:"not null;type:varchar(100)"
gibt an, dass dieses Feld in der Datenbank nicht null sein darf und eine maximale Länge von 100 Zeichen hat.
Planets: Dies wird ein Array sein, das die Planeten speichert die zum System gehören.
Das
struct
-Taggorm:"foreignKey:PlanetarySystemID"
zeigt an, dass es eine Fremdschlüsselbeziehung zwischen den ModellenPlanetarySystem
undPlanet
gibt. Es weist GORM spezifisch darauf hin, dass das ModellPlanet
ein FeldPlanetarySystemID
hat, das als Fremdschlüssel dient, um jeden Planeten mit einem planetarischen System zu verknüpfen.
Asteroids: Dies wird ein Array sein, das die Asteroiden im System speichert.
Ähnlich wie beim Feld
Planets
.
Die
json
-Tags in denstruct
-Definitionen geben die Feldnamen an, die beim Codieren oder Decodieren von JSON-Daten verwendet werden. Zum Beispiel stelltjson:"name"
sicher, dass dasName
-Feld in der JSON-Ausgabe als „name“ dargestellt wird.Die Modelle enthalten eine grundlegende Go-
struct
„gorm.Model“, die einige grundlegende Eigenschaften wie ID, CreatedAt, UpdatedAt und DeletedAt umfasst, die für die Verwaltung der Modelle nützlich sein werden.Weitere Informationen zur Deklaration von Modellen findest du hier:
models .Das könnte in einem ER-Diagramm wie folgt dargestellt werden:

Migrationen ausführen
Um die Tabellen in der Datenbank basierend auf diesen Modellen zu erstellen, müssen wir die Migrationen ausführen. Eine Migration ist eine Methode zur Verwaltung und Anwendung von Änderungen am Datenbankschema, um es mit den Modellen synchron zu halten. In db.go
müssen wir die Funktion db.Automigrate
aufrufen. Fügen wir das also hinzu:
internal/db/db.go
...
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
fmt.Println("Connected to database! 🚀")
db.AutoMigrate(
&models.PlanetarySystem{},
&models.Planet{},
...other models
)
...
Nachdem wir diesen Code ausgeführt haben, sollten die Tabellen bereits erstellt sein. Du kannst überprüfen, ob die Tabellen erstellt wurden indem du psql
verwendest und den Befehl \dt
ausführst:
stellar_backend-# \dt
List of relations
Schema | Name | Type | Owner
--------+-------------------+-------+-------
public | asteroids | table | admin
public | astronauts | table | admin
...
(5 rows)
Wir können Details zu den Tabellen mit \d+
und dem Namen der Tabelle überprüfen.
Was kommt als Nächstes? Nun, diese Tabellen sind leer. Wir möchten in der Lage sein Daten in diesen Tabellen hinzuzufügen, zu lesen, zu bearbeiten und zu entfernen. Dazu müssen wir den HTTP-Server, die Routen und die Controller erstellen.
Ein HTTP-Server einrichten
HTTP-Server erstellen
Wir haben das Echo-Framework verwendet, um einen HTTP-Server einzurichten der meine Endpunkte bedient.
api/server/main.go
Diese Datei enthält den Einstiegspunkt unseres Programms und startet den Server, der in server.go
definiert ist.
import "stellar_backend/internal/server"
...
func main() {
server.Server.Start(":8080")
}
...
internal/server/server.go
Dieser Code enthält eine init
-Funktion, in der ein neuer Server und eine Datenbankinstanz erstellt werden und er ruft die Funktion InitRoutes
für alle Module auf, die Endpunkte haben und die vom Server bedient werden müssen. Für den Server haben wir das Echo-Webframework verwendet.
var Server *echo.Echo
func init() {
Server = echo.New()
db := db.DB()
planet.InitRoutes(Server, db)
...
}
routes.go
Für jedes Modul, das Endpunkte benötigt, wollen wir eine routes.go
-Datei haben, die den Handler für jeden Endpunkt und die InitRoutes
-Funktion enthält, ähnlich wie diese:
...
func InitRoutes(server *echo.Echo, db *gorm.DB) {
server.GET("/planetarysystems", getPlanetarySystems(db))
...
}
func getPlanetarySystems(db *gorm.DB) echo.HandlerFunc {
return func(c echo.Context) error {
return c.JSON(200, "Get Planetary Systems")
}
}
...
Die InitRoutes
-Funktion richtet HTTP-Routen auf dem bereitgestellten Echo-Server ein. Sie verknüpft bestimmte Endpunkte mit ihren jeweiligen Handler-Funktionen zur Verwaltung des Modells. Jede Route (GET, POST, PUT, DELETE) ist mit einer Funktion verknüpft, die Anfragen verarbeitet und über GORM mit der Datenbank interagiert.
Unser Projekt sieht jetzt folgendermaßen aus:
├── api/
│ └── server/
│ └── main.go
│
├── internal/
│ ├── planetarySystem/
│ │ └── routes.go
...
│ ├── server/
│ │ └── server.go
│ └── db/
│ └── db.go
│
├── ...
└── go.sum
Server starten
Voraussetzungen:
Starte den PostgreSQL-Container mit
docker-compose up
.Führe den Code aus:
Um den Arbeitsablauf zu verbessern, verwenden wir eine Makefile. Mit
make build
undmake start
können wir den gesamten Build-Prozess automatisieren.Die Ausgabe sollte etwa wie folgt aussehen:

Verarbeitung einer Anfrage
Für das planetarySystems
-Modul enthält mein routes.go
-Code die Funktionen zur Verarbeitung der Anfragen. Hier ist zum Beispiel ein Beispiel, um eine Nachricht abzurufen, wenn eine GET-Anfrage den Endpunkt /planetarysystems
erreicht:
internal/planetarySystems/routes.go
...
server.GET("/planetarysystems", getPlanetarySystems(db))
...
func getPlanetarySystems(db *gorm.DB) echo.HandlerFunc {
return func(c echo.Context) error {
return c.JSON(200, "Get Planetary Systems")
}
}
Wenn ich also auf die Route http://localhost:3000/planetarysystems
zugreife, erhalte ich die Nachricht:

Jetzt, da alles eingerichtet und funktionsfähig ist, ist es an der Zeit, grundlegende CRUD-Operationen für unsere Modelle zu implementieren, um die Daten in der Datenbank zu verwalten.
Implementierung grundlegender CRUD-Operationen
Um die CRUD-Operationen hinzuzufügen, habe ich die routes.go
-Datei für jedes Modell aktualisiert. Ich habe Dependency Injection verwendet, um einen entkoppelten Controller zu erstellen, sodass der aktualisierte Code für meine Modelle folgendermaßen aussieht:
internal/planetarySystem/routes.go
...
func InitRoutes(server *echo.Echo, db *gorm.DB) {
controller := NewPlanetarysystemController(db)
server.GET("/planetarysystems", controller.getPlanetarySystems)
...
}
type planetarysystemController struct {
repository repository.PlanetarysystemRepository
}
type PlanetarysystemController interface {
getPlanetarySystems(c echo.Context) error
...
}
func NewPlanetarysystemController(db *gorm.DB) PlanetarysystemController {
return &planetarysystemController{
repository: repository.NewPlanetarysystemRepository(db),
}
}
func (controller *planetarysystemController) getPlanetarySystems(c echo.Context) error {
systems, err := controller.repository.GetPlanetarySystems()
if err != nil {
return c.JSON(500, err)
}
return c.JSON(200, systems)
}
...
Ich habe mich entschieden, ein Repository für jedes Modell zu erstellen, um alle Datenzugriffsoperationen zu kapseln und sie von der Geschäftsschicht zu abstrahieren.
Zum Beispiel lautet der Repository-Code für das PlanetarySystems
-Modul wie folgt:
...
type PlanetarysystemRepository interface {
GetPlanetarySystems() ([]models.PlanetarySystem, error)
SavePlanetarySystem(*models.PlanetarySystem) error
...
}
...
func (r *planetarysystemRepository) GetPlanetarySystems() ([]models.PlanetarySystem, error) {
var planetarySystems []models.PlanetarySystem
if err := r.db.Find(&planetarySystems).Error; err != nil {
return nil, err
}
return planetarySystems, nil
}
func (r *planetarysystemRepository) SavePlanetarySystem(planetarySystem *models.PlanetarySystem) error {
if err := r.db.Create(planetarySystem).Error; err != nil {
return err
}
return nil
}
...
In diesem Code haben wir eine NewPlanetarysystemRepository
-Funktion, die eine Konstruktionsfunktion ist und eine Instanz von planetarysystemRepository
erstellt und zurückgibt, die mit der bereitgestellten GORM-Datenbankverbindung initialisiert wird.
Sie werden feststellen, dass ich 4 Funktionen für das PlanetarysystemRepository
-Interface implementiert habe:
GetPlanetarySystems
: Holt alle planetarischen Systeme aus der Datenbank.SavePlanetarySystem
: Erstellt oder aktualisiert ein planetarisches System in der Datenbank.GetPlanetarySystem
: Holt Informationen über ein planetarisches System basierend auf seiner ID.DeletePlanetarySystem
: Entfernt ein planetarisches System basierend auf seiner ID.
Diese Funktionen sind erforderlich, um die grundlegenden CRUD-Operationen zu implementieren. Für die anderen Module können Sie dieselben Muster und den gleichen Code-Stil verwenden, die wir für dieses spezielle Modul verwendet haben, und so die API vervollständigen.
Testen
Jetzt können wir die Endpunkte mit Postman testen.
Eintrag erstellen
Wir können überprüfen, ob die Tabelle aktualisiert wurde, um diesen neuen Eintrag einzuschließen:

Einträge abrufen:
In diesem Fall führt die Anwendung die umgekehrte Operation aus: Sie ruft Daten aus der Datenbank mithilfe des Modells ab und konvertiert sie dann in das JSON-Format zur Übertragung.
Eintrag entfernen
Sie können überprüfen, dass die Spalte deleted_at
mit dem Zeitstempel aktualisiert wurde, als die Anfrage gesendet wurde. Dies wird als Soft-Delete bezeichnet.
Zusammenfassung
In diesem Beitrag haben wir eine umfassende Reise durch die Integration von GORM in einen Go-Microservice unternommen und die wesentlichen Aspekte von der Bedeutung von Microservices und ORMs bis hin zur praktischen Umsetzung behandelt.
Was kommt als Nächstes?
Jetzt, da Sie GORM mit Go-Microservices integriert haben, sind hier einige nächste Schritte, um Ihr Wissen zu vertiefen:
Erforschen Sie fortgeschrittene GORM-Funktionen: Tauchen Sie ein in Transaktionen, Hooks und Assoziationen für komplexere Datenbankoperationen.
Optimieren Sie die Leistung: Lernen Sie mehr über Indizierung, Abfrageoptimierung und Caching, um die Leistung der Anwendung zu verbessern.
Verbessern Sie die Sicherheit: Implementieren Sie bewährte Methoden für Authentifizierung, Autorisierung und Datenverschlüsselung.
Automatisieren Sie mit CI/CD: Richten Sie Continuous Integration- und Continuous Deployment-Pipelines ein, um Ihren Entwicklungsworkflow zu optimieren.
Treten Sie der Community bei: Engagieren Sie sich in den Go- und GORM-Communities, tragen Sie zu Open-Source-Projekten bei und bleiben Sie über die neuesten Trends informiert.
Indem Sie diese nächsten Schritte verfolgen, werden Sie Ihr Wissen weiter ausbauen und noch robustere und effizientere Microservices erstellen. Viel Spaß beim Programmieren!
Partnerschaft mit Golang-Experten
Lassen Sie uns besprechen, wie unsere Golang-Expertise Ihr nächstes Projekt voranbringen kann. Kontaktieren Sie uns, und lassen Sie uns gemeinsam erfolgreich sein.