change package name
This commit is contained in:
@@ -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.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module trankilou.fr/lasebuche
|
module gitea.trankilou.fr/fabien/lasebuche
|
||||||
|
|
||||||
go 1.26.5
|
go 1.26.5
|
||||||
|
|
||||||
|
|||||||
+16
-16
@@ -6,8 +6,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
orm "gitea.trankilou.fr/fabien/lasebuche"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
orm "trankilou.fr/lasebuche"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
@@ -127,7 +127,7 @@ func TestHelloName(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Insert for DeleteWhere test failed: %v", err)
|
t.Errorf("Insert for DeleteWhere test failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the test user
|
// Delete the test user
|
||||||
err = tableUser.DeleteWhere("firstname = $1", "Test")
|
err = tableUser.DeleteWhere("firstname = $1", "Test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -170,7 +170,7 @@ func TestCRUDOperations(t *testing.T) {
|
|||||||
Theme: "dark",
|
Theme: "dark",
|
||||||
Lang: "en",
|
Lang: "en",
|
||||||
}
|
}
|
||||||
|
|
||||||
insertedUser, err := tableUser.Insert(user1)
|
insertedUser, err := tableUser.Insert(user1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Insert failed: %v", err)
|
t.Fatalf("Insert failed: %v", err)
|
||||||
@@ -184,7 +184,7 @@ func TestCRUDOperations(t *testing.T) {
|
|||||||
if insertedUser.DateCreated.IsZero() {
|
if insertedUser.DateCreated.IsZero() {
|
||||||
t.Error("Inserted user should have DateCreated set")
|
t.Error("Inserted user should have DateCreated set")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("Inserted user with ID: %s, VersionId: %s", insertedUser.ID, insertedUser.VersionId)
|
t.Logf("Inserted user with ID: %s, VersionId: %s", insertedUser.ID, insertedUser.VersionId)
|
||||||
|
|
||||||
// Test Get
|
// Test Get
|
||||||
@@ -211,7 +211,7 @@ func TestCRUDOperations(t *testing.T) {
|
|||||||
if updatedUser.DateUpdated == nil {
|
if updatedUser.DateUpdated == nil {
|
||||||
t.Error("Update should have set DateUpdated")
|
t.Error("Update should have set DateUpdated")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify update
|
// Verify update
|
||||||
verifiedUser, err := tableUser.Get(insertedUser.ID)
|
verifiedUser, err := tableUser.Get(insertedUser.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -267,7 +267,7 @@ func TestCRUDOperations(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Delete failed: %v", err)
|
t.Fatalf("Delete failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify deletion
|
// Verify deletion
|
||||||
_, err = tableUser.Get(insertedUser.ID)
|
_, err = tableUser.Get(insertedUser.ID)
|
||||||
if err != sql.ErrNoRows {
|
if err != sql.ErrNoRows {
|
||||||
@@ -279,7 +279,7 @@ func TestCRUDOperations(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("DeleteWhere failed: %v", err)
|
t.Fatalf("DeleteWhere failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify deletion
|
// Verify deletion
|
||||||
_, err = tableUser.Get(user2.ID)
|
_, err = tableUser.Get(user2.ID)
|
||||||
if err != sql.ErrNoRows {
|
if err != sql.ErrNoRows {
|
||||||
@@ -392,12 +392,12 @@ func TestVersionLocking(t *testing.T) {
|
|||||||
Lastname: "Test",
|
Lastname: "Test",
|
||||||
Email: "version@test.com",
|
Email: "version@test.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
insertedUser, err := tableUser.Insert(user)
|
insertedUser, err := tableUser.Insert(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Insert failed: %v", err)
|
t.Fatalf("Insert failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
originalVersionId := insertedUser.VersionId
|
originalVersionId := insertedUser.VersionId
|
||||||
t.Logf("Original VersionId: %s", originalVersionId)
|
t.Logf("Original VersionId: %s", originalVersionId)
|
||||||
|
|
||||||
@@ -406,34 +406,34 @@ func TestVersionLocking(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Get failed: %v", err)
|
t.Fatalf("Get failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// First update should succeed with correct version
|
// First update should succeed with correct version
|
||||||
fetchedUser.Firstname = "Version2"
|
fetchedUser.Firstname = "Version2"
|
||||||
updatedUser, err := tableUser.Update(fetchedUser)
|
updatedUser, err := tableUser.Update(fetchedUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("First update failed: %v", err)
|
t.Fatalf("First update failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// VersionId should have changed
|
// VersionId should have changed
|
||||||
if updatedUser.VersionId == originalVersionId {
|
if updatedUser.VersionId == originalVersionId {
|
||||||
t.Error("VersionId should have changed after update")
|
t.Error("VersionId should have changed after update")
|
||||||
}
|
}
|
||||||
|
|
||||||
newVersionId := updatedUser.VersionId
|
newVersionId := updatedUser.VersionId
|
||||||
t.Logf("New VersionId after first update: %s", newVersionId)
|
t.Logf("New VersionId after first update: %s", newVersionId)
|
||||||
|
|
||||||
// Second update with correct version should succeed
|
// Second update with correct version should succeed
|
||||||
updatedUser.Firstname = "Version3"
|
updatedUser.Firstname = "Version3"
|
||||||
updatedUser2, err := tableUser.Update(updatedUser)
|
updatedUser2, err := tableUser.Update(updatedUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Second update failed: %v", err)
|
t.Fatalf("Second update failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// VersionId should have changed again
|
// VersionId should have changed again
|
||||||
if updatedUser2.VersionId == newVersionId {
|
if updatedUser2.VersionId == newVersionId {
|
||||||
t.Error("VersionId should have changed after second update")
|
t.Error("VersionId should have changed after second update")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now try to update with old version - should fail
|
// Now try to update with old version - should fail
|
||||||
// Create a new user with the old version
|
// Create a new user with the old version
|
||||||
oldVersionUser := &User{
|
oldVersionUser := &User{
|
||||||
@@ -443,7 +443,7 @@ func TestVersionLocking(t *testing.T) {
|
|||||||
Email: "version@test.com",
|
Email: "version@test.com",
|
||||||
VersionId: originalVersionId, // Old version
|
VersionId: originalVersionId, // Old version
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tableUser.Update(oldVersionUser)
|
_, err = tableUser.Update(oldVersionUser)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Update with old VersionId should have failed")
|
t.Error("Update with old VersionId should have failed")
|
||||||
|
|||||||
Reference in New Issue
Block a user