Introduzione

Come indicato nella documentazione ufficiale, Simple.Data è un framework che usa le funzionalità di .net per fornire metodi di accesso e di manipolazione dei dati senza la necessità della generazione preventiva di codice (che spesso è necessario per altri framework). In questo articolo esso verrà applicato a SQL Server anche se è possibile utilizzarlo con altri RDBMS e database documentali. Andremo a gestire quanti più casi possibile, mostrando le operazioni CRUD principali, la gestione delle transazioni e le chiamate alla programmabilità.


Setup e configurazione

Il processo di setup può essere eseguito via nuGet (utilizzando il Package Manager Console in Visual Studio) con il seguente comando:

PM> Install-Package Simple.Data.SqlServer

Tuttavia possiamo utilizzare anche il nuGet Package Manager, come mostrato nella figura sottostante:

Alla fine del processo di installazione, avremo tre nuove referenze nel nostro progetto (l’installer del package aggiunge Simple.Data.Ado e Simple.Data.Core, necessari per il funzionamento delle librerie per SQL Server):

Come possiamo notare, i componenti installati sono:

  • Librerie core (Simple.Data)
  • Librerie adaptor (Simple.Data.Ado)
  • Librerie provider (Simple.Data.SqlServer)

Il modello di Simple.Data è strutturato come indicato in questa figura:

Nel nostro progetto, dopo l’installazione, possiamo specificare una connection string di default (oppure una nominata) nel nostro file app.config. Leggere qui per maggiori informazioni.

Aprire una connessione

In base a come abbiamo configurato la connection string, possiamo aprire una connessione come segue:

  • Default connection con il metodo .Open(), la connection string è salvata nel file di configurazione con la chiave “Simple.Data.Properties.Settings.DefaultConnectionString”.
  • Named connection con il metodo .OpenNamedConnection(string), passando il nome della chiave indicata nel file di configurazione.
  • Connection string e provider con il metodo .OpenConnection(string[, string])
  • File con il metodo .OpenFile(string), ad esempio un file sdf)

Ecco un file app.config di esempio con una connection string definita:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<
startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<connectionStrings>
<add name="MyConnString" connectionString="Data Source=.;Initial Catalog=Samples;Integrated Security=SSPI;"/>
</connectionStrings>
</configuration>

Questo è il codice relativo per la lettura della stringa di connessione (nominata):

var db = Database.OpenConnection(ConfigurationManager.ConnectionStrings["MyConnString"].ConnectionString);

La variabile chiamata db è il punto da cui andremo ad eseguire le query ed in cui andremo ad interrogare l’albero degli oggetti. Possiamo considerarla come il nostro database vero e proprio.

 

Operazioni CRUD

Simple.Data è molto potente se usato per gestire le operazioni CRUD semplici (create, read, update and delete). Consente di creare comandi di INSERT, UPDATE, SELECT e DELETE. Purtroppo le subquery non sono supportate nella versione corrente (come descritto qui).

I paragrafi che seguiranno mostrano come scrivere codice con Simple.Data al fine di creare le relative sintassi SQL. Tratteremo comandi di INSERT, DELETE and UPDATE più in profondo, poiché la documentazione online manca di qualche informazione sul ritorno dei dati.

Immaginiamo di avere le seguenti tabelle:

CREATE TABLE dbo.TableWithoutIdentity
(      
Id
int NOT NULL PRIMARY KEY,
Value
varchar(50) NOT NULL
);

CREATE
TABLE dbo.TableWithIdentity
(
Id
int IDENTITY(1, 1) NOT NULL PRIMARY KEY,
Value
varchar(50) NOT NULL
);

Come possiamo notare, la prima tabella ha semplicemente una chiave primaria ed una colonna valore, mentre la seconda, ha l’identità impostata sulla chiave primaria. Questo cambia per Simple.Data, e vedremo il perché a breve.

 

Aggiungere dati

Negli esempi seguenti vedremo come effettuare comandi di INSERT. Ci sono quattro tipi di comando, documentati qui. Il metodo relativo è Insert, il quale supporta due forme:

  • Named parameters
  • Objects.

Sintassi (nessuna identità)

//named parameters
var result = db.dbo.TableWithoutIdentity.Insert(Id: 1, value: "Hello");
 
//objects (anonymous o POCO)
var record = new { Id = 2, Value = "Hello" };
var result = db.dbo.TableWithoutIdentity.Insert(record);

Valori di ritorno (nessuna identità)

La documentazione ufficiale indica che il valore di ritorno è una riga singola di dati relativa a quanto appena inserito. In realtà, entrambi gli esempi di cui sopra, in caso di successo, tornano NULL. Quando l’operazione di inserimento viola un vincolo (Foreign Key, Primary Key, Check) viene rilanciata un’eccezione AdoAdaptorException.

 

Sintassi (con identità)

//named parameters
var result = db.dbo.TableWithoutIdentity.Insert(value: "Hello");
 
//objects (anonymous o POCO)
var record = new { Value = "Hello" };
var result = db.dbo.TableWithoutIdentity.Insert(record);

Valori di ritorno (con identità)

Quando il record è inserito con successo, viene ritornato un SimpleRecord. La variabile result consente di accedere ai metadati del record (le colonne, come result.Id in questo esempio). Questo significa che, per ogni colonna, la variabile avrà una proprietà. Ad esempio, avendo una colonna chiamata ColumnAge sulla tabella, avremo anche un rispettivo result.ColumnAge.

var record1 = new { Value = "Hello" };
var record2 = new { Value = "Goodbye" };
var users = new[] {record1, record2};
var result = db.dbo.TableWithIdentity.Insert(users).ToList();

Quando l’operazione di inserimento viola un vincolo (Foreign Key, Primary Key, Check) viene rilanciata un’eccezione AdoAdaptorException.

Nota
: Anche gli inserimenti multipli sono supportati, ma solo nella forma con oggetti. È sufficiente creare un’array di oggetti e passarlo al metodo Insert:

L’operazione è possibile solo se la tabella ha una identità. Altrimenti la Insert tornerebbe NULL e il metodo .ToList() non andrebbe a buon fine. In tal caso, un’eccezione RuntimeBinderException verrebbe rilanciata.

Modificare dati

Negli esempi seguenti viene mostrato il commando di UPDATE. Simple.Data fornisce due metodi: UpdateAll e Update. Il primo può essere usato:
  • Senza clausola WHERE
  • Named argument
  • Espressione
Il secondo ripete la forma dell’inserimento, con una piccola differenza: 
  • Named parameters
  • Object
  • Using PK/Estensione “By”

Il metodo UpdateAll consente di modificare un set di colonne/righe con una condizione di WHERE (opzionale). Il metodo Update consente di modificare un set di colonne/righe tramite oggetti con notazione POCO. Supporta anche un’estensione chiamata UpdateBy.

Sintassi (UpdateAll)

//UpdateAll
var result = db.dbo.TableWithoutIdentity.UpdateAll(Value: "New value");
 
//UpdateAll con WHERE
var result = db.dbo.TableWithoutIdentity.UpdateAll(db.dbo.TableWithoutIdentity.Id == 1, Value: "New value");
 
//UpdateAll con named argument (Condition:)
var result = db.dbo.TableWithoutIdentity.UpdateAll(Value: "New value", Condition: db.dbo.TableWithoutIdentity.Id == 1);

Valori di ritorno (UpdateAll)

La documentazione ufficiale indica che il valore di ritorno è il numero di righe aggiornate, purtroppo manca la descrizione dell’oggetto che viene ritornato al chiamante. Infatti, questo valore è nell’espressione result.ReturnValue, che corrisponde quindi al numero di righe coinvolte nell’operazione di aggiornamento oppure a zero se non sono stati considerati record.

 

Sintassi (Update)

//UpdateBy colonna Id
var result = db.dbo.TableWithoutIdentity.UpdateById(Id: 2, Value: "New value");
 
//Update con object (anonymous o POCO)
var record = new { Id = 1, Value = "New value" };
var result = db.dbo.TableWithoutIdentity.Update(record);

Valori di ritorno (Update)

La documentazione ufficiale indica che il valore di ritorno è il numero di righe aggiornate. Il valore è in questo caso direttamente nella variabile result, che corrisponde al numero di righe coinvolte nell’operazione di aggiornamento oppure a zero se non sono stati considerati record.

 

Cancellare dati

Nei seguenti esempi vengono mostrati comandi di DELETE. Simple.Data fornisce due metodi: DeleteAll e Delete.  Il primo dei due può essere usato:
  • Senza clausola WHERE
  • Con clausola WHERE
Il secondo, indicato per le cancellazioni basate su particolari colonne, supporta due forme:
  • Named parameters
  • Estensione “By”

Anche in questo caso la documentazione manca di precisione. Essa indica che il valore di ritorno dei metodi di cancellazione è una riga o un set di righe corrispondenti a quelle coinvolte nell’operazione. In realtà, dai test, sembra che l’esecuzione delle operazioni torni il numero di righe considerate. Mentre il metodo Delete torna un intero, Il metodo DeleteAll torna un oggetto con all’interno il valore del numero di righe considerate. Esattamente come per i metodi di aggiornamento.

Sintassi (DeleteAll)

//DeleteAll
var result = db.dbo.TableWithoutIdentity.DeleteAll();
 
//DeleteAll con where
var result = db.dbo.TableWithoutIdentity.DeleteAll(db.dbo.TableWithoutIdentity.Id == 1);

Valori di ritorno (DeleteAll)

A differenza di quanto affermato nella documentazione ufficiale, l’espressione result.ReturnValue corrisponde al numero di righe coinvolte nell’operazione di aggiornamento oppure a zero se non sono stati considerati record. Provando a effettuare scan sulla variabile result, non sarà possibile tornare i SimpleRecord che ci si aspettava.

 

Sintassi (Delete)

//Delete con condizione
var result = db.dbo.TableWithoutIdentity.Delete(Value: "Hello");
 
//DeleteBy
var result = db.dbo.TableWithoutIdentity.DeleteByValue("Hello");

Valori di ritorno (Delete)

A differenza di quanto affermato nella documentazione ufficiale, la variabile result corrisponde al numero di righe coinvolte nell’operazione di aggiornamento oppure a zero se non sono stati considerati record.

 

Leggere dati

Per effettuare operazioni di SELECT, leggere qui (sezione “Retrieving “Data). Con Simple.Data possiamo:

  • Leggere più record (SimpleQuery)
  • Leggere un solo record (SimpleRecord)
  • Leggere dati scalari
  • Effettuare Join
  • Aggregare i risultati
  • Applicare funzioni built-in 
  • Tutti questi argomenti sono ben documentati online.

 

Eseguire stored procedure

Con Simple.Data, possiamo anche eseguire stored procedure. Esattamente come succede per tabelle e viste, esiste una naming convention con la quale effettuare i puntamenti agli oggetti del nostro database. Puntare una stored procedure significa invocarne l’esecuzione. Siccome la documentazione online non è così approfondita, proviamo a riassumere quanto è possibile fare:

Sintassi

//chiama la procedura proc_DoSomething (con parametri nominati)
db.dbo.proc_DoSomething(param1: 1, param2: "value");
 
//chiama la procedura proc_DoSomething (con parametri posizionali)
db.dbo.proc_DoSomething(1, "value");
 
//chiama la procedura proc_DoSomething la quale ritorna un resultset (Column1 e Column2 sono gli alias di colonna tornati)
var result = db.dbo.proc_DoSomething(param1: 1, param2: "value");
Console.WriteLine("first column:" + result.Column1);
Console.WriteLine("second column:" + result.Column2);

Valori di ritorno

Possiamo eseguire scan sulla variabile result per leggere ogni record ritornato. Ogni record è posto all’interno di un oggetto di tipo SimpleResultSet e consente l’accesso ai metadati del record stesso.

Nel caso in cui volessimo gestire parametri di output, dovremo accedere alla collezione OutputValues dell’oggetto SimpleResultSet tornato dalla chiamata. Per accedere al parametro di output dovremo indicare il nome del parametro come definito nella stored procedure:

//chiama la procedura proc_DoSomething
db.dbo.proc_DoSomething(param1: 1, param2: "value");
Console.WriteLine("Output parameter called 'myOutput' returned: " + (int)result.OutputValues["myOutput"]);

Come possiamo notare, result.OutputValues contiene la definizione ed il valore di myOutput.

Transazioni

Prima di tutto vi è da dire che l’oggetto TransactionScope non è supportato. La soluzione fornita (valida solo nella stessa istanza su cui risiedono i nostri database) consente di creare un oggetto SimpleTransaction di Simple.Data.

Sintassi

//con successo e commit
using (var transaction = db.BeginTransaction())
{
    transaction.dbo.TableWithoutIdentity.Insert(Id: 1000, Value: "SUCCESS");
    transaction.dbo.TableWithIdentity.Insert(Value: "SUCCESS");
    transaction.Commit();
}
 
//con rollback (implicito)
using (var transaction = db.BeginTransaction())
{
    transaction.dbo.TableWithoutIdentity.Insert(Id: 1000, Value: "SUCCESS");
    transaction.dbo.TableWithIdentity.Insert(Value: "SUCCESS");
    //no commit
}

Se non si esegue il metodo .Commit() dell’oggetto SimpleTransaction la rollback è implicita, così come avviene per il metodo .Complete() del TransactionScope. Vediamo cosa succede tracciando. Il setting è il seguente:

Andiamo a tracciare RPC, poiché qui Simple.Data invierà il comando e tutti i tipi di gestione di transazione (BEGIN, ROLLBACK e COMMIT) al fine di verificare se il comportamento analizzato è quello che ci aspettiamo.

Eseguendo il codice nell’esempio con rollback implicito, otterremo la traccia seguente:

La rollback implicita viene eseguita non appena si esce dal costrutto using.

Nota: Le transazioni distribuite non sono supportate. Tuttavia è possibile fare sharing di connessioni SqlConnection (ma solo con l’adapter ADO) così come descritto qui.

 

Conclusioni

Simple.Data, come il nome recita, è veramente semplice da installare, da applicare ed è dotato anche di un’interfaccia fluent di sviluppo davvero niente male. Tuttavia, e forse anche a causa della sua semplicità, non tutti i tipi di operazione sono disponibili e non tutte le sintassi SQL sono effettivamente coperte dall’implementazione (possiamo leggere alcune limitazioni qui). 
Utilizzarlo per eseguire stored procedure potrebbe non essere una strada da seguire. Teniamo a mente che si tratta di un framework orientato all’ORM e non all’astrazione delle logiche di business direttamente su database (cosa che la programmabilità t-SQL fa). 
Infine, la gestione delle transazioni tramite un oggetto dedicato non consente di utilizzare il TransactionScope, il quale non risulta supportato.

Risorse

Lista di requisiti e dipendenze necessarie per l’utilizzo di Simple.Data.
Sito da cui scaricare le librerie (GitHub disponibile, ma con Visual Studio, preferire nuGet).
Lista di naming conventions qui.