Riprendendo il discorso lasciato in un mio precedente post, vediamo di creare una soluzione che ci faccia capire come i Dataset Distribuiti possano essere utilizzati per creare soluzioni che impieghino questa tecnica per l'accesso ai dati.
Lavoreremo con un Database di nome ECommerce. La struttura è volutamente molto semplice (solo 2 tabelle) perchè non è importante complicare lo schema del database per comprendere come il Dataset Distribuito possa essere utilizzato per i nostri fini. Le tabelle sono Order e OrderDetail, collegate con una relazione uno a molti.
Procediamo creando un progetto di nome CodeSapiens.DataAccess, in cui creeremo il nostro dataset di nome DataContext.xsd, ed una classe DataManager che incapsulerà i metodi utilizzati per accedere ai dati. Dovremo creare contestualmente anche un progetto "Class Library" di nome CodeSapiens.Entities. Questo progetto verrà utilizzato indicando nel Dataset il "DataSet Project", ovvero il progetto in cui il dataset genererà le classi "entità". Come abbiamo già visto nel precedente post, infatti, il dataset distribuito separa nettamente l'accesso ai dati dalla parte di "entità".
Altra proprietà a cui fare attenzione nel Dataset è la "Hierarchical Update". La troveremo già impostata a True, e questo indica che il generatore di codice avrà generato per noi una classe TableAdapterManager ed un metodo UpdateAll che avrà come parametro un'istanza del nostro Dataset. Questo metodo consente di aggiornare tutte le tabelle del Dataset rispettando l'ordine delle relazioni tra le tabelle che compongono il dataset. In precedenza eravamo noi che dovevamo sopperire all'ordinamento degli insert/update/delete effettuati dal dataset. Per maggiori approfondimenti riguardanti questa interessante caratteristica vi invito a consultare questo riferimento.
Nel pezzo di codice seguente possiamo vedere i metodi della classe DataManager, la quale si occuperà di implementare i metodi ad alto livello che nasconderanno le particolari implementazioni a livello TableAdapter, stringa di connessione ed utilizzo del Dataset per quanto riguarda l'accesso ai dati. In pratica si tratterà di scrivere dei metodi che accettino in input tipi base del Framework e/o classi delle Entities derivate dal Dataset DataContext già creato nel nostro progetto.
1: /// <summary>
2: /// Salva un ordine
3: /// </summary>
4: /// <param name="order">Dataset che rappresenta l'ordine</param>
5: public void SaveOrder(DataContext order)
6: {
7: if (order != null)
8: {
9: CodeSapiens.DataAccess.DataContextTableAdapters.TableAdapterManager _ta = new CodeSapiens.DataAccess.DataContextTableAdapters.TableAdapterManager();
10: _ta.OrderTableAdapter = new CodeSapiens.DataAccess.DataContextTableAdapters.OrderTableAdapter();
11: _ta.OrderDetailTableAdapter = new CodeSapiens.DataAccess.DataContextTableAdapters.OrderDetailTableAdapter();
12: _ta.Connection.ConnectionString = _connectionString;
13: _ta.UpdateAll(order);
14: }
15: else
16: {
17: throw new Exception("Order can't be null.");
18: }
19: }
20:
21: /// <summary>
22: /// Carica un ordine
23: /// </summary>
24: /// <param name="orderId">Id interno (guid) dell'ordine</param>
25: /// <returns></returns>
26: public DataContext LoadOrder(Guid orderId)
27: {
28: DataContext _context = new DataContext();
29: DataContextTableAdapters.OrderTableAdapter _orderAdapter = new DataContextTableAdapters.OrderTableAdapter();
30: _orderAdapter.Connection.ConnectionString = _connectionString;
31: DataContextTableAdapters.OrderDetailTableAdapter _orderDetailAdapter = new DataContextTableAdapters.OrderDetailTableAdapter();
32: _orderDetailAdapter.Connection.ConnectionString = _connectionString;
33: _orderAdapter.FillByOrderId(_context.Order, orderId);
34: _orderDetailAdapter.FillByOrderId(_context.OrderDetail, orderId);
35: return _context;
36: }
Notiamo come tutto il codice sia privo di istruzioni SQL. Queste istruzioni sono memorizzate all'interno dei Table Adapters a livello di Dataset, e sono istruzioni SQL che abbiamo scritto personalmente. Vediamo dei fatti derivanti da questo approccio:
- Posso scrivere il codice SQL ESATTAMENTE come voglio, ottimizzando così le mie query
- Devo scrivere il codice di tutte le query, mentre con altri approcci questo viene autogenerato (a run time o a design time)
- Ho un unico punto in cui scrivere tutto il codice SQL
- Posso usare delle Stored Procedures se voglio implementare un accesso ai dati tramite esse
In questo post utilizzeremo la classe DataManager in un progetto a due livelli, ovvero Client-Server. Creiamo quindi un progetto WinForm per consumare il nostro dataset. Al progetto aggiungeremo come riferimenti il progetto CodeSapiens.DataAccess e CodeSapiens.Entities e scriviamo una semplice interfaccia composta da due finestre, la principale con la lista degli ordini e la seconda da utilizzare per la modifica/creazione del singolo ordine.
Per la gestione dell'associazione dati-interfaccia impiegheremo il Databinding a design time di Visual Studio. A questo riguardo, dobbiamo fare una precisazione: tutto il databinding viene fatto tramite le entities del progetto Codesapiens.Entities, e non tramite il Dataset DataContext. Questo è molto importante, perchè in pratica crea un legame tra l'interfaccia e queste entità e non tramite l'interfaccia e l'accesso ai dati.
1: public partial class MainForm : Form
2: {
3: public MainForm()
4: {
5: InitializeComponent();
6: }
7:
8: private void Form1_Load(object sender, EventArgs e)
9: {
10: LoadOrders();
11: }
12:
13: private void LoadOrders()
14: {
15: CodeSapiens.DataAccess.DataManager _dm = new CodeSapiens.DataAccess.DataManager();
16: dataContext_OrderDataTableBindingSource.DataSource = _dm.LoadOrders().Order;
17: }
18:
19: private void bindingNavigatorAddNewItem_Click(object sender, EventArgs e)
20: {
21: EditOrderDialog _dialog = new EditOrderDialog();
22: if (_dialog.CreateNewOrder() == DialogResult.OK)
23: {
24: LoadOrders();
25: }
26: }
27:
28: private void dgvOrders_DoubleClick(object sender, EventArgs e)
29: {
30: EditOrderDialog _dialog = new EditOrderDialog();
31: if (_dialog.EditOrder(((DataContext.OrderRow)((DataRowView)dgvOrders.SelectedRows[0].DataBoundItem).Row).Id) == DialogResult.OK)
32: {
33: LoadOrders();
34: }
35: }
36: }
La form principale è estremamente semplice in quanto deve solamente caricare la lista degli ordini e permettere di andare in modifica o creazione di un nuovo ordine.
1: public DialogResult CreateNewOrder()
2: {
3: dataContext.OrderDetail.TableNewRow += new DataTableNewRowEventHandler(OrderDetail_TableNewRow);
4: DataContext.OrderRow _newOrder = dataContext.Order.NewOrderRow();
5: _newOrder.Id = Guid.NewGuid();
6: _newOrder.OrderDate = DateTime.Today;
7: _newOrder.ShipmentAddress = string.Empty;
8: _newOrder.CustomerAddress = string.Empty;
9: dataContext.Order.AddOrderRow(_newOrder);
10: return ShowDialog();
11: }
12:
13: public DialogResult EditOrder(Guid orderId)
14: {
15: dataContext.OrderDetail.TableNewRow += new DataTableNewRowEventHandler(OrderDetail_TableNewRow);
16: dataContext.Merge(_dm.LoadOrder(orderId));
17: return ShowDialog();
18: }
19:
20: void OrderDetail_TableNewRow(object sender, DataTableNewRowEventArgs e)
21: {
22: ((DataContext.OrderDetailRow)e.Row).Id = Guid.NewGuid();
23: }
24:
25: private void btnSaveAndClose_Click(object sender, EventArgs e)
26: {
27: orderBindingSource.EndEdit();
28: orderDetailDataGridView.EndEdit();
29: orderDetailBindingSource.EndEdit();
30: _dm.SaveOrder((DataContext)dataContext.GetChanges());
31: this.DialogResult = DialogResult.OK;
32: }
La form per la creazione/modifica ordine è anch'essa molto semplice, e l'accesso ai dati è mantenuto al minimo. In questo esempio non sono state gestite problematiche riguardanti concorrenza, possibili errori dovuti a errori sul database e tutto è stato semplificato per far emergere l'utilizzo del dataset distribuito.
A questo punto possiamo fare un riassunto e vedere che cosa abbiamo guadagnato con questo approccio, considerato che stiamo realizzando un Client-Server:
- Se cambiamo qualcosa sul database, è sufficiente aggiornare il Dataset per vedere propagate le modifiche anche a livello di entità
- Utilizzando il databinding di Visual Studio a Design-Time abbiamo a disposizione un eccezionale strumento che ci assicura un'ottima produttività
- Possiamo accedere anche a database non-SQL Server (Oracle, Sybase, ODBC generici), contrariamente a LINQ2SQL
- Non dobbiamo gestire a mano l'entity tracking, in quanto il dataset lo fa per noi
- Utilizzando i guid abbiamo risolto il problema della gestione dei campi identity sul database. Inoltre vedremo che in ambiente distribuito questa si rivelerà una scelta felice
- Non ci dobbiamo preoccupare dell'ordine dell'aggiornamento delle tabelle, in quanto il TableAdapterManager con le opportune configurazioni segue l'ordine di Update/Insert/Delete corretto
- Le classi delle entities, come quelle per l'accesso ai dati (TableAdapters), possono essere estese mediante l'uso delle partial class, permettendo così di implementare funzioni con visibilità limitata al progetto in cui si trovano
Ovviamente, come già detto nell'articolo precedente, questo approccio è da tenere in considerazione per applicazioni RAD di dimensioni medio/piccole. Non deve mai essere sottovalutato il fatto che comunicando attraverso le entità del dataset stiamo legando il modello (database) alla rappresentazione (interfaccia). Anche se questo può essere ricorrente e accettabile in un numero molto elevato di applicazioni (tipicamente proprio in quelle medio piccole con requisiti di tempistiche molto strette), in applicazioni enterprise di grosso livello può non essere consigliabile. Nel prossimo post relativo a questo argomento vedremo come utilizzare un servizio WCF per esporre le entità e come modificare l'applicazione Client-Server per farle utilizzare i servizi, piuttosto che accedere direttamente al database. Il codice sorgente dell'applicazione è scaricabile a questo indirizzo.