As of December 1st, 2019 Buffalo, and all related packages, require Go Modules and the use of the GOPATH is no longer supported.

Please see this blog post for more information https://blog.gobuffalo.io/the-road-to-1-0-requiring-modules-5672c6b015e5.

Modèles

Pop, en tant qu'ORM, vous permet de traduire les tables de votre base de données en des structures en Go. De cette manière, vous pouvez manipuler écrire en Go ce qui nécessiterait d'écrire du SQL. Le code en Go qui permet de faire ce travail est nommé « modèles », en référence à l'architecture MVC.

Dans ce chapitre, vous allez apprendre comment travailler avec les modèles à la main ; puis comment le faire avec les générateurs fournis, pour améliorer votre productivité.

Le dossier models

Les fichiers de modèle de Pop sont placés dans le dossier models, à la racine de votre projet (voir le chapitre sur la structure d'un projet pour plus d'informations sur la manière dont Buffalo organise ses fichiers).

Ce répertoire contient :

  • Un fichier models.go, qui définit le code commun à tous les modèles. Il contient également un pointeur sur la connexion à la base de données courante. N'oubliez pas que le code généré vous appartient, donc vous pouvez placer ce que vous voulez ici.
  • Les fichiers de définition des modèles, un par modèle (donc un par table de la base de données cible).

Définir un modèle simple

Le fichier de modèle définit une structure pour accueillir une ligne de la table cible, des méthodes de validation et des fonctions callback optionnelles, qui permettent de définir des traitements liés aux modèles.

Prenons par exemple cette définition de table SQL, et créons la structure associée :

CREATE TABLE sodas (
    id uuid NOT NULL,
    created_at timestamp without time zone NOT NULL,
    updated_at timestamp without time zone NOT NULL,
    label character varying(255)
);

ALTER TABLE sodas ADD CONSTRAINT sodas_pkey PRIMARY KEY (id);

Nous allons commencer en créant un nouveau fichier dans le dossier models, que l'on nommera soda.go (la convention est d'utiliser la forme au singulier du nom du modèle). Dans ce fichier, nous allons créer une structure pour la table sodas (la structure est aussi au singulier, puisqu'elle ne contient d'une seule ligne de la table) :

package models

import (
	"time"

	"github.com/gobuffalo/pop/nulls"
	"github.com/gobuffalo/uuid"
)

type Soda struct {
	ID                   uuid.UUID    `db:"id"`
	CreatedAt            time.Time    `db:"created_at"`
	UpdatedAt            time.Time    `db:"updated_at"`
	Label                nulls.String `db:"label"`
}

C'est tout ! Vous n'avez besoin de rien de plus pour travailler avec Pop ! Notez que pour chaque champ, nous avons défini un tag pop qui correspond au nom du champ de la table, mais cela n'est pas obligatoire. Si vous ne fournissez pas de nom, il sera déterminé à partir de celui du champ de la structure.

En utilisant le générateur

Note pour les utilisateurs de Buffalo: les commandes de soda sont intégrées à la commande buffalo, sous la commande pop. À chaque fois que vous voulez utiliser une commande de soda, il vous suffit d'utiliser buffalo pop à la place.

Écrire les fichiers de modèles à la main n'est pas de tout repos. Soda (et donc Buffalo, si vous avez bien suivi le chapitre sur Soda) fournit un générateur pour vous aider :

$ soda g model --help

Generates a model for your database

Usage:
  soda generate model [name] [flags]

Aliases:
  model, m


Flags:
  -s, --skip-migration   Skip creating a new fizz migration for this model.

Global Flags:
  -c, --config string   The configuration file you would like to use.
  -d, --debug           Use debug/verbose mode
  -e, --env string      The environment you want to run migrations against. Will use $GO_ENV if set. (default "development")
  -p, --path string     Path to the migrations folder (default "./migrations")

Vous pouvez supprimer le code généré pour le modèle à l'aide de la commande suivante :

$ soda destroy model [name]

Ou dans sa forme courte :

$ soda d m [name]

Gestion des NULL

Si vous avez besoin de stocker des valeurs NULL dans votre table, vous allez avoir besoin d'utiliser des types spéciaux : par exemple, vous ne pouvez pas stocker une valeur NULL si votre type est int.

La bibliothèque standard Go fournit des types spéciaux pour gérer ce cas, comme sql.NullBool ou sql.NullInt64.

Si vous avez besoin de plus que ce que la bibliothèque standard peut offrir, vous pouvez utiliser le paquet gobuffalo/nulls qui fournit plus de types nulls et une meilleure gestion de la sérialisation / désérialisation JSON.

type User struct {
  ID       uuid.UUID
  Email    string
  Password nulls.String
}

Personnaliser les modèles

Définir les noms des champs

Par défaut, Pop utilisera le nom du champ de la structure pour trouver celui qui correspond dans la base de données.

type User struct {
  ID       uuid.UUID
  Email    string
  Password string
}

Pour la structure User définie ici, les noms de colonnes utilisés seront donc ID, Email et Password.

Vous pouvez changer ces noms en définissant explicitement celui à utiliser, à l'aide du tag db.

type User struct {
  ID       uuid.UUID `db:"id"`
  Email    string    `db:"email"`
  Password string    `db:"password"`
}

Avec ce changement, les noms recherchés en base sont id, email et password.

À titre de comparaison, c'est très similaire à la manière dont fonctionne l'association d'un formulaire à une structure.

Tout type implémentant les interfaces Scanner et Valuer peut être utilisé. Cependant, il est recommandé d'utiliser les types suivants si vous ne voulez pas vous fatiguer à les écrire vous-même :

Type Nullable Slice/Array
int nulls.Int slices.Int
int32 nulls.Int32 ------
int64 nulls.Int64 ------
uint32 nulls.UInt32 ------
float32 nulls.Float32 ------
float, float64 nulls.Float64 slices.Float
bool nulls.Bool ------
[]byte nulls.ByteSlice ------
string nulls.String slices.String
uuid.UUID nulls.UUID slices.UUID
time.Time nulls.Time ------
map[string]interface{} --------- slices.Map

Note : Il est nécessaire d'initialiser les types slices.Map avant de pouvoir les utiliser (avant de pouvoir appeler Bind par exemple).

widget := &models.Widget{Data: slices.Map{}}

Champs en lecture seule

Il est souvent nécessaire de lire un champ de la base de données, mais de ne pas vouloir écrire ce champ dans la base. C'est possible, grâce au tag rw.

type User struct {
  ID       uuid.UUID `db:"id"`
  Email    string    `db:"email"`
  Password string    `db:"password" rw:"r"`
}

Dans cet exemple, tous les champs seront récupérés depuis la base, et tous les champs sauf Password pourront être écrits dans la base.

Champs en écriture seule

Les champs en écriture seule sont le pendant opposé des champs en lecture seule. Utilisez cette option pour les champs que vous voulez écrire, mais jamais récupérer de la base de données. Là encore, on utilise le tag rw.

type User struct {
  ID       uuid.UUID `db:"id"`
  Email    string    `db:"email"`
  Password string    `db:"password" rw:"w"`
}

Ignorer des champs

Dans certains cas, vous voudrez faire en sorte que Pop ignore complètement un champ de la structure. Pensez à un champ temporaire en mémoire, ou qui sert dans une certaine logique de votre application.

Vous pouvez signaler à Pop qu'il faut ignorer ce champ, en utilisant le tag db avec la valeur -, comme dans l'exemple ci-après :

type User struct {
  ID       uuid.UUID `db:"id"`
  Email    string    `db:"email"`
  Password string    `db:"-"`
}

Comme vous pouvez le voir, le champ Password possède le tag db:"-", ce qui signifie que Pop n'enregistrera pas ce champ dans la base, et n'ira pas non-plus récupérer sa valeur.

Modifier la clause de sélection d'un champ

Par défaut, Pop essaie de construire ses requêtes de selection pour une structure en utilisant tous les noms des champs.

type User struct {
  ID       uuid.UUID `db:"id"`
  Email    string    `db:"email"`
  Password string    `db:"password"`
}

Pour cette structure, la requête de sélection ressemblerait à ceci :

select id, email, password from users

Il est possible de changer le nom utilisé pour un champ en utilisant le tag select.

type User struct {
  ID       uuid.UUID `db:"id"`
  Email    string    `db:"email"`
  Password string    `db:"password" select:"password as p"`
}

La requête de sélection aurait alors cette tête :

select id, email, password as p from users

Utiliser un nom de table personnalisé

Parfois, vous devrez travailler avec un schéma existant, ne respectant pas les conventions de nommage de Pop. Vous pouvez personnaliser le nom de la table associée à un modèle, en implémentant l'interface TableNameAble :

type User struct {
  ID       uuid.UUID `db:"id"`
  Email    string    `db:"email"`
  Password string    `db:"password"`
}

// TableName personnalise le nom de la table du modèle, utilisé par Pop.
func (u User) TableName() string {
  return "my_users"
}

Timestamps UNIX

depuis v4.7.0

Si vous définissez les champs CreatedAt et UpdatedAt dans votre modèle (ce qui est fait par défaut lorsque vous utilisez le générateur de modèles), Pop se chargera de gérer la valeur de ces champs pour vous. Cela signifie que si vous créez une nouvelle entité en base de données, le champ CreatedAt aura pour valeur la date et l'heure courantes ; et que le champ UpdatedAt sera mis à jour à chaque vous que vous sauvegarderez une entité existante en base.

Ces champs sont définis avec le type time.Time, mais vous pouvez leur donner le type int à la place. Ils seront alors considérés comme des timestamps UNIX.

type User struct {
  ID        int    `db:"id"`
  CreatedAt int    `db:"created_at"`
  UpdatedAt int    `db:"updated_at"`
  FirstName string `db:"first_name"`
  LastName  string `db:"last_name"`
}

Si vous utilisez les migrations de type fizz, assurez-vous de définir ces champs vous-même et de désactiver l'horodatage automatique :

create_table(“users”) {
  t.Column("id", "int", {primary: true})
  t.Column(“created_at”, “int”)
  t.Column(“updated_at”, “int”)
  t.Column(“first_name”, “string”)
  t.Column(“last_name”, “string”)
  t.DisableTimestamps()
}

Modèles de vues

Une vue est un objet de base de données qui stocke le résultat d'une requête. Puisque cet objet agit comme une table en lecture seule, il est possible de le lier avec un modèle de Pop tout comme vous le feriez avec une table.

Si vous voulez utiliser un modèle avec plus d'une table, définir une vue est probablement la meilleure solution pour vous.

Exemple

L'exemple suivant utilise la syntaxe de PostgreSQL. Nous allons commencer par créer deux tables :

-- Créer la table sodas
CREATE TABLE sodas (
    id uuid NOT NULL,
    created_at timestamp without time zone NOT NULL,
    updated_at timestamp without time zone NOT NULL,
    provider_id uuid NOT NULL,
    label character varying(255) NOT NULL
);

ALTER TABLE sodas ADD CONSTRAINT sodas_pkey PRIMARY KEY (id);

-- Créer la table providers
CREATE TABLE providers (
    id uuid NOT NULL,
    label character varying(255) NOT NULL
);

ALTER TABLE providers ADD CONSTRAINT providers_pkey PRIMARY KEY (id);

-- Créer une clé étrangère pour lier les deux tables
ALTER TABLE sodas ADD FOREIGN KEY (provider_id) REFERENCES providers(id);

Puis continuons en créant la vue :

CREATE VIEW sodas_with_providers AS
SELECT s.id, s.created_at, s.updated_at, p.label AS provider_label, s.label
FROM sodas s
LEFT JOIN providers p ON p.id = s.provider_id;

Comme la vue est considérée comme une table par Pop, terminons en déclarant un nouveau modèle :

type Soda struct {
	ID                   uuid.UUID    `db:"id" rw:"r"`
	CreatedAt            time.Time    `db:"created_at" rw:"r"`
	UpdatedAt            time.Time    `db:"updated_at" rw:"r"`
	Label                string       `db:"label" rw:"r"`
	ProviderLabel        string       `db:"provider_label" rw:"r"`
}

Comme nous l'avons appris dans ce chapitre, chaque attribut de la structure possède un tag lecture seule rw:"r". Puisqu'une vue est un objet en lecture seule, cela évite de laisser passer une opération d'écriture; avant même d'atteindre la base de données.

Contenu lié

  • Migrations - Écrire des migrations de base de données.
  • Requêtage - Lire des données depuis votre base de données.