Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Design pattern per il Data Access Layer

Progettare lo strato di accesso ai dati
Progettare lo strato di accesso ai dati
Link copiato negli appunti

Abbiamo già parlato dell'importanza che riveste l'astrazione in livelli delle applicazioni Web, in un articolo precedente in cui indicavamo un modello a 3 livelli: Data Access Layer, Business Logic Layer, User Interface). In questo articolo approfondiamo l'accesso ai dati.

Il Data Access Layer (da ora in poi DAL) di un'applicazione è il livello di interazione con la base di dati, dove si effettuano le connessioni e si eseguono query di selezione, inserimento, aggiornamento o cancellazione. Detto questo dovrebbe essere chiaro il grado di importanza di questo "strato".

L'implementazione di tutta la logica di accesso alle informazioni deve essere allo stesso tempo di facile utilizzo e strutturalmente robusta, questo sia per garantire una corretta astrazione dal tipo di database che si intende utilizzare, sia per fornire ai livelli superiori un modello unificato e semplice da usare, in modo tale che a livello di presentazione (le Web Form), ci si limiti a trattare solo la visualizzazione delle informazioni.

Le prime versioni di ASP.NET (1.0 e 1.1) non offrivano strumenti o classi apposite per la realizzazione dell'architettura del DAL e del Domain Model delle proprie applicazioni; si era costretti a scrivere tutto a mano, dalla gestione della connessione al mapping degli oggetti della base di dati, implementando i design pattern più appropriati alle proprie esigenze.

La versione 2.0 del .NET Framework ha introdotto classi che implementassero questi pattern architetturali, il che ad esempio, ha facilitato molto la creazione di DAL indipendenti dal database (attraverso le classi del namespace System.Data.Common).

Successivamente sono arrivati gli ORM, soluzioni diventate vieppiù robuste e che offrono oggi una via ottimale per gestire l'accesso ai dati, con meccanismi di mapping degli oggetti (a formare il Domain Model) e persistenza delle informazioni in memoria. Le più famose implementazionei di ORM per applicazioni .NET sono sicuramente NHibernate e LINQ to SQL (di casa Microsoft).

Durante lo sviluppo di grossi progetti quindi, risulta ormai impossibile evitare l'utilizzo di un DAL per la gestione dell'interazione con la base di dati e, allo stesso tempo, non è sempre possibile affidare tale compito interamente ad un ORM, ma dovrebbe essere buona regola scrivere per intero l'architettura del proprio livello di accesso ai dati facendosi aiutare da un ORM per le parti più ripetitive.

Vediamo ora come utilizzare due dei pattern architetturali più famosi per la creazione di un DAL personalizzato per l'accesso alle informazioni presenti nel database Northwind.

Nota: per il corretto funzionamento del codice allegato e per comprendere a fondo il codice presente in questo articolo, bisogna prima scaricare il database Northwind dal download center di Microsoft ed installarlo in locale, in quanto questo non è presente nella versione 2005 di SQL Server.

Utilizzo del pattern Abstract Factory

Il modello più utilizzato per l'astrazione del proprio DAL dalla tipologia di database utilizzato è l'Abstract Factory Pattern. Prevede la creazione di una classe astratta dalla quale possiamo istanziare famiglie di oggetti, tra loro correlate, in questo modo non c'è necessità da parte dei client di specificare i nomi delle classi concrete all'interno del proprio codice.

Figura 1. Abstract Factory Pattern (fonte: ugidotnet.org)
Abstract Factory Pattern

Questo design pattern può essere applicato ad un DAL specificando una classe astratta (nel nostro caso la classe Database) che ha il compito di definire tutti i metodi per l'utilizzo di tutti gli oggetti utili ad una connessione con la base di dati (quindi command, adapter, reader e chiaramente connection); tali metodi saranno poi implementati effettivamente da classi, dette appunto "concrete", che realizzano l'accesso a singoli database (quindi SqlConnection per Sql Server, OleDbConnection per Access, e così via).

Listato 1. La classe astratta: Database.cs

namespace PeppeDotNet.it.DAL.AbstractFactoryPattern
{
  public abstract class Database
  {
    public abstract IDbCommand CreateCommand();
    public abstract IDbCommand CreateCommand(string cmdText);
    public abstract IDbCommand CreateCommand(string cmdText, IDbConnection cn);
    public abstract IDbConnection CreateConnection();
    public abstract IDbConnection CreateConnection(string cnString);

    public abstract IDbDataAdapter CreateDataAdapter();
    public abstract IDbDataAdapter CreateDataAdapter(IDbCommand selectCmd);

    public abstract IDataReader CreateDataReader(IDbCommand dbCmd);
  }
}

Come abbiamo detto precedentemente l'implementazione di tutti i vari metodi astratti viene fatta all'interno di ogni singola classe concreta che vogliamo aggiungere al nostro sistema. All'interno del codice sorgente allegato all'articolo è presente l'implementazione sia per Sql Server che per Access che per MySql; qui, per non riempire l'articolo di codice, vediamo solamente la prima implementazione:

Listato 2. Esempio di implementazione: SqlServer.cs

namespace PeppeDotNet.it.DAL.AbstractFactoryPattern
{
  public class SqlServer : Database
  {
    public override IDbCommand CreateCommand()
    {
      IDbCommand cmd = new SqlCommand();
      return cmd;
    }
    // [... altri overload di CreateCommand]

    public override IDbConnection CreateConnection()
    {
      IDbConnection conn = new SqlConnection();
      return conn;
    }   
    // [... altri overload di CreateConnection]

    public override IDbDataAdapter CreateDataAdapter()
    {
      IDbDataAdapter adapter = new SqlDataAdapter();
      return adapter;
    }
    // [... altri overload di CreateDataAdapter]

    public override IDataReader CreateDataReader(IDbCommand dbCmd)
    {
      IDataReader reader = dbCmd.ExecuteReader();
      return reader;
    }
  }
}

Una volta implemetato ogni metodo della classe astratta, abbiamo bisogno di un ultimo "pezzo". Per utilizzare al meglio il modello appena visto, è infatti necessaria una classe "manager", in grado di gestire la connessione al DB, l'esecuzione di comandi e la chiusura della connessione, e che per tali operazioni utilizzi le classi del pattern Abstract Factory in modo tale da costituire un layer di accesso ai dati, indipendente dal tipo di database sottostante l'applicazione.

Esaminiamo un esempio di classe "Manager", la struttura e come essa utilizza le strutture viste precedentemente.

Listato 3. La classe "Manager": DatabaseManager.cs

namespace PeppeDotNet.it.DAL.AbstractFactoryPattern
{
  public sealed class DatabaseManager : IDisposable
  {
    public DatabaseManager(Provider provider)
    {
      this.provider = provider;
      switch (provider)
      {
        case Provider.SQLServer:
          database = new SqlServer();
          break;
        case Provider.Access:
          database = new Access();
          break;
        default:
          break;
      }
    }
  // Visualizza tutto il codice della classe
  }
}

Il codice scritto fin'ora può sembrare parecchio, tra classi astratte, concrete e manager, ma una volta scritto non dovrà più essere toccato e il risultato dell'utilizzo di tali strutture è veramente facile.

Listato 4. Esempio di utilizzo

DatabaseManager manager = new DatabaseManager(Provider.SQLServer);
using (manager)
{
  manager.Open();
  manager.ExecuteReader("SELECT * FROM Customers");

  GridView1.DataSource = manager.Reader;
  GridView1.DataBind();
}

Questa tecnica risulta perfetta per applicazioni Web scalabili che risultano indipendenti dal database e quindi totalmente indifferenti a cambi radicali di politiche aziendali. Come abbiamo detto nell'introduzione, un'implementazione di tale pattern è stata aggiunta nella versione 2.0 del .NET Framework, con le seguenti classi inserite all'interno del namespace System.Data.Common:

  • DbCommand
  • DbConnection
  • DbDataAdapter
  • DbParameter

e con le seguenti classi Factory:

  • System.Data.Common.DbProviderFactory
  • System.Data.Odbc.OdbcFactory
  • System.Data.OleDb.OleDbFactory
  • System.Data.OracleClient.OracleClientFactory
  • System.Data.SqlClient.SqlClientFactory

Per un esempio d'uso di queste classi, rimandiamo alla lettura dell'articolo sull'implementazione di applicazioni a 3 livelli con ASP.NET.

Da solo però, il pattern Abstract Factory, non può definire un Data Access Layer perfetto, in quanto la sua implementazione (in questo caso specifico) si preoccupa solamente del tipo di database a cui accedere e non delle informazioni presenti al suo interno. Tale architettura va infatti arricchita con logiche riguardanti il mapping delle entità e integrata con un Domain Model adeguato, utile alla rappresentazione di tali entità all'interno dell'applicazione stessa.

Utilizzo del pattern Template Method

L'implementazione di un altro noto pattern architetturale sul DAL permette l'applicazione di logiche di mapping su oggetti appartenenti al Domain Model dell'applicazione e diminuisce il numero di righe di codice da scrivere per eseguire le normali (spesso ripetitive) operazioni di selezione sulle informazioni presenti all'interno della base di dati.

Il pattern in questione è il Template Method Pattern, che prevede la definizione di una classe astratta che descrive lo schema, ma non lo implementa, delegando l'adattamento verso il basso a classi concrete, ma fornendo agli strati superiori una struttura immutata.

Figura 2. Template Method Pattern (fonte: ugidotnet.org)
Template Method Pattern

Nel nostro caso dobbiamo sviluppare due schemi: uno per l'operazione di lettura dalla base di dati e l'altro per il mapping delle informazioni lette su oggetti appartenenti al Domain Model dell'applicazione.

Entrambe le operazioni fanno parte di quei costrutti che vengono ripetuti molto spesso all'interno dell'applicazione e quindi, secondo quanto definito dal Template Method Pattern necessitano di due differenti classi astratte e di due differenti classi concrete.

Vediamo la definizione di tali classi per quanto riguarda l'operazione di mapping.

Listato 5. classe MapperBase

public abstract class MapperBase<T>
{
  public abstract T Map(IDataRecord record);

  public List<T> MapAll(IDataReader reader)
  {
    List<T> collection = new List<T>();
    while (reader.Read())
    {
      collection.Add(Map(reader));
    }
    return collection;
  }
}

La classe astratta MapperBase implementa l'algoritmo di mapping generico delle entità. Una relativa classe concreta, che realizza il mapping dell'entità Customer (definita nel Domain Model dell'applicazione) ha bisogno di implementare solamente il metodo Map().

Listato 6. Classe CustomerMapper

class CustomerMapper : MapperBase<Customer>
{
  public override Customer Map(System.Data.IDataRecord record)
  {
    Customer c = new Customer(record["CustomerID"].ToString());
    c.Address = (record["Address"] != null) ? record["Address"].ToString() : "";
    c.City = (record["City"] != null) ? record["City"].ToString() : "";
    c.CompanyName = (record["CompanyName"] != null) ? record["CompanyName"].ToString() : "";
    c.ContactName = (record["ContactName"] != null) ? record["ContactName"].ToString() : "";
    c.ContactTitle = (record["ContactTitle"] != null) ? record["ContactTitle"].ToString() : "";
    c.Country = (record["Country"] != null) ? record["Country"].ToString() : "";
    c.Fax = (record["Fax"] != null) ? record["Fax"].ToString() : "";
    c.Phone = (record["Phone"] != null) ? record["Phone"].ToString() : "";
    c.PostalCode = (record["PostalCode"] != null) ? record["PostalCode"].ToString() : "";
    c.Region = (record["Region"] != null) ? record["Region"].ToString() : "";
    
    return c;
  }
}

Così facendo, a fronte di un generico singolo record (IDataRecord) di un eventuale lettore (IDataReader), abbiamo già implementato tutto il codice che assegna le informazioni lette dalle base di dati alle proprietà di un nuovo oggetto di tipo Customer e che lo aggiunge ad una collezione di oggetti che sarà quindi il risultato dell'operazione di selezione.

Anche per quanto riguarda la selezione, abbiamo la definizione di una classe astratta e di una possibile implementazione.

Listato 7. Classe ReaderBase

public abstract class ReaderBase<T>
{        
  protected abstract Provider Provider { get; }
  protected abstract MapperBase<T> GetMapper();

  public List<T> Execute(string query)
  {
    DatabaseManager manager = new DatabaseManager(Provider);
    List<T> collection = new List<T>();
    using (manager)
    {               
      manager.Open();
      IDataReader reader = manager.ExecuteReader(query);
      MapperBase<T> mapper = GetMapper();
      collection = mapper.MapAll(reader);

      return collection;
    }
  }
}

Listato 8. Classe CustomerReader

public class CustomerReader : ReaderBase<Customer>
{
  protected override Provider Provider { get { return Provider.SQLServer; } }
  
  protected override MapperBase<Customer> GetMapper()
  {
    MapperBase<Customer> mapper = new CustomerMapper();
    return mapper;
  }

  public List<Customer> GetAll()
  {
    return base.Execute("SELECT * FROM Customers");
  }

  public List<Customer> GetAllByCity(string city)
  {
    return base.Execute(String.Format("SELECT * FROM Customers WHERE City = '{0}'", city));
  }
}

La classe astratta fornisce il meccanismo di selezione (attraverso il metodo Execute()), mentre quella concreta definisce il tipo di mapper da utilizzare (implementando il metodo GetMapper()) e vari metodi di selezione (metodi GetAll() e GetAllByCity()).

Inoltre, per le operazioni di connessione alla base di dati utilizza le classi definite precedentemente e basate sull'Abstract Factory Pattern.

Con l'aggiunta delle strutture secondo il pattern Template, la selezione di informazioni sulla base di dati risulta ancora più semplice.

Listato 9. Esempio di utilizzo del mapping

CustomerReader reader = new CustomerReader();
List<Customer> customers = reader.GetAll();

GridView1.DataSource = customers;
GridView1.DataBind();

Ora il nostro Data Access Layer è quasi pronto per l'utilizzo, manca solamente la definizione di tutte le entity proprie del Domain Model dell'applicazione e la relativa definizione di tutte le classi concrete di mapping su tali entità e di lettura.

Per semplificare la gestione delle entity (e non solo), soprattutto all'interno di progetti di grosse dimensioni, risulta spesso molto utile appoggiarsi ad un ORM. Nel nostro caso, solo per la generazione della classi entità mappate sulla struttura della base di dati, ci potrebbe essere d'aiuto SubSonic, un progetto open-source che a partire dalla definizione di un database, genera in automatico entity e classi manager da inserire all'interno del proprio progetto.

Utilizzo di un ORM

Pensare che tutto quello che abbiamo visto fin'ora lo può fare in automatico "qualcun altro" è sicuramente gratificante. Esattamente, un ORM, definisce il mapping tra gli oggetti della propria applicazione e quelli presenti all'interno della base di dati, fornendo anche tutti i metodi per l'accesso e la modifica delle informazioni. Risulta quindi ovvio che l'utilizzo di uno strumento del genere nei propri progetti semplifica notevolmente il lavoro, soprattutto per piccole applicazioni da dover sviluppare in poco tempo (quindi come nella maggior parte dei casi).

Per quanto riguarda invece, progetti di grosse dimensioni architetturali, è sempre bene affiancare ad un ORM una propria gestione personalizzata del DAL dell'applicazione; così facendo si può avere il controllo completo sull'accesso ai dati, soprattutto per quanto riguarda le performance dell'applicazione, e allo stesso tempo è possibile avvalersi di uno strumento potente che evita errori di base sui nomi e soprattutto la stesura di una grossa quantità di righe di codice.

Conclusioni

L'accesso ai dati richiede particolare attenzione nello sviluppo di applicazioni Web (e non) e mantenere questi meccanismi in un livello a parte è vantaggioso, specie per lo sviluppo degli altri strati dell'applicazione come l'interfaccia utente.

Rispettare precisi pattern architetturali, rende ancora più facile esporre funzionalità agli altri livelli, diminuire la scrittura di codice ripetitivo ed ottenere applicazioni più flessibili.

Giuseppe Marchi è consulente informatico in ambito Microsoft .NET e dottore in Comunicazione Digitale; co-autore del libro "Pocket C#", editore Apogeo, collabora con community on-line di sviluppatori fornendo articoli e materiale. Dal 2006 è certificato Microsoft su ASP.NET 2.0 e Microsoft Certified Technology Specialist su Windows Sharepoint Services 3.0. Il suo sito Web personale www.peppedotnet.it contiene ulteriori informazioni ed esempi di codice.

Ti consigliamo anche