diff --git a/Src/A69WD/A69WD.csproj b/Src/A69WD/A69WD.csproj index c18b60b..4fa7741 100644 --- a/Src/A69WD/A69WD.csproj +++ b/Src/A69WD/A69WD.csproj @@ -62,6 +62,7 @@ + diff --git a/Src/A69WD/Data/DBTools.cs b/Src/A69WD/Data/DBTools.cs index 33d17db..1b3e1a4 100644 --- a/Src/A69WD/Data/DBTools.cs +++ b/Src/A69WD/Data/DBTools.cs @@ -1071,106 +1071,6 @@ namespace A69WD { } } - public cRac GetRacById(long IDrac) { - /* - * Il metodo più pulito e leggibile consiste nell'eseguire una query principale per - * i dati del racconto e poi delle query mirate per popolare le collezioni. - * - * Ecco come implementare la funzione in C# utilizzando Microsoft.Data.Sqlite. - * - * Spiegazione della logica: - * - LEFT JOIN: Ho usato i LEFT JOIN nella query principale così, anche se un racconto - * non ha ancora una trama o un voto assegnato, il record viene comunque recuperato - * correttamente senza errori. - * - Mapping Manuale: Poiché non stiamo usando un ORM pesante (come Entity Framework), - * mappiamo manualmente le colonne del reader alle proprietà dell'oggetto cRac. - * - Gestione Valori Nulli: Noterai il controllo reader["Trama"]?.ToString() ?? "" che - * evita eccezioni nel caso in cui certi campi opzionali siano vuoti nel database. - * - Helper GetList: Per evitare di riscrivere lo stesso codice 3 volte per Autori, - * Tags e Origini, ho creato una piccola funzione di supporto. - * - * Questo approccio è molto performante su SQLite e ti restituisce l'oggetto cRac - * esattamente come lo avevi immaginato, pronto per essere mostrato nella tua - * interfaccia utente. - * - */ - lock (objLock) { - cRac rac = null; - - // 1. Query principale: Dati Racconto, Trama e Voto - string queryBase = @" - SELECT r.*, t.Trama, v.Voto as Voto - FROM Racconti r - LEFT JOIN Trame t ON r.IDrac = t.IDrac - LEFT JOIN Links_Rac_Vot lrv ON r.IDrac = lrv.IDrac - LEFT JOIN Voti v ON lrv.IDvot = v.IDvot - WHERE r.IDrac = @IDrac"; - using (SQLiteCommand cmd = new SQLiteCommand(queryBase, cn)) { - cmd.Parameters.AddWithValue("@IDrac", IDrac); - using (SQLiteDataReader reader = cmd.ExecuteReader()) { - if (reader.Read()) { - rac = new cRac { - IDrac = Convert.ToInt64(reader["IDrac"]), - Title = reader["Title"].ToString(), - SubTitle = reader["SubTitle"].ToString(), - Creato = DateTime.Parse(reader["Creato"] as string), - Updated = DateTime.Parse(reader["Aggiornato"].ToString()), - Trama = reader["Trama"]?.ToString() ?? "", - Voto = reader["Voto"]?.ToString() ?? "Non Letto" - }; - } - } - } - if (rac == null) return null; // Racconto non trovato - - // 2. Recupero Autori - rac.Autori = GetList(cn, "SELECT a.Autore FROM Autori a JOIN Links_Rac_Aut l ON a.IDaut = l.IDaut WHERE l.IDrac = @IDrac", IDrac); - - // 3. Recupero Tags - rac.Tags = GetList(cn, "SELECT t.Tag FROM Tags t JOIN Links_Rac_Tag l ON t.IDtag = l.IDtag WHERE l.IDrac = @IDrac", IDrac); - - // 4. Recupero Origini - rac.Origini = GetList(cn, "SELECT o.Origine FROM Origini o JOIN Links_Rac_Ori l ON o.IDori = l.IDori WHERE l.IDrac = @IDrac", IDrac); - - // 5. Recupero Note (Key-Value) - string queryNotes = @" - SELECT n.NKey, n.NVal - FROM Notes n - JOIN Links_Rac_Not l ON n.IDnot = l.IDnot - WHERE l.IDrac = @IDrac"; - using (var cmd = new SQLiteCommand(queryNotes, cn)) { - cmd.Parameters.AddWithValue("@IDrac", IDrac); - using (var reader = cmd.ExecuteReader()) { - while (reader.Read()) { - cNota nt = new cNota(reader["NKey"].ToString(), reader["NVal"].ToString()); - rac.Notes.Add(nt); - } - } - } - - // // 6. Recupero Segnalini - // string querySng = @" - // SELECT s.Linea, s.Segnalino - // FROM Segnalini s - // JOIN Links_Rac_Sng l ON s.IDsng = l.IDsng - // WHERE l.IDrac = @IDrac"; - // using (var cmd = new SQLteCommand(querySng, cn) { - // cmd.Parameters.AddWithValue("@IDrac", IDrac); - // using (var reader = cmd.ExecuteReader()) { - // while (reader.Read()) { - // rac.Segnalini.Add(new SegnalinoInfo { - // Linea = Convert.ToInt32(reader["Linea"]), - // Descrizione = reader["Segnalino"].ToString() - // }); - // } - // } - // } - - return rac; - - }//lock - } - /// /// Importa il racconto passato sul DB /// @@ -1825,21 +1725,218 @@ namespace A69WD { - // Helper per la lettura rapida di liste di stringhe - public static List GetList(SQLiteConnection conn, string sql, long IDrac) { - var list = new List(); - using (var cmd = new SQLiteCommand(sql, conn)) { - cmd.Parameters.AddWithValue("@IDrac", IDrac); - using (var reader = cmd.ExecuteReader()) { - while (reader.Read()) list.Add(reader.GetValue(0).ToString()); + // Metodi ricavati ottimizzando i miei tramite gemini + + /// + /// Ricava il racconto con l'ID indicato leggendolo dal DB + /// + /// Indice identificativo del racconto voluto + /// oggetto cRac che rappresenta il racconto + public cRac GetRacById(long IDrac) { + /* + * Il metodo più pulito e leggibile consiste nell'eseguire una query principale per + * i dati del racconto e poi delle query mirate per popolare le collezioni. + * + * Ecco come implementare la funzione in C# utilizzando Microsoft.Data.Sqlite. + * + * Spiegazione della logica: + * - LEFT JOIN: Ho usato i LEFT JOIN nella query principale così, anche se un racconto + * non ha ancora una trama o un voto assegnato, il record viene comunque recuperato + * correttamente senza errori. + * - Mapping Manuale: Poiché non stiamo usando un ORM pesante (come Entity Framework), + * mappiamo manualmente le colonne del reader alle proprietà dell'oggetto cRac. + * - Gestione Valori Nulli: Noterai il controllo reader["Trama"]?.ToString() ?? "" che + * evita eccezioni nel caso in cui certi campi opzionali siano vuoti nel database. + * - Helper GetList: Per evitare di riscrivere lo stesso codice 3 volte per Autori, + * Tags e Origini, ho creato una piccola funzione di supporto. + * + * Questo approccio è molto performante su SQLite e ti restituisce l'oggetto cRac + * esattamente come lo avevi immaginato, pronto per essere mostrato nella tua + * interfaccia utente. + * + */ + lock (objLock) { + try { + + cRac rac = null; + + // 1. Query principale: Dati Racconto, Trama e Voto + string queryBase = @" + SELECT r.*, t.Trama, v.Voto as Voto + FROM Racconti r + LEFT JOIN Trame t ON r.IDrac = t.IDrac + LEFT JOIN Links_Rac_Vot lrv ON r.IDrac = lrv.IDrac + LEFT JOIN Voti v ON lrv.IDvot = v.IDvot + WHERE r.IDrac = @IDrac"; + using (SQLiteCommand cmd = new SQLiteCommand(queryBase, cn)) { + cmd.Parameters.AddWithValue("@IDrac", IDrac); + using (SQLiteDataReader reader = cmd.ExecuteReader()) { + if (reader.Read()) { + rac = new cRac { + IDrac = Convert.ToInt64(reader["IDrac"]), + Title = reader["Title"].ToString(), + SubTitle = reader["SubTitle"].ToString(), + Creato = DateTime.Parse(reader["Creato"] as string), + Updated = DateTime.Parse(reader["Aggiornato"].ToString()), + Trama = reader["Trama"]?.ToString() ?? "", + Voto = reader["Voto"]?.ToString() ?? "Non Letto" + }; + } + } + } + if (rac == null) return null; // Racconto non trovato + + // 2. Recupero Autori + rac.Autori = Helper.GetList(cn, "SELECT a.Autore FROM Autori a JOIN Links_Rac_Aut l ON a.IDaut = l.IDaut WHERE l.IDrac = @IDrac", IDrac); + + // 3. Recupero Tags + rac.Tags = Helper.GetList(cn, "SELECT t.Tag FROM Tags t JOIN Links_Rac_Tag l ON t.IDtag = l.IDtag WHERE l.IDrac = @IDrac", IDrac); + + // 4. Recupero Origini + rac.Origini = Helper.GetList(cn, "SELECT o.Origine FROM Origini o JOIN Links_Rac_Ori l ON o.IDori = l.IDori WHERE l.IDrac = @IDrac", IDrac); + + // 5. Recupero Note (Key-Value) + string queryNotes = @" + SELECT n.NKey, n.NVal + FROM Notes n + JOIN Links_Rac_Not l ON n.IDnot = l.IDnot + WHERE l.IDrac = @IDrac"; + using (var cmd = new SQLiteCommand(queryNotes, cn)) { + cmd.Parameters.AddWithValue("@IDrac", IDrac); + using (var reader = cmd.ExecuteReader()) { + while (reader.Read()) { + cNota nt = new cNota(reader["NKey"].ToString(), reader["NVal"].ToString()); + rac.Notes.Add(nt); + } + } + } + + // // 6. Recupero Segnalini + // string querySng = @" + // SELECT s.Linea, s.Segnalino + // FROM Segnalini s + // JOIN Links_Rac_Sng l ON s.IDsng = l.IDsng + // WHERE l.IDrac = @IDrac"; + // using (var cmd = new SQLteCommand(querySng, cn) { + // cmd.Parameters.AddWithValue("@IDrac", IDrac); + // using (var reader = cmd.ExecuteReader()) { + // while (reader.Read()) { + // rac.Segnalini.Add(new SegnalinoInfo { + // Linea = Convert.ToInt32(reader["Linea"]), + // Descrizione = reader["Segnalino"].ToString() + // }); + // } + // } + // } + + return rac; + + } catch (Exception ex) { + Logger.Error($"GetRacById({IDrac}) error: {ex.Message}"); + return null; } - } - return list; + }//lock + } + + /// + /// Salva il racconto nel DB + /// + /// Racconto da salvare sul DB + /// Esito del salvataggio + public SaveResult SalvaRacconto(cRac racconto) { + lock (objLock) { + using (var transaction = cn.BeginTransaction()) { + try { + + // 1. Inserimento Record Principale [Racconti] + var cmdRac = new SQLiteCommand(@" + INSERT INTO Racconti (Title, SubTitle, Creato, Aggiornato) + VALUES (@t, @st, @c, @a); + SELECT last_insert_rowid();", + cn, transaction); + cmdRac.Parameters.AddWithValue("@t", racconto.Title); + cmdRac.Parameters.AddWithValue("@st", racconto.SubTitle); + cmdRac.Parameters.AddWithValue("@c", racconto.Creato); + cmdRac.Parameters.AddWithValue("@a", racconto.Updated); + + long newIDrac = (long)cmdRac.ExecuteScalar(); // + racconto.IDrac = newIDrac; + + // 2. Salvataggio Relazioni Molti-a-Molti + + // Inserimento Trama (Tabella [Trame]) + if (!string.IsNullOrWhiteSpace(racconto.Trama)) { + var cmdTra = new SQLiteCommand("INSERT INTO Trame (IDrac, Trama) VALUES (@id, @txt)", cn, transaction); + cmdTra.Parameters.AddWithValue("@id", newIDrac); + cmdTra.Parameters.AddWithValue("@txt", racconto.Trama); + cmdTra.ExecuteNonQuery(); // + } + + // Inserimento Voto (Tabella [Links_Rac_Vot]) + if (!string.IsNullOrWhiteSpace(racconto.Voto)) { + var cmdVot = new SQLiteCommand("INSERT OR REPLACE INTO Links_Rac_Vot (IDrac, IDvot) VALUES (@r, @v)", cn, transaction); + cmdVot.Parameters.AddWithValue("@r", newIDrac); + cmdVot.Parameters.AddWithValue("@v", racconto.Voto); // IDvot corrisponde al valore rating + cmdVot.ExecuteNonQuery(); + } + + // 3. Salvataggio Relazioni Molti-a-Molti + + // Autori + racconto.Autori.ForEach(autore => { + long idAut = Helper.GetOrCreateId(cn, transaction, "Autori", "Autore", "IDaut", autore); + Helper.ExecuteLink(cn, transaction, "Links_Rac_Aut", "IDaut", newIDrac, idAut); + }); + + // Tags + racconto.Tags.ForEach(tag => { + long idTag = Helper.GetOrCreateId(cn, transaction, "Tags", "Tag", "IDtag", tag); + Helper.ExecuteLink(cn, transaction, "Links_Rac_Tag", "IDtag", newIDrac, idTag); + }); + + // Origini Multiple + racconto.Origini.ForEach(origine => { + long idOri = Helper.GetOrCreateId(cn, transaction, "Origini", "Origine", "IDori", origine); + Helper.ExecuteLink(cn, transaction, "Links_Rac_Ori", "IDori", newIDrac, idOri); + }); + + // Note (Coppia Chiave-Valore) + foreach (var nota in racconto.Notes) { + var idNot = Helper.GetOrCreateNoteId(cn, transaction, nota.Key, nota.Description); + Helper.ExecuteLink(cn, transaction, "Links_Rac_Not", "IDnot", newIDrac, idNot); + } + + // // 4. Inserimento Segnalini (Tabella [Segnalini]) + // foreach (var sng in racconto.Segnalini) { + // var cmdSng = new SqliteCommand(@" + // INSERT INTO Segnalini (Linea, Segnalino) VALUES (@l, @s); + // SELECT last_insert_rowid();", connection, transaction); + // cmdSng.Parameters.AddWithValue("@l", sng.Linea); + // cmdSng.Parameters.AddWithValue("@s", sng.Descrizione); + // long idSng = (long)cmdSng.ExecuteScalar(); + // + // // Link Segnalino al Racconto + // ExecuteLink(connection, transaction, "Links_Rac_Sng", "IDsng", newIDrac, idSng); + // } + + transaction.Commit(); + Logger.Information($"Inserito racconto {newIDrac}"); + return SaveResult.Success; + + } catch (Exception ex) { + transaction.Rollback(); + Logger.Information($"Errore SQL durante l'inserimento del racconto racconto {racconto.Title}"); + return SaveResult.GenericError; + } + }//transaction + }//lock } - } // class DBTools + + + } //class DBTools }//namespace A69WD diff --git a/Src/A69WD/Data/Helper.cs b/Src/A69WD/Data/Helper.cs new file mode 100644 index 0000000..78c9ff1 --- /dev/null +++ b/Src/A69WD/Data/Helper.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Data.SQLite; + + +namespace A69WD.Data { + + /* + * + * Questo file contiene i metodi statici per gestire le tabelle di lookup e i collegamenti. + * Nota che questi metodi accettano SqliteTransaction per operare all'interno del contesto della transazione principale. + * + * Vantaggi di questa suddivisione: + * - Helper statico: Non serve istanziare nulla per usare le funzioni di utilità. + * - Manutenibilità: Se cambi il modo in cui cerchi un autore, lo cambi in un solo punto (Helper.cs). + * - Transazioni pulite: DBTools.cs si occupa solo di aprire la transazione e chiamare in ordine i "mattoncini" forniti dall'Helper. + * + */ + public static class Helper { + // Ottiene ID o crea nuovo record per Autori, Tags, Origini + public static long GetOrCreateId(SQLiteConnection conn, SQLiteTransaction trans, string table, string colName, string idCol, string value) { + var cmdSelect = new SQLiteCommand($"SELECT {idCol} FROM {table} WHERE {colName} = @v", conn, trans); + cmdSelect.Parameters.AddWithValue("@v", value); + var result = cmdSelect.ExecuteScalar(); + if (result != null) return (long)result; + + var cmdInsert = new SQLiteCommand($"INSERT INTO {table} ({colName}) VALUES (@v); SELECT last_insert_rowid();", conn, trans); + cmdInsert.Parameters.AddWithValue("@v", value); + return (long)cmdInsert.ExecuteScalar(); + } + + + // Helper specifico per le Note (Key-Value) + public static long GetOrCreateNoteId(SQLiteConnection conn, SQLiteTransaction trans, string key, string val) { + var cmdSelect = new SQLiteCommand("SELECT IDnot FROM Notes WHERE NKey = @k AND NVal = @v", conn, trans); + cmdSelect.Parameters.AddWithValue("@k", key); + cmdSelect.Parameters.AddWithValue("@v", val); + var result = cmdSelect.ExecuteScalar(); + if (result != null) return (long)result; + + var cmdInsert = new SQLiteCommand("INSERT INTO Notes (NKey, NVal) VALUES (@k, @v); SELECT last_insert_rowid();", conn, trans); + cmdInsert.Parameters.AddWithValue("@k", key); + cmdInsert.Parameters.AddWithValue("@v", val); + return (long)cmdInsert.ExecuteScalar(); + } + + // Crea il legame nelle tabelle Links_... + public static void ExecuteLink(SQLiteConnection conn, SQLiteTransaction trans, string table, string targetIdCol, long idRac, long targetId) { + var cmd = new SQLiteCommand($"INSERT OR IGNORE INTO {table} (IDrac, {targetIdCol}) VALUES (@r, @t)", conn, trans); + cmd.Parameters.AddWithValue("@r", idRac); + cmd.Parameters.AddWithValue("@t", targetId); + cmd.ExecuteNonQuery(); + } + + + // Helper per la lettura rapida di liste di stringhe + public static List GetList(SQLiteConnection conn, string sql, long IDrac) { + var list = new List(); + using (var cmd = new SQLiteCommand(sql, conn)) { + cmd.Parameters.AddWithValue("@IDrac", IDrac); + using (var reader = cmd.ExecuteReader()) { + while (reader.Read()) list.Add(reader.GetValue(0).ToString()); + } + } + return list; + } + + + + }//class Helper + + public enum SaveResult { + Success = 0, + DatabaseConnectionError = 1, + MainRecordInsertError = 2, + TramaInsertError = 3, + RelationInsertError = 4, + SegnaliniInsertError = 5, + GenericError = 99 + } + + +}//namespace A69WD.Data + + diff --git a/Src/A69WD/UI/frmDownloader.cs b/Src/A69WD/UI/frmDownloader.cs index 4eabc09..cc3562d 100644 --- a/Src/A69WD/UI/frmDownloader.cs +++ b/Src/A69WD/UI/frmDownloader.cs @@ -183,45 +183,71 @@ namespace A69WD { } private Service_Importer serviceImporter = null; - private void btnTest_Click(object sender, EventArgs e) { - // Test dell'esportazione in html - //int MaxRac = 60; - //pbST.Maximum = MaxRac; - //pbST.Minimum = 0; - //pbST.Value = 0; - //lblST.Text = "Inizio esportazione racconti"; - //for (int i = 0; i < 61; i++) { - // cRac rac = App.DB.GetRacById(i); - // cRac.EsportaInHtml(rac, Cfg.Settings.PathDownloadWorking); - // pbST.Value = i; - // lblST.Text = $"Esportato racconto ID={rac.strIDrac}"; - //} - //lblST.Text = $"Fine esportazione {MaxRac.ToString("000")} racconti"; + private async void btnTest_Click(object sender, EventArgs e) { + // // Test dell'esportazione in html + // int MaxRac = 60; + // pbST.Maximum = MaxRac; + // pbST.Minimum = 0; + // pbST.Value = 0; + // lblST.Text = "Inizio esportazione racconti"; + // for (int i = 0; i < 61; i++) { + // cRac rac = App.DB.GetRacById(i); + // cRac.EsportaInHtml(rac, Cfg.Settings.PathDownloadWorking); + // pbST.Value = i; + // lblST.Text = $"Esportato racconto ID={rac.strIDrac}"; + // } + // lblST.Text = $"Fine esportazione {MaxRac.ToString("000")} racconti"; - // Test funzione di lettura dal DB - Logger.Information("-- Start test 100*50 letture originali --"); - DateTime start = DateTime.Now; - for (int j = 0; j < 100; j++) { - for (int i = 1; i <= 50; i++) { - cRac rac = App.DB.GetRacById_Orig(i); - if (rac.IDrac != i) Logger.Information($"Letto ID={i.ToString()}"); - } - } - TimeSpan tp = start-DateTime.Now; - Logger.Information($" End test in {tp.ToString()}"); - Logger.Information("-- Start test 100*50 letture nuove --"); - start = DateTime.Now; - for (int j = 0; j < 100; j++) { - for (int i = 1; i <= 50; i++) { - cRac rac = App.DB.GetRacById(i); - if (rac.IDrac != i) Logger.Information($"Letto ID={i.ToString()}"); - } - } - tp = start - DateTime.Now; - Logger.Information($" End test in {tp.ToString()}"); + // // Test funzione di lettura dal DB + // Logger.Information("-- Start test 100*50 letture originali --"); + // DateTime start = DateTime.Now; + // for (int j = 0; j < 100; j++) { + // for (int i = 1; i <= 50; i++) { + // cRac rac = App.DB.GetRacById_Orig(i); + // if (rac.IDrac != i) Logger.Information($"Letto ID={i.ToString()}"); + // } + // } + // TimeSpan tp = start-DateTime.Now; + // Logger.Information($" End test in {tp.ToString()}"); + // Logger.Information("-- Start test 100*50 letture nuove --"); + // start = DateTime.Now; + // for (int j = 0; j < 100; j++) { + // for (int i = 1; i <= 50; i++) { + // cRac rac = App.DB.GetRacById(i); + // if (rac.IDrac != i) Logger.Information($"Letto ID={i.ToString()}"); + // } + // } + // tp = start - DateTime.Now; + // Logger.Information($" End test in {tp.ToString()}"); + // Test di salvataggio sul DB + // leggo 3 nuovi racconti + string file = @"C:\Commesse\_zPrivate\A69WD\Test\Down\A69_altro_Decisione-estrema_133364.html"; + cLink_A69 lk = new cLink_A69(file, "A69_altro_Decisione-estrema_133364.html"); + cRac rac1 = await lk.EstraiRac(file); + file = @"C:\Commesse\_zPrivate\A69WD\Test\Down\A69_altro_Decisione_133364 - Copy.html"; + lk = new cLink_A69(file, "A69_altro_Decisione_133364 - Copy.html"); + cRac rac2 = await lk.EstraiRac(file); + file = @"C:\Commesse\_zPrivate\A69WD\Test\Down\A69_altro_Dammi-un-bacio_153877.html"; + lk = new cLink_A69(file, "A69_altro_Dammi-un-bacio_153877.html"); + cRac rac3 = await lk.EstraiRac(file); + file = @"C:\Commesse\_zPrivate\A69WD\Test\Down\A69_altro_dammi-la-mano_157904.html"; + lk = new cLink_A69(file, "A69_altro_dammi-la-mano_157904.html"); + cRac rac4 = await lk.EstraiRac(file); + // // Controprova: + // var res1 = App.DB.ImportRac(rac1); + // var res2 = App.DB.ImportRac(rac2); + // var res3 = App.DB.ImportRac(rac3); + // var res4 = App.DB.ImportRac(rac4); + // Provo a salvarli + var res1 = App.DB.SalvaRacconto(rac1); + var res2 = App.DB.SalvaRacconto(rac2); + var res3 = App.DB.SalvaRacconto(rac3); + var res4 = App.DB.SalvaRacconto(rac4); + + // }