From f01e93ead7f936ee124be7fb2848fe9854b8b469 Mon Sep 17 00:00:00 2001 From: Fabien Masson Date: Sat, 8 Aug 2026 20:39:31 +0200 Subject: [PATCH] change package name --- README.md | 344 ++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- orm_test.go | 32 ++--- testdb | Bin 12288 -> 12288 bytes 4 files changed, 361 insertions(+), 17 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..060ec2b --- /dev/null +++ b/README.md @@ -0,0 +1,344 @@ +# 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. diff --git a/go.mod b/go.mod index fdb9528..7d72c08 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module trankilou.fr/lasebuche +module gitea.trankilou.fr/fabien/lasebuche go 1.26.5 diff --git a/orm_test.go b/orm_test.go index d86d8f9..a32ca12 100644 --- a/orm_test.go +++ b/orm_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" + orm "gitea.trankilou.fr/fabien/lasebuche" _ "modernc.org/sqlite" - orm "trankilou.fr/lasebuche" ) type User struct { @@ -127,7 +127,7 @@ func TestHelloName(t *testing.T) { if err != nil { t.Errorf("Insert for DeleteWhere test failed: %v", err) } - + // Delete the test user err = tableUser.DeleteWhere("firstname = $1", "Test") if err != nil { @@ -170,7 +170,7 @@ func TestCRUDOperations(t *testing.T) { Theme: "dark", Lang: "en", } - + insertedUser, err := tableUser.Insert(user1) if err != nil { t.Fatalf("Insert failed: %v", err) @@ -184,7 +184,7 @@ func TestCRUDOperations(t *testing.T) { if insertedUser.DateCreated.IsZero() { t.Error("Inserted user should have DateCreated set") } - + t.Logf("Inserted user with ID: %s, VersionId: %s", insertedUser.ID, insertedUser.VersionId) // Test Get @@ -211,7 +211,7 @@ func TestCRUDOperations(t *testing.T) { if updatedUser.DateUpdated == nil { t.Error("Update should have set DateUpdated") } - + // Verify update verifiedUser, err := tableUser.Get(insertedUser.ID) if err != nil { @@ -267,7 +267,7 @@ func TestCRUDOperations(t *testing.T) { if err != nil { t.Fatalf("Delete failed: %v", err) } - + // Verify deletion _, err = tableUser.Get(insertedUser.ID) if err != sql.ErrNoRows { @@ -279,7 +279,7 @@ func TestCRUDOperations(t *testing.T) { if err != nil { t.Fatalf("DeleteWhere failed: %v", err) } - + // Verify deletion _, err = tableUser.Get(user2.ID) if err != sql.ErrNoRows { @@ -392,12 +392,12 @@ func TestVersionLocking(t *testing.T) { Lastname: "Test", Email: "version@test.com", } - + insertedUser, err := tableUser.Insert(user) if err != nil { t.Fatalf("Insert failed: %v", err) } - + originalVersionId := insertedUser.VersionId t.Logf("Original VersionId: %s", originalVersionId) @@ -406,34 +406,34 @@ func TestVersionLocking(t *testing.T) { if err != nil { t.Fatalf("Get failed: %v", err) } - + // First update should succeed with correct version fetchedUser.Firstname = "Version2" updatedUser, err := tableUser.Update(fetchedUser) if err != nil { t.Fatalf("First update failed: %v", err) } - + // VersionId should have changed if updatedUser.VersionId == originalVersionId { t.Error("VersionId should have changed after update") } - + newVersionId := updatedUser.VersionId t.Logf("New VersionId after first update: %s", newVersionId) - + // Second update with correct version should succeed updatedUser.Firstname = "Version3" updatedUser2, err := tableUser.Update(updatedUser) if err != nil { t.Fatalf("Second update failed: %v", err) } - + // VersionId should have changed again if updatedUser2.VersionId == newVersionId { t.Error("VersionId should have changed after second update") } - + // Now try to update with old version - should fail // Create a new user with the old version oldVersionUser := &User{ @@ -443,7 +443,7 @@ func TestVersionLocking(t *testing.T) { Email: "version@test.com", VersionId: originalVersionId, // Old version } - + _, err = tableUser.Update(oldVersionUser) if err == nil { t.Error("Update with old VersionId should have failed") diff --git a/testdb b/testdb index cd5b420b2dc86e8873053d8bfabfc35ef1d5fb6f..1dfde8e4997240b52526fe74d1ad820249f24537 100644 GIT binary patch delta 246 zcmZojXh_(=DZm`Yw|BCDfWl-20hY}M0`vGp!;L*Xf^tG5(+r)xLyHrWlJgTYN+&;- z)8{v~urf8(Gd4FeGc&U=HlC~~FXC=yU}|h`mJ@1jZs407<{#*uSYFs4_!y~9F7gAFQlGc3)#HYr@>*T?EwCJAP9POSC- E0KHR1WB>pF delta 246 zcmZojXh_(=DZm`cw|BCDfWl-20hY}M0`vGpojlS)B8+l#i+oc|jI+w4e5wqxD<(gd z)8{udwlcKPvoN+Wv@kU{G@Yy{FXComYHna+S{7lVYvLN1TbOO>l5Xmf8*T?EwCJAP9POKII E09-~!r2qf`