Tuto : développement d'API en Golang simplifier avec GORM

Le Golang est un langage de programmation compilé développé par Google en 2009 et inspiré des langages C et Pascal. Ce langage renforce sa popularité au fil des années.

Le Mappage Relationnel Objet (ORM) est une technique de programmation informatique qui se place entre un programme applicatif (API) et une base de données relationnelle. L’ensemble des langages de programmation disposent d’ORM. Le plus populaire en Golang est GORM.

Dans cet article, nous allons expliquer pourquoi utiliser un ORM dans ses applications et comment mettre en place un ORM dans une API REST Golang.

Pourquoi utiliser un ORM ?

Un ORM permet de faire correspondre en informatique le monde objet (programmation orientée objet) et le monde relationnel (base de données relationnelles). Pour cela, il met à disposition des classes objet que les développeurs manipulent et qui sont ensuite transformées par l’ORM en requêtes compréhensibles par la base de données.

L’intérêt principal pour les développeurs est d’éviter beaucoup de codes très similaires, ce qui peut être source d’erreur et de temps perdu. En effet, en ajoutant un ORM à son application qui se charge de la traduction, les développeurs n’ont pas à devoir recoder à la main l’ensemble des fonctions de base CRUD (Création, Lecture, Modification, Suppression).

Avantages

Inconvénients

Qu'est-ce que la bibliothèque GORM ?

La bibliothèque GORM est l’ORM le plus populaire dans l’écosystème Golang. Il fournit un grand nombre de fonctionnalités pour la migration automatique de schéma, la journalisation, les contextes, les requêtes SQL, les associations, les contraintes, et bien plus encore.

Pour utiliser le paquet GORM et interagir avec votre base de données, vous aurez besoin d’un pilote de base de données et du paquet GORM installé dans votre espace de travail. Il fournit une suite de pilotes de base de données pour les bases de données SQL populaires (MySQL, SQLite, MSSQL, PostgreSQL), mais vous pouvez également utiliser d’autres pilotes de base de données développés en Golang.

Dans notre démonstration, nous allons utiliser la bibliothèque GORM et une base de données SQLite. Nous allons également utiliser le framework Gin pour créer une API REST. Si vous souhaitez en savoir plus sur ce framework, vous pouvez consulter notre article “Tuto : comment réaliser une API REST en Golang ?”.

Comment utiliser GORM dans son API REST ?

Étape 1 : initialisation du projet

Pour commencer, créons un dossier pour le projet contenant un fichier “main.go”, ainsi que trois dossiers “controllers”, “database” et “models”.

Initialisons un nouveau Go module pour gérer les dépendances de notre projet et installer le framework Gin, la bibliothèque GORM et le pilote SQLite driver pour GORM.

go mod init
go get github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get github.com/glebarez/sqlite

Étape 2 : Création du model Book

Dans un premier temps, nous allons créer une classe “métier” représentant un livre dans le dossier “models” dans un fichier nommé “book.go”.

Dans notre cas, un livre sera représenté par un Titre, un Auteur et les attributs génériques contenus dans le modèle de GORM.

type Book struct {
gorm.Model
Title  string `json:"title"`
Author string `json:"author"`
}

Puis, nous allons créer toutes les fonctions liées au modèle, nécessaires pour agir sur la base de données. Ces fonctions sont toutes dans le fichier “models/book.go”. Elles utilisent toutes des fonctions elles-mêmes fournies par la bibliothèque GORM.

Obtenir tous les livres

Pour obtenir tous les livres présents dans la base de données, nous implémentons une fonction “GetBooks”.

func (book *Book) GetBooks(db *gorm.DB) (*[]Book, error) {
var books []Book
err := db.Model(&Book{}).Find(&books).Error
return &books, err
}

Cette dernière permet de récupérer l’ensemble des livres grâce à la fonction “Find” sur notre modèle “Book”.

Obtenir un livre par ID

Pour obtenir un livre grâce à son ID, nous implémentons une fonction “GetBookById”.

func (book *Book) GetBookById(db *gorm.DB, id uint) error {
return db.Model(&Book{}).First(book, id).Error
}

Cette dernière permet grâce à la fonction “First” de récupérer le premier livre ayant pour ID celui précisé par l’utilisateur. À noter que chaque livre est enregistré dans la base de données avec un ID unique.

Obtenir tous les livres d’un auteur

Pour obtenir tous les livres d’un auteur, nous implémentons une fonction “GetBooksByAuthor”.

func (book *Book) GetBooksByAuthor(db *gorm.DB, author string) (*[]Book, error) {
var books []Book
err := db.Model(&Book{}).Where("author = ?", author).Find(&books).Error
return &books, err
}

Cette dernière permet grâce aux fonctions “Where” et “Find” de récupérer tous les livres ayant pour auteur celui précisé par l’utilisateur.

Modifier ou créer un livre

Pour modifier ou créer un livre, nous implémentons une fonction “UpdateOrCreateBook”.

func (book *Book) UpdateOrCreateBook(db *gorm.DB) error {
return db.Save(book).Error
}

Cette dernière permet, grâce à la fonction “Save”, de modifier un élément dans la base de données ou s’il n’existe pas, de le créer.

Supprimer un livre

Pour supprimer un livre, nous implémenterons une fonction “DeleteBook”.

func (book *Book) DeleteBook(db *gorm.DB, id uint) error {
err := db.Delete(book, id).Error
return err
}

Cette dernière permet grâce à la fonction “Delete” de supprimer un élément dans la base de données.

Étape 3 : création de la base de données

Dans un second temps, nous allons créer un fichier “db” contenant les fonctions liées à la base de données dans le dossier “database”.

Connexion à la base de données

Pour pouvoir nous connecter à notre base de données, nous implémentons une fonction “ConnectDb” qui prend en paramètre le nom du fichier SQLite à ouvrir ou créer.

func ConnectDb(host string) *gorm.DB {
db, err := gorm.Open(sqlite.Open(host), &gorm.Config{})
if err != nil {
fmt.Println("Failed to connect database")
}
return db
}

Puis, dans le fichier “main.go”, nous appelons la fonction précédemment créée nous permettant de nous connecter.

func main() {
  db := database.ConnectDb("library.db")
}

Création du modèle

Une fois la connexion établie, nous créons le modèle “Book” dans la base de données, à l’aide de la structure précédemment créée dans le fichier “models/Book.go”. Pour cela, nous implémentons une fonction “CreateModel” dans le fichier “database/db.go”.

func CreateModel(db *gorm.DB) *gorm.DB {
  db.AutoMigrate(&models.Book{})
  return db
}

Puis, dans le fichier “main.go”, nous appelons cette fonction en passant en paramètre la base de données, précédemment récupérée lors de la création.

func main() {
  db := database.ConnectDb("library.db")
  database.CreateModel(db)
}

Initialisation avec des livres

Une fois le modèle créé, nous pouvons ajouter des livres dans notre librairie. Pour cela, nous implémentons une fonction “InitDatabase” dans le fichier “database/db.go”.

func InitDatabase(db *gorm.DB) {
var book1 = models.Book{
    Title:  "Voyage au centre de la Terre",
    Author: "Jules Verne",
   }
   book1.UpdateOrCreateBook(db)

   var book2 = models.Book{
    Title:  "Vingt Mille Lieues sous les mers",
    Author: "Jules Verne",
   }
   book2.UpdateOrCreateBook(db)

   var book3 = models.Book{
    Title:  "Les Misérables",
   Author: "Victor Hugo",
   }
   book3.UpdateOrCreateBook(db)
}

Puis, dans le fichier “main.go”, nous appelons cette fonction en passant en paramètre la base de données, précédemment récupérée lors de la création.

func main() {
  db := database.ConnectDb("library.db")
  database.CreateModel(db)
  database.InitDatabase(db)
}

Étape 4 : création des routes REST

Dans un dernier temps, nous allons créer un fichier “book.go” dans le dossier “controllers” qui va contenir toutes les fonctions, nous permettant de gérer nos livres au travers d’une API.

Initialisation du controller

Pour pouvoir stocker la base de données et l’utiliser dans nos fonctions, nous créons une structure “BookRepo” pour la stocker.

type BookRepo struct {
  Db *gorm.DB
}

Puis, dans le fichier “main.go”, nous initialisons cette structure.

func main() {
  db := database.ConnectDb("library.db")
  database.CreateModel(db)
  database.InitDatabase(db)

  bookRepo := controllers.BookRepo{
    Db: db,
   }
}

Initialisation de Gin

Pour pouvoir créer des routes API, il faut commencer par initialiser Gin dans le fichier “main.go”. Nous définissons une variable “r” représentant le routeur par défaut de Gin.

func main() {
  db := database.ConnectDb("library.db")
  database.CreateModel(db)
  database.InitDatabase(db)

  bookRepo := controllers.BookRepo{
    Db: db,
   }
   r := gin.Default()
   r.Run()
}

Route pour obtenir tous les livres

Pour cela, nous devons créer une route de type GET dans le fichier “main.go”, ayant pour endpoint “/books”.

func main(){
  db := database.ConnectDb("library.db")
  database.CreateModel(db)
  database.InitDatabase(db)

  bookRepo := controllers.BookRepo{
    Db: db,
  }
  r := gin.Default()
  r.GET("/books", bookRepo.FindBooks)
  r.Run()
}

Puis, nous implémentons une fonction “FindBooks” dans le fichier “controllers/book.go”, permettant de récupérer l’ensemble des livres. Pour cela, nous allons utiliser la fonction “GetBooks”, précédemment créée dans notre modèle.

func (repository *BookRepo) FindBooks(c *gin.Context) {
 var bookModel models.Book
 books, err := bookModel.GetBooks(repository.Db)
 if err != nil {
    c.String(http.StatusInternalServerError, "Erreur récupération des livres")
    return
}
 c.JSON(http.StatusOK, gin.H{"books": books})
}

Une fois le serveur démarré, il est possible de tester cette requête GET dans Postman, afin de vérifier la récupération des livres.

Test de la requête “GetBooks”
Source : Postman

Route pour obtenir tous les livres d’un auteur

Pour cela, nous devons créer une route de type GET dans le fichier “main.go” ayant pour endpoint “/books/author” et un paramètre “author”.

func main() {
  db := database.ConnectDb("library.db")
  database.CreateModel(db)
  database.InitDatabase(db)

  bookRepo := controllers.BookRepo{
    Db: db,
  }
  r := gin.Default()
  r.GET("/books", bookRepo.FindBooks)
  r.GET("/books/author/:author", bookRepo.FindBooksByAuthor)
  r.Run()
}

Puis, nous implémentons une fonction “FindBooksByAuthor” dans le fichier “controllers/book.go” permettant de récupérer l’ensemble des livres liés à un auteur spécifié, par l’utilisateur en paramètre. Pour cela, nous allons utiliser la fonction “GetBooksByAuthor”, précédemment créée dans notre modèle.

func (repository *BookRepo) FindBooksByAuthor(c *gin.Context) {
author := c.Param("author")

 var bookModel models.Book
 books, err := bookModel.GetBooksByAuthor(repository.Db, author)
 if err != nil {
    c.String(http.StatusInternalServerError, "Erreur récupération des livres")
    return
}
 c.JSON(http.StatusOK, gin.H{"books": books})
}

Une fois le serveur démarré, il est possible de tester cette requête GET dans Postman, afin de vérifier la récupération des livres pour l’auteur “Victor Hugo”.

Test de la requête “GetBooksByAuthor”
Article YPSI : test requête “GetBooksByAuthor”
Source : Postman

Route pour créer un livre

Pour créer un livre à travers l’API, nous devons créer une route de type POST dans le fichier “main.go” ayant pour endpoint “/book”.

func main() {
  db := database.ConnectDb("library.db")
  database.CreateModel(db)
  database.InitDatabase(db)

  bookRepo := controllers.BookRepo{
    Db: db,
  }
  r := gin.Default()
  r.GET("/books", bookRepo.FindBooks)
  r.GET("/books/author/:author", bookRepo.FindBooksByAuthor)
  r.POST("/book", bookRepo.CreateBook)
  r.Run()
}

Puis, nous créons la fonction “CreateBook” dans le fichier “controllers/book.go”. Elle permet de vérifier que le livre passé par l’utilisateur dans le corps de la requête est valide, et de créer un livre à l’aide de la fonction “UpdateOrCreateBook”, précédemment créée dans notre modèle.

Pour vérifier que le livre est valide, nous avons besoin de créer une structure intermédiaire contenant les champs présents dans le JSON passé par l’utilisateur.

type BookCreate struct {
  Title  string `json:"title"`
  Author string `json:"author"`
}
func (repository *BookRepo) CreateBook(c *gin.Context) {
 var bookInput BookCreate
if err := c.ShouldBindJSON(&bookInput); err != nil {
    c.String(http.StatusInternalServerError, "Erreur récupération du JSON")
    return
}

newBook := models.Book{
    Title:  bookInput.Title,
    Author: bookInput.Author,
}
err := newBook.UpdateOrCreateBook(repository.Db)
if err != nil {
c.String(http.StatusInternalServerError, "Erreur création du livre")
return
}

c.JSON(http.StatusOK, gin.H{"book": newBook})
}

Enfin, il est possible de vérifier la requête avec Postman, après avoir redémarré le serveur.

Test de la requête “UpdateOrCreateBook”
Source : Postman

Route pour supprimer un livre

Pour supprimer un livre à travers l’API, nous devons créer une route de type DELETE dans le fichier “main.go” ayant pour endpoint “/book” et un paramètre “id”.

func main() {
  db := database.ConnectDb("library.db")
  database.CreateModel(db)
  database.InitDatabase(db)
  bookRepo := controllers.BookRepo{
    Db: db,
  }
  r := gin.Default()
  r.GET("/books", bookRepo.FindBooks)
  r.GET("/books/author/:author", bookRepo.FindBooksByAuthor)
  r.POST("/book", bookRepo.CreateBook)
  r.DELETE("/book/:id", bookRepo.DeleteBook)
  r.Run()
}

Puis, nous créons la fonction “DeleteBook” dans le fichier “controllers/book.go”. Elle permet tout d’abord de vérifier que l’ID passé, par l’utilisateur correspond à un livre dans la base de données.

Pour cela, nous allons utiliser la fonction “GetBookById” précédemment créée, dans notre modèle qui nous renvoie une erreur si le livre n’est pas trouvé. C’est cela qui nous permet de renvoyer un code erreur 404, correspondant à “Not Found”.

Enfin, nous utilisons la fonction “DeleteBook”, précédemment créée dans notre modèle qui supprime un élément avec son ID.

func (repository *BookRepo) DeleteBook(c *gin.Context) {
 bookId, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.String(http.StatusBadRequest, "Erreur récupération du paramètre")
    return
}

var bookFind models.Book
 err = bookFind.GetBookById(repository.Db, uint(bookId))
if err != nil {
    c.String(http.StatusNotFound, "Le livre n'existe pas")
    return
}

err = bookFind.DeleteBook(repository.Db, uint(bookFind.ID))
 if err != nil {
    c.String(http.StatusInternalServerError, "Erreur suppression du livre")
    return
}

c.JSON(http.StatusOK, gin.H{"data": true})
}

Enfin, il est possible de vérifier la requête avec Postman après avoir redémarré le serveur.

Test de la requête “DeleteBook”
Source : Postman

L’intégralité du code présenté dans ce tutoriel est sur le GitHub de YPSI SAS. Il est possible de le cloner sur votre ordinateur.

Conclusion

Les ORM sont devenus des outils indispensables aux développeurs, leur permettant de travailler efficacement et de gagner du temps dans leur développement. Ils permettent d’effectuer une connexion entre un programme applicatif et une base de données relationnelle. Ils sont utilisés dans de nombreux langages.

Chez YPSI, nous réalisons des API utilisant des ORM dans nos développements pour nos clients. N’hésitez pas à nous contacter pour des projets ou toutes questions.

Partagez cet article :

Laisser un commentaire