# 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.