Database locali con SQL Server CE in Windows Phone 7.1

di Matteo Pagani, in Windows Phone 7.1,

Una delle lamentele più forti che è stata fatta dagli sviluppatori al lancio di Windows Phone era l'assenza di un qualsiasi tipo di supporto nativo ai database relazionali: l'unico modo per salvare i dati della propria applicazione era sfruttare l'Isolated Storage e serializzare i nostri oggetti, ad esempio, in file XML. Si tratta di una soluzione pratica e agevole, ma ideale per modeste quantità di dati, possibilmente non relazionati tra di loro. L'altra l'alternativa era quella di affidarsi a servizi WCF e di memorizzare i dati server-side: questa, però, è una soluzione non adatta a tutte le esigenze, dato che richiede di realizzare e mantenere un'applicazione in più, oltre che una connessione sempre attiva.

Aggiornamento sulle ultime novità introdotte in SQL CE Toolbox

SQL CE Toolbox, la preziosa estensione di Visual Studio descritta in questo articolo, è in continua evoluzione e numerosi aggiornamenti sono stati rilasciati negli ultimi mesi. L'articolo è stato perciò aggiornato per includere tutte le novità che sono state introdotte riguardanti il supporto a Windows Phone, novità che vengono qui riepilogate.La versione a cui si riferisce questo aggiornamento dell'articolo è la 2.6.

Supporto a VB.NET

Al momento della stesura dell'articolo l'unico linguaggio supportato da SQL CE Toolbox era C#: ora è stata aggiunta una nuova opzione nella schermata di generazione del DataContext per Windows Phone che permette di specificare il linguaggio da utilizzare tra C# e VB.NET.

Supporto alla creazione di file separati per le entità

Il tool di generazione del DataContext, in origine, creava un unico file con la definizione di tutte le classi necessarie per l'utilizzo del database: il DataContext vero e proprio più tutte le entità (corrispondenti alle varie tabelle). Questo approccio non è amato da tutti gli sviluppatori, che preferiscono avere un file separato per ogni classe. Per venire incontro a questa esigenza, è stata introdotta nella schermata di generazione del DataContext una opzione per far si che venga generato un nuovo file per ogni classe.

Debugging delle query SQL

Nella lettura dell'articolo avrete imparato che l'accesso ai dati sul database viene effettuato tramite LINQ to SQL, che si occupa di fare per voi tutto il "lavoro sporco" di generazione delle query necessarie per interagire con il database. A volte, però, può essere utile conoscere le query generate da LINQ to SQL, per risolvere eventuali problemi e ottimizzare le performance. Il DataContext di LINQ to SQL include una proprietà chiamata Log, che permette di specificare un output sul quale vogliamo mostrare le query SQL eseguite. Su Windows Phone non è così semplice sfruttare questa feature, dato che non è disponibile una console e i file possono essere salvati solamente nell?Isolated Storage, rendendo complicate le operazioni di estrazione e visualizzazione. Nella versione più recente di SQL CE Toolbox, in fase di creazione del DataContext, viene creata anche una classe chiamata DebugWriter, che può essere utilizzata semplicemente impostando a true la proprietà LogDebug del DataContext. In questo modo, quando testerete la vostra applicazione con il debugger colelgato, ad ogni operazione sul database, la relativa query SQL verrà scritta nella Output Window di Visual Studio. Ecco un semplice esempio di utilizzo:

using (OrdersContext context = new OrdersContext(OrdersContext.ConnectionString))
{
  context.LogDebug = true;
  context.Orders.InsertOnSubmit(order);
  context.SubmitChanges();
}

Connessione read-only al database

Un'altra cosa che avrete imparato nel corso dell'articolo è che esistono due modi per utilizzare un database SQL CE in Windows Phone: code-first (il database viene generato al primo avvio nell'Isolated Storage) o incluso come file di progetto (con accesso in sola lettura). Il DataContext generato da SQL CE Toolbox includeva già una proprietà statica chiamata ConnectionString, con la stringa di connessione da utilizzare per la connessione al database nell'Isolated Storage. Per supportare anche gli scenari di accesso in sola lettura, ora il DataContext include anche una proprietà chiamata ConnectionStringReadOnly, da utilizzare nei casi in cui un database prepopolato sia stato incluso tra i file di progetto. Ecco un esempio di utilizzo:

//database aggiunto come file di progetto
using (OrdersContext context = new OrdersContext(OrdersContext.ConnectionStringReadOnly))
{
  //interazione con il database
}

Nuovo tool a riga di comando

Poco dopo il rilascio della release 2.6 di SQL CE Toolbox ErikEJ (l'autore) ha rilasciato un nuovo tool, dedicato agli utilizzatori di Visual Studio Express. La versione gratuita dell'ambiente di sviluppo Microsoft, infatti, non supporta le estensioni, perciò SQL CE Toolbox non è utilizzabile: questo nuovo tool è una semplice applicazione a riga di comando che, appoggiandosi a SQLMetal (un tool di Microsoft per la generazione di codice per il mapping di databse con LINQ to SQL), genera il DataContext necessario per usare il vostro database in un'applicazione Windows Phone. Il risultato è lo stesso che si ottiene usando l'interfaccia grafica spiegata in questa guida: trattandosi però di un tool esterno e non di un'estensione di Visual Studio potete tranquillamente utilizzarlo anche con la versione Express. L'utilizzo è molto semplice: una volta che vi siete assicurati di avere installato SQLMetal tra i vostri tool (nel post originale trovate tutti i dettagli) non dovete far altro che scaricare il tool e dopodichè lanciare il comando:

exportsqlce wpdc "Data Source=C:\projects\Chinook\Chinook.sdf" C:\temp\Chinook.cs

dove:

  • Il primo parametro (wpdc) serve per specificare che si vuole generare un DataContext per Windows Phone
  • Il secondo parametro è la stringa di connessione al database, tipicamente il percorso al file fisico SDF (che, vi ricordo, deve essere un database SQL CE 3.5 e non 4.0)
  • Il terzo parametro è il percorso dove volete creare la classe comprensiva di DataContext e relative entità

Ecco SQL Server CE

Windows Phone 7.1 ha ovviato a questa lacuna, introducendo il supporto a SQL Server CE: si tratta di un database relazionale memorizzato su singolo file e che gira in process, perciò perfetto per essere ospitato in un file system come quello dell'Isolated Storage.

Come vedremo nel corso di questo articolo, se siamo abituati all'utilizzo di ORM moderni, come Entity Framework o NHibernate, dovremo fare un "passo indietro": l'approccio adottato è infatti quello di LINQ to SQL, che è ottimo per un dispositivo mobile, dato che è più leggero e meno avido di risorse, ma che non ci permette di avere a disposizione tutti i benefici che un ORM più moderno e maturo comporta.

L'approccio supportato da Windows Phone 7.1 è chiamato Code-First, introdotto recentemente anche in Entity Framework: il mapping tra il database e i nostri oggetti viene fatto direttamente da codice. Al primo avvio, l'applicazione creerà il nostro database, in base ad una serie di convenzioni che possiamo utilizzare in fase di definizione delle nostre classi.

Quali sono al momento i limiti di questo approccio? Eccoli di seguito:

  • non esiste un designer visuale specifico per LINQ to SQL per Windows Phone: con Entity Framework (o anche con LINQ to SQL "tradizionale") abbiamo a disposizione, invece, un tool visuale che ci permette di "disegnare" le nostre classi (definendo proprietà e relazioni) e di lasciare a Visual Studio la generazione del codice. In realtà, come vedremo, ci sono delle soluzioni "non ufficiali" che ci permetteranno di ovviare a questo limite;
  • LINQ to SQL non supporta "out of the box" le relazioni many to many, ma dovremo gestirle "manualmente" con una terza tabella che funga da collegamento tra le prime due.

In questo articolo useremo un esempio molto classico: una semplice applicazione di e-commerce in cui mapperemo sul database dei clienti (la classe Customer) con i loro ordini (la classe Order). Si tratta di una relazione one-to-many, che può essere rappresentata come in figura:

Il mapping

Il mapping tra il database e le nostre classi avviene tramite due passaggi:

  1. la creazione delle entità e l'inserimento di alcuni attributi che serviranno a LINQ to SQL per capire come fare il mapping;
  2. la creazione di un DataContext, che ci permetterà di definire la struttura del nostro database e di effettuare le operazioni con lo stesso (inserimento, aggiornamento, selezione, ecc.).

In realtà, come vedremo più avanti, useremo un approccio che ci risparmierà di effettuare manualmente entrambi i passaggi. È importante, però, capire cosa succede dietro le quinte, perciò scendiamo un po' più in dettaglio.

Gli attributi

Come detto poco fa, il primo passaggio è quello di creare manualmente le classi e, dopodichè, decorarle con una serie di attributi:

  • l'attributo Table viene applicato alla classe e indica a LINQ to SQL che questa verrà mappata su una tabella. Di default, la tabella avrà lo stesso nome della nostra entità, ma grazie alla proprietà Name possiamo personalizzarlo;
  • l'attributo Column viene applicato alle singole proprietà che dovranno diventare colonne della tabella. Anche in questo caso, di default, la colonna avrà lo stesso nome della proprietà: oltre a poter personalizzare il nome, però, abbiamo tutta una serie di proprietà che possiamo specificare per personalizzare il mapping (se si tratta di una chiave primaria, se può accettare valori null, se il valore è auto generato, ecc.).

Ecco un esempio della classe Customer decorata con gli attributi:

[Table]
public class Customer
{
  [Column(AutoSync = AutoSync.OnInsert, 
          DbType = "Int NOT NULL IDENTITY",
          IsPrimaryKey = true,
          IsDbGenerated = true,
          CanBeNull = false)]
  public int CustomerId { get; set; }

  [Column]
  public string Name { get; set; }

  [Column]
  public string Surname { get; set; }

  [Column]
  public DateTime BirthDate { get; set; }
}

Nel caso della proprietà CustomerId, essendo la chiave primaria auto generata (si tratta di un numero intero auto incrementante), possiamo notare l'uso di diverse proprietà per l'attributo Column, quali IsPrimaryKey e IsDbGenerated.

Il DataContext

Il DataContext, come anticipato poco fa, rappresenta il "contesto" del nostro mapping e fa da interfaccia tra il database e le nostre entità: grazie al DataContext potremo agire sugli elementi memorizzati nella tabella ed eseguire le operazioni più comuni: selezione, inserimento, modifica e cancellazione.

Il DataContext è una classe ad hoc che eredità da DataContext, ha un costruttore che accetta una connection string e contiene la definizione delle tabelle coinvolte nel mapping. Ecco un esempio di DataContext per la nostra applicazione:

public class OrdersContext : DataContext
{
  public static string DBConnectionString = "Data Source=isostore:/Orders.sdf";

  public OrdersContext(string connectionString)
    : base(connectionString)
  {
  }

  public Table<Order> Orders;
  public Table<Customer> Customers;
}

Come possiamo notare, abbiamo semplicemente dichiarato che il nostro database è composto da due tabelle: una utilizzata per memorizzare gli oggetti di tipo Order e una gli oggetti di tipo Customer.

Gestire le relazioni

Come anticipato all'inizio dell'articolo, le relazioni non sono il punto forte di LINQ to SQL e questo si sente ancora più per la mancanza di un designer visuale: al contrario, ad esempio, di Entity Framework, non ci è sufficiente dichiarare una proprietà del tipo della classe che vogliamo collegare (nel nostro caso, una proprietà di tipo Customer all'interno della classe Order) per far generare in automatico la relazione, ma bisogna fare una serie di passaggi intermedi. Nel nostro esempio, abbiamo la necessità di esprimere una relazione di tipo one-to-many: un cliente può infatti fare un numero illimitato di ordini, ma un ordine è legato ad un solo cliente. Per ottenere questo risultato, si utilizzano dei tipi speciali offerti da LINQ to SQL.

La classe corrispondente alla parte "one" della relazione (nel nostro caso, Customer) contiene una proprietà di tipo EntitySet<T>, dove T è la parte "many" della relazione (nel nostro esempio, Order).

La classe corrispondente alla parte "many" della relazione (cioè Order) contiene una proprietà di tipo EntityRef<T>, dove T è la parte "one" della relazione (ovvero Customer).

Entrambe le proprietà saranno marcate con l'attributo Association. Grazie a queste due proprietà speciali, saremo in grado, tramite il DataContext, di:

  • accedere a tutti gli ordini fatti da un cliente (Customer);
  • Accedere ai dati del cliente che ha fatto un ordine (Order.Customer).

Ecco come appare la proprietà della classe Order di tipo Customer:

private EntityRef<Customer> _Customer;
[Association(Name="CustomerOrders", 
             Storage="_Customer",
             ThisKey="CustomerId",
             OtherKey="CustomerId",
             IsForeignKey=true)]
public Customer Customer
{
  get
  {
    return this._Customer.Entity;
  }
  set
  {
    Customer previousValue = this._Customer.Entity;
    if (((previousValue != value)  || (this._Customer.HasLoadedOrAssignedValue == false)))
    {
      if ((previousValue != null))
      {
        this._Customer.Entity = null;
        previousValue.Orders.Remove(this);
      }
      this._Customer.Entity = value;
      if ((value != null))
      {
        value.Orders.Add(this);
        this._CustomerId = value.CustomerId;
      }
      else
      {
        this._CustomerId = default(Nullable<int>);
      }
  
    }
  }
}

La proprietà pubblica (di tipo Customer) si appoggia ad una proprietà privata di tipo EntityRef<Customer>, la quale espone la proprietà Entity, che contiene il valore vero e proprio memorizzato nel database. I metodi get e set della proprietà di tipo Customer non fanno altro che leggere e scrivere questo valore, anche se, in realtà, la definizione del metodo set è più articolata perchè vengono fatti tutta una serie di controlli per capire se si tratta di un nuovo inserimento o di un aggiornamento di un dato già memorizzato.

Ecco invece come appare questa relazione nella classe Customer:

private EntitySet<Order> _Orders;

[Association(Name="CustomerOrders",
             Storage="_Orders",
             ThisKey="CustomerId",
             OtherKey="CustomerId", 
             DeleteRule="NO ACTION")]
public EntitySet<Order> Orders
{
  get
  {
    return this._Orders;
  }
  set
  {
    this._Orders.Assign(value);
  }
}

A questo punto la relazione è stata correttamente specifica e possiamo passare oltre.

5 pagine in totale: 1 2 3 4 5
Contenuti dell'articolo

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

Top Ten Articoli

Articoli via e-mail

Iscriviti alla nostra newsletter nuoviarticoli per ricevere via e-mail le notifiche!

In primo piano

I più letti di oggi

In evidenza

Misc