345 lines
8.1 KiB
Markdown
345 lines
8.1 KiB
Markdown
# Lasebuche ORM
|
|
|
|
Une library ORM (Object-Relational Mapping) legere et generique pour Go. Permet de mapper des structs Go a des tables SQL avec un minimum de configuration.
|
|
|
|
## Fonctionnalites
|
|
|
|
- **CRUD complet** : Create, Read, Update, Delete operations
|
|
- **Versionnement optimiste** : Controle de concurrence avec VersionId
|
|
- **Auto-generation des IDs** : Generation automatique des identifiants uniques
|
|
- **Gestion des timestamps** : DateCreated et DateUpdated auto-remplis
|
|
- **Support SQLite** : Integre un dialecte SQLite
|
|
- **Requetes parametrees** : Protection contre les injections SQL
|
|
- **Mapping automatique** : Mapping entre structs Go et tables SQL via tags
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
go get trankilou.fr/lasebuche
|
|
```
|
|
|
|
## Utilisation
|
|
|
|
### Configuration
|
|
|
|
Importez le package et creez une table :
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"log"
|
|
orm "trankilou.fr/lasebuche"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
type User struct {
|
|
ID string `db:"id" json:"id"`
|
|
Firstname string `db:"firstname" json:"firstname,omitempty"`
|
|
Lastname string `db:"lastname" json:"lastname,omitempty"`
|
|
Email string `db:"email" json:"email,omitempty"`
|
|
Enabled bool `db:"enabled" json:"enabled"`
|
|
VersionId string `db:"_version" json:"_version"`
|
|
DateCreated time.Time `db:"_date_created" json:"_date_created"`
|
|
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
|
|
}
|
|
|
|
func main() {
|
|
// Ouvrir la connexion a la base de donnees
|
|
db, err := sql.Open("sqlite", "mydb.sqlite")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Creer le dialecte
|
|
dialect := orm.NewSqliteDialect()
|
|
|
|
// Creer une table ORM
|
|
table, err := orm.NewTable[User](db, dialect, User{}, "users")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Synchroniser le schema
|
|
err = table.Sync()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Utiliser l'ORM...
|
|
}
|
|
```
|
|
|
|
### Operations CRUD
|
|
|
|
#### Insert
|
|
|
|
```go
|
|
user := &User{
|
|
Firstname: "John",
|
|
Lastname: "Doe",
|
|
Email: "john.doe@example.com",
|
|
Enabled: true,
|
|
}
|
|
|
|
insertedUser, err := table.Insert(user)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
// ID, VersionId, et DateCreated sont auto-remplis
|
|
```
|
|
|
|
#### Get
|
|
|
|
```go
|
|
user, err := table.Get("user-id-123")
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
// Utilisateur non trouve
|
|
}
|
|
log.Fatal(err)
|
|
}
|
|
```
|
|
|
|
#### SelectOne
|
|
|
|
```go
|
|
user, err := table.SelectOne("email = $1", "john.doe@example.com")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
```
|
|
|
|
#### SelectWhere
|
|
|
|
```go
|
|
users, err := table.SelectWhere("enabled = $1", true)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
for _, u := range users {
|
|
// Traiter chaque utilisateur
|
|
}
|
|
```
|
|
|
|
#### Update
|
|
|
|
```go
|
|
user.Firstname = "Jane"
|
|
updatedUser, err := table.Update(user)
|
|
if err != nil {
|
|
// Peut echouer si VersionId ne correspond pas (versionnement optimiste)
|
|
log.Fatal(err)
|
|
}
|
|
// VersionId est regenere apres chaque mise a jour
|
|
```
|
|
|
|
#### Delete
|
|
|
|
```go
|
|
err := table.Delete("user-id-123")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
```
|
|
|
|
#### DeleteWhere
|
|
|
|
```go
|
|
err := table.DeleteWhere("email LIKE $1", "%@example.com")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
```
|
|
|
|
### Versionnement Optimiste
|
|
|
|
Le systeme utilise un champ `VersionId` pour implementer le versionnement optimiste. A chaque mise a jour, un nouveau VersionId est genere. Si une mise a jour est tentee avec un VersionId obsolète, l'operation echouera.
|
|
|
|
```go
|
|
// Premier utilisateur
|
|
user1, _ := table.Get("user-id")
|
|
version1 := user1.VersionId
|
|
|
|
// Deuxieme lecture (apres une mise a jour par quelqu'un d'autre)
|
|
user2, _ := table.Get("user-id")
|
|
version2 := user2.VersionId // Different de version1
|
|
|
|
// Tentative de mise a jour avec l'ancienne version
|
|
user1.Firstname = "NewName"
|
|
_, err := table.Update(user1) // Echouera car VersionId est obsolète
|
|
if err != nil {
|
|
// Erreur: "no rows affected: record not found or version mismatch"
|
|
}
|
|
```
|
|
|
|
## Struct Tags
|
|
|
|
Les structs doivent utiliser le tag `db` pour mapper les champs aux colonnes SQL :
|
|
|
|
```go
|
|
type Product struct {
|
|
ID string `db:"id"` // Colonne 'id'
|
|
Name string `db:"name"` // Colonne 'name'
|
|
Price float64 `db:"price"` // Colonne 'price'
|
|
CreatedAt time.Time `db:"created_at"` // Colonne 'created_at'
|
|
VersionId string `db:"version"` // Colonne 'version' pour le versionnement
|
|
}
|
|
```
|
|
|
|
### Champs Speciaux
|
|
|
|
- **ID** : Champ string, auto-rempli avec GenID() lors de l'insertion
|
|
- **VersionId** : Champ string, auto-rempli avec GenID() lors de l'insertion, regenere a chaque mise a jour
|
|
- **DateCreated** : Champ time.Time, auto-rempli avec l'heure actuelle lors de l'insertion
|
|
- **DateUpdated** : Champ *time.Time ou time.Time, mis a jour avec l'heure actuelle lors de chaque mise a jour
|
|
|
|
## API Reference
|
|
|
|
### Table[T]
|
|
|
|
#### NewTable[T](db *sql.DB, dialect Dialect, sample T, tablename string) (Table[T], error)
|
|
|
|
Cree une nouvelle instance de table ORM.
|
|
|
|
#### (t Table[T]) Sync() error
|
|
|
|
Synchronise le schema de la table avec la base de donnees.
|
|
|
|
#### (t Table[T]) Insert(value *T) (*T, error)
|
|
|
|
Insere un nouvel enregistrement. Retourne l'objet avec les champs auto-remplis.
|
|
|
|
#### (t Table[T]) Get(id string) (*T, error)
|
|
|
|
Recupere un enregistrement par son ID.
|
|
|
|
#### (t Table[T]) SelectOne(where string, args ...any) (*T, error)
|
|
|
|
Recupere un seul enregistrement avec une clause WHERE.
|
|
|
|
#### (t Table[T]) SelectWhere(where string, args ...any) ([]*T, error)
|
|
|
|
Recupere plusieurs enregistrements avec une clause WHERE.
|
|
|
|
#### (t Table[T]) Update(value *T) (*T, error)
|
|
|
|
Met a jour un enregistrement. Genere un nouveau VersionId. Retourne une erreur si le VersionId ne correspond pas a la version actuelle.
|
|
|
|
#### (t Table[T]) Delete(id string) error
|
|
|
|
Supprime un enregistrement par son ID.
|
|
|
|
#### (t Table[T]) DeleteWhere(where string, args ...any) error
|
|
|
|
Supprime des enregistrements avec une clause WHERE.
|
|
|
|
#### (t Table[T]) Debug()
|
|
|
|
Affiche les requetes SQL pre-compilees pour le debogage.
|
|
|
|
### Dialect
|
|
|
|
#### NewSqliteDialect() Dialect
|
|
|
|
Cree un dialecte pour SQLite.
|
|
|
|
## Exemple Complet
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
orm "trankilou.fr/lasebuche"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
type Task struct {
|
|
ID string `db:"id"`
|
|
Title string `db:"title"`
|
|
Description string `db:"description"`
|
|
Completed bool `db:"completed"`
|
|
VersionId string `db:"version"`
|
|
DateCreated time.Time `db:"created_at"`
|
|
DateUpdated *time.Time `db:"updated_at"`
|
|
}
|
|
|
|
func main() {
|
|
db, err := sql.Open("sqlite", "./tasks.db")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
dialect := orm.NewSqliteDialect()
|
|
table, err := orm.NewTable[Task](db, dialect, Task{}, "tasks")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Synchroniser la table
|
|
err = table.Sync()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Inserer une tache
|
|
task := &Task{
|
|
Title: "Apprendre Go",
|
|
Description: "Etudier le langage Go",
|
|
Completed: false,
|
|
}
|
|
|
|
inserted, err := table.Insert(task)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Printf("Tache creee avec ID: %s, Version: %s\n", inserted.ID, inserted.VersionId)
|
|
|
|
// Mettre a jour la tache
|
|
inserted.Title = "Maitriser Go"
|
|
updated, err := table.Update(inserted)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Printf("Tache mise a jour, Nouvelle version: %s\n", updated.VersionId)
|
|
|
|
// Recuperer toutes les taches
|
|
tasks, err := table.SelectWhere("")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
for _, t := range tasks {
|
|
fmt.Printf("Tache: %s (terminee: %v)\n", t.Title, t.Completed)
|
|
}
|
|
|
|
// Supprimer la tache
|
|
err = table.Delete(inserted.ID)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Println("Tache supprimee")
|
|
}
|
|
```
|
|
|
|
## Dependances
|
|
|
|
- Go 1.18+ (pour les generics)
|
|
- modernc.org/sqlite (driver SQLite)
|
|
- github.com/sixafter/nanoid (generation d'IDs)
|
|
|
|
## Contribution
|
|
|
|
Les contributions sont les bienvenues ! Ouvrez une issue ou soumettez une pull request.
|
|
|
|
## Licence
|
|
|
|
Ce projet est sous licence MIT.
|