Windows Azure datalagring - Tables

I min förra artikel skrev jag om hur du kan lagra filer i Windows Azure Blob Storage med hjälp av WCF REST Starter Kit. I Azure Blobs lagrar du som bekant ostrukturerad information som t ex dokument och bilder. Windows Azure Tables är byggt för att erbjuda dig en plats att lagra strukturerad information i en tabelliknande struktur med väldigt hög skalbarhet och pålitlighet.

Jag skriver tabelliknande av anledningen att det inte är tabeller i den bemärkelsen som du säkerligen är van vid. En traditionell tabell har sina kolumner definierade i ett schema. Tabeller i Windows Azure Tables har inget schema. Schemat definieras istället av de Entiteter (Entities) som du väljer att lägga i tabellen.  Entiteten består i sin tur av ett antal Egenskaper (Properties).

Ett enklare sett att se det på är att tänka på en Entitet som en rad i tabellen och Egenskaper (Properties) som dess kolumner.

clip_image001

Psst! En tabell kan innehålla flera typer av Entiteter. Tabeller är också entiteter.

Precis som övriga lagringserbjudanden (Blobs, Queues) i Windows Azure är det via REST som du kommunicerar med Table Storage. Det innebär att du även med Tables kan använda WCF REST Starter Kit för dataåtkomst.

Jag tänktei denna artikel istället visa hur du med ADO.NET Data Services och ett par hjälpklasser från Windows Azure SDK kan konsumera Windows Azure Tables på ett väldigt attraktivt sätt. Med dessa verktyg får du bland annat hjälp med att skapa nödvändiga tabeller samt möjligheten att ställa frågor med LINQ. En annan stor fördel är du slipper jobba med REST gränssnittet, det gör ADO.NET Data Services åt dig vilket är till stor hjälp när frågor skall konstrueras.

Tillvägagångssättet ser sammanfattningsvis ut på följande sätt:

  1. Skapa dina Entiteter som vanliga klasser.
  2. Skapa ett sk DataServiceContext som exponerar de olika kollektionerna av Entiteter som du vill arbeta med.
  3. Använd hjälpmetoden CreateTablesFromModel från Windows Azure SDK för att skapa tabellerna utifrån ditt Modell (DataServiceContext).
  4. Tuta och kör!

Jag tar stegen igen lite utförligare.

Först ut vill jag skapa min datamodell. Detta gör jag genom att först definiera modellens entiteter. I detta fall handlar det endast om en typ av entitet; Motorbike. En Windows Azure Tables entitet måste exponera två egenskaper som används som sk Partition Key  och Row Key. Dessa två egenskaper bildar tillsammans entitetens unika primärnyckel (Primary Key).

Partition Key anger hur entiteterna skall grupperas i datacentret. Entiter med samma Partition Key kommer alltid att lagras på samma nod i datacentret.  Att identifiera partitioner (Partition Keys) är ett viktigt steg i ditt designarbete för bla  prestandans skull då det kan vara väldigt kostsamt att ställa frågor över flera partitioner.

Jag ärver min Motorbike entitet från TableStorageEntity som kommer med exempelprojektet StorageClient   i Windows Azure SDK. Jag får då PartitionKey och RowKey egenskaperna “på köpet” och behöver inte definiera dem själv genom attribut. Värdena för dessa sätter jag i konstruktorn.

 public class Motorbike : TableStorageEntity {

    public Motorbike() {
        this.PartitionKey = "bikes";
        this.RowKey = string.Format("{0}", 
            DateTime.MaxValue.Ticks - DateTime.Now.Ticks);
    }

    public string Name {
        get;
        set;
    }

    public string Brand {
        get;
        set;
    }

    public double Price {
        get;
        set;
    }

}

Nästa steg är att implementera mitt DataServiceContext. För att slippa hanteringen av auktoriseringsinformationen i HTTP förfrågan ärver jag min DataServiceContext klass från TableStorageDataServiceContext istället för att ärva direkt från ADO.NET Data Services basklass. Jag behöver då bara ange mitt kontonamn och kontots delade nyckel utan signeringskrångel.

Min MotorbikeDataServiceContext klass behöver nu bara exponera alla mina typer av entiteter i form av en IQueryable kollektion per entitetstyp. Kollektionen Motorbikes läser och skriver Motorbike entiteter till tabellen Motorbikes.

 public class MotorbikeDataServiceContext : TableStorageDataServiceContext {

    public MotorbikeDataServiceContext()
        : this(
            new StorageAccountInfo(
            new Uri("https://127.0.0.1:10002"), null,
            "devstoreaccount1",
            "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==")
            ) {
    }

    public MotorbikeDataServiceContext(StorageAccountInfo accountInfo) 
        : base(accountInfo) { }

    public IQueryable<Motorbike> Motorbikes {
        get {
            return this.CreateQuery<Motorbike>("Motorbikes");
        }
    }
}

Så, datamodellen är på plats. Nu behöver jag skapa tabellen Motorbikes. Genom att använda metoden CreateTablesFromModel (även den från StorageClient projektet) behöver jag inte manuellt skapa mina tabeller. Parametern till metoden är typen av min DataServiceContext klass. CreateTablesFromModel identifierar vilka entiteter som exponeras och skapar behövda tabeller.

 TableStorage.CreateTablesFromModel(typeof(MotorbikeDataServiceContext));

Tänk på! För Development Storage måste du fortfarande köra DevTableGen.exe för att skapa en databas.

Tuta och kör! skrev jag sedan i punktlistan ovan.. Ser nedan bekant ut? Jag tror det.

Hämta Motorcykel med Id…

 public ActionResult Details(string id) {

    MotorbikeDataServiceContext context = new MotorbikeDataServiceContext();
    var motorbike = (from m in context.Motorbikes
                    where m.PartitionKey == "bikes" && m.RowKey == id
                    select m).FirstOrDefault();

    return View(motorbike);
}
 Skapa Motorcykel…
 [AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection collection) {
      
    MotorbikeDataServiceContext context = new MotorbikeDataServiceContext();

    context.AddObject("Motorbikes",
        new Motorbike() {
            Name = collection["Name"],
            Brand = collection["Brand"],
            Price = double.Parse(collection["Price"])
        });
    context.SaveChanges();

    return RedirectToAction("Index");

}

-c