WebMap – Teil 2: Go struct in MongoDB speichern

In Teil 1 der WebMap Serie wurde gezeigt, wie in einer Goroutine Domain-Namen generiert, in einen Channel geschrieben und an anderer Stelle im Programm weiterverarbeitet werden können. Der Code des ersten Teils ist auf GitHub zu finden.

Im zweiten Teil werden Informationen zu einer Domain in einem Struct festgehalten und in einer Datenbank (hier MongoDB) gespeichert. Der Code hiervon ist ebenfalls auf GitHub zu finden.

Lokal mit MongoDB arbeiten

Um lokal mit einer MongoDB zu arbeiten, bietet es sich an Docker zu verwenden. Unter Einsatz von Docker-Compose lässt sich in wenigen Schritten eine MongoDB starten und verwenden. Notwendig ist dafür eine docker-compose.yml Datei, in welcher beschrieben wird, welche Dienste es benötigt. Beispiele hierfür gibt es unter anderem auf Docker Hub.

version: '3.1'

services:
  mongo:
    image: mongo
    ports:
      - 27017:27017

  mongo-express:
    image: mongo-express
    ports:
      - 8081:8081

Bei dem Service mongo handelt es sich um die Datenbank selbst. Das Port-Mapping ist notwendig, damit das Go-Skript eine Verbindung zu der Datenbank aufbauen kann. Bei dem Service mongo-express handelt es sich um ein Web-Interface für die Datenbank, welches über http://localhost:8081/ im Browser erreichbar ist. Starten lässt sich das Ganze mit dem Befehl docker-compose up -d.

MongoDB Go Driver

Ein guter Einstieg, um unter Go mit einer MongoDB zu arbeiten, ist das MongoDB Go Driver Tutorial. Um den Driver zu installieren, muss der Befehl go get go.mongodb.org/mongo-driver ausgeführt werden. Ein ebenfalls hilfreicher Blogpost über den Umgang mit dem MongoDB Go Driver ist unter https://vkt.sh/go-mongodb-driver-cookbook/ zu finden.

Projekt Strukturierung & Datenbankzugriff

Um aus mehreren Modulen heraus auf die Datenbank zugreifen zu können wurde ein neues Modul „models“ angelegt, welches die Arbeit mit der Datenbank weg abstrahiert.

.
├── LICENSE
├── README.md
├── docker-compose.yml
├── extractors
│   └── domain.go
├── fetchers
│   └── domain.go
├── generators
│   └── domain.go
├── go.mod
├── go.sum
├── main.go
└── models
    ├── db.go
    └── domain.go

In der Datei db.go wird die Datenbank initialisiert und als globale Variable abgespeichert. So besteht die Möglichkeit, von überall auf die Datenbank zugreifen zu können. Der Aufruf der Initialisierung findet in main.go statt. Das Speichern des Datenbankhandels als globale Variable ist sicherlich nicht der Beste, aber ein einfacher Weg und für die aktuelle Projektgröße ausreichend. Weitere Möglichkeiten, wie der Datenbankzugriff in Go organisiert werden kann, zeigt Alex Edwards in seinem Blogpost „Practical Persistence in Go: Organising Database Access“.

Initialisierung der Datenbank

In dem folgenden Ausschnitt ist dargestellt, wie in der Datei db.go ein Client erstellt wird, mit welchem auf die MongoDB zugegriffen werden kann. Auf die globale Variable Db kann aus anderen Modulen zugegriffen werden.

package models

import (
    "context"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "log"
)

var Db *mongo.Collection

func InitDB() {
    clientOptions := options.Client().ApplyURI("mongodb://localhost:27017")
    client, err := mongo.Connect(context.TODO(), clientOptions)

    if err != nil {
        log.Panic(err)
    }

    Db = client.Database("webmap").Collection("domains")
}

Wichtig ist, dass nach Beginn des Programms in der main.go die InitDB()-Funktion aufgerufen wird.

Domain als Struct definieren

Wie aus dem ersten Teil der Blog-Reihe hervorgeht, sollen Domain-Namen generiert und auf Verfügbarkeit geprüft werden. Bisher wurden die Domains als String in den Channel geschrieben. Nun soll eine Domain als Struct dargestellt und gespeichert werden. Dafür werden folgende Informationen gespeichert:

type Domain struct {
    Id primitive.ObjectID `bson:"_id,omitempty"`
    Name string
    Tld string
    Ips []net.IP
    CreatedAt time.Time
    FetchedAt time.Time
}
  • Id: wird gesetzt, sobald die Domain in der Datenbank gespeichert wurde. Bei der Information bson:"_id,omitempty" handelt es sich um ein Struct Tag. Durch die Information „omitempty“ wird MongoDB angewiesen, automatisch eine ID zu generieren, wenn das Id-Feld leer ist.
  • Name: Ist der Name der Domain. (Beispiel timoisik von timoisik.de).
  • TLD: Ist die Toplevel Domain. (Beispiel de bei timoisik.de).
  • Ips: sind die durch den Fetcher gefundenen IP-Adresse zu einer Domain.
  • CreatedAt: Das Datum, wann eine Domain in der DB erstellt wurde.
  • FetchedAt: Das Datum, wann eine Domain überprüft wurde.

Struct in DB speichern

Die Logik zur Speicherung und Bearbeitung eines Datenbank-Eintrags einer Domain befinden sich in der Datei models/domain.go. Die Funktionen wurden auf dem Domain-Struct implementiert, somit lassen sie sich direkt auf der Instanz einer Domain aufrufen. Im folgenden Code-Ausschnitt ist die Funktion zum Speichern einer Domain der Datenbank zu sehen:

func (d *Domain) Create() *mongo.InsertOneResult {
    model, err := Db.InsertOne(context.TODO(), d)

    if err != nil {
        log.Fatal(err)
    }

    d.Id = model.InsertedID.(primitive.ObjectID)
    return model
}

In der main.go wird, wie im ersten Teil auch ein Channel erstellt, in welchem der Domain-Generator die Domains schreiben soll. Allerdings handelt es sich nun nicht mehr um einen String-Channel. Da der Generator nun Domains erstellt, die in den Channel geschrieben werden (ch <- models.Domain{Name: domainName, Tld: tlds[j], CreatedAt: time.Now()}), muss der Channel entsprechend erstellt werden. In der for-Schleife, in welcher die Domains so lange ausgelesen werden, wie der Generator sie erstellt, wird weiterhin die Überprüfung nach den IP-Adressen angestoßen. Sind IP-Adressen vorhanden, wird der Datenbank Eintrag aktualisiert, sind keine vorhanden, wird der Eintrag aus der Datenbank gelöscht. Der folgende Ausschnitt zeigt den relevanten Code aus der main.go:

domainsChannel := make(chan models.Domain)
go generators.GenerateDomains(domainsChannel)

for domain := range domainsChannel {
    domain.Create()

    ip, err := fetchers.FetchDomainIp(domain)
    if err != nil {
        // Remove domain from DB if no IP was found for it
        fmt.Printf("No ip for domain %v\n", domain.GetUrl())
        domain.Delete()
    } else {
        fmt.Printf("Update Domain %v with ips %v\n", domain.GetUrl(), ip)
        // Save IPs in DB
        domain.Update(bson.D{
            {"$set", bson.D{
                {"ips", ip},
            }},
            {"$currentDate", bson.D{
                {"fetchedat", true},
            }},
        })
    }
}

In Zeile 15 ist zu sehen, dass auf der Domain die Update-Methode aufgerufen wird. Als Parameter wird BSON übergeben. Hierbei handelt es sich um Binäres-JSON (Binary-encoded JSON), welches von MongoDB eingesetzt wird, um Dokumente in der Datenbank zu erstellen und remote procedure Calls durchzuführen.

Domains in Datenbank anzeigen

Wie in dem Abschnitt zu Docker-Compose beschrieben, kann über http://localhost:8081/ auf ein Interface zugegriffen werden, um sich die Einträge in der MongoDB anzeigen zu lassen. Während die _id automatisch generiert wurde, wurden alle anderen Spalten (name, tld, ips, createdat und fetachedat) von uns befüllt. Da es sich bei den ips um ein Array von Byte-Arrays handelt, ist die Lesbarkeit der IP-Adressen für einen Eintrag nicht ganz optimal.

Schlusswort

Dank des offiziellen MongoDB Go Drivers und Docker-Compose können schnell Testumgebungen aufgebaut werden, um lokal erste Tests durchzuführen. Die Schreibweise von BSON, vor allem für das Update eines Eintrags sind etwas kompliziert und somit schwer zu lesen.

Um den Zugriff auf eine Datenbank in einem Go-Programm zu organisieren, gibt es mehrere Möglichkeiten, wie Alex Edwards in seinem Blogpost „Practical Persistence in Go: Organising Database Access“ beschrieben hat. Einer davon ist, den Zugriff über eine globale Variable zu ermöglichen.

Kommentare anzeigen