Pull to refresh

Windows Azure: Blob и Table дополняют друг друга

Reading time10 min
Views2.1K
Облачные вычисления всё прочнее занимают свои позиции. Всё большее количество людей осознаёт этот факт и уже сейчас предпринимает шаги для переноса своих приложений в «облако». Так случилось и с нашим проектом, заказчик сказал, что хочет «витать в облаках». Т.к. приложение разрабатывалось на Silverlight, то никаких сомнений в выборе Cloud-а не возникло и решено было использовать Windows Azure.

В числе прочих задач при переходе в «облако» возникла задача структурированного хранения шаблонов приложения. Windows Azure предоставляет несколько возможностей хранения информации в рамках Windows Azure Storage Services:
  • Windows Azure Blob – обеспечивает хранилище больших элементов данных.
  • Windows Azure Table – обеспечивает структурированное хранилище состояний сервиса.
  • Windows Azure Queue – обеспечивает диспетчеризацию асинхронных заданий для реализации обмена данными между сервисами.
Queue явно не подходит для текущей задачи, значит остаётся выбрать между Blob и Table. Разбираемся дальше.

На первый взгляд Table подходит как нельзя лучше, но это только на первый взгляд. Мы можем выполнять примитивные запросы и получать только те строки, которые нас интересуют, мы можем организовать иерархию объектов, размещая в свойствах записи Guid другой записи. По мере углубления знаний в этой области выясняется, что каждая запись не может содержать более 255 полей (что для нас не критично), не может быть размером более 1 мБ (что на текущий момент нас устраивает, но в процессе эволюции системы может выйти боком) и, наконец, размер каждого свойства не может превышать 64 кБ, а вот с этим мириться мы уже не можем, так как на данном этапе уже существует необходимость хранить текст размером до 300 кБ.

А что же с Blob-ами? Blob-ы позволяют хранить записи со смешным для текущей задачи ограничением по размеру в 200 ГБ, позволяет структурировать данные, размещая их в разных контейнерах и используя виртуальные папки, так же каждый Blob может сопровождаться метаданными общим размером 8 кБ. Казалось бы, однозначно Blob-ы? И снова нет! Мы не можем делать выборку Blob-ов, нет (во всяком случае, мы его не нашли) механизма осуществить поиск в соответствии с каким-либо запросом. Вы спросите: «А как же метаданные? Там ведь можно разместить информацию о структуре!» И будете почти правы, можно, можно получить метаданные без содержания Blob-а и даже LINQ-запросом сделать выборку, но это придётся делать уже получив все метаданные всех Blob-ов в контейнере. А при большом количестве элементов это будет затратная операция.

Так что, при помощи Windows Azure нельзя решить нашу задачу? Конечно, можно! И мы нашли способ, может он не идеален, но нас полностью устраивает. Было принято решение, хранить сами данные в Blob-ах, а информацию о структуре данных в таблицах. Таким образом мы компенсируем недостатки и используем преимущества за счёт использования этих двух технологий в комплексе.

Ну и напоследок немного кода, который проиллюстрирует всё выше написанное. Для начала сделаем класс-обёртку, который позволит более просто обращаться к Table Storage.
  1. public class DynamicTableServiceContext<T> : TableServiceContext where T : TableServiceEntity
  2. {
  3.     private CloudStorageAccount storageAccount;
  4.     private string tableName;
  5.  
  6.     public DynamicTableServiceContext(CloudStorageAccount storageAccount, string tableName)
  7.         : base(storageAccount.TableEndpoint.AbsoluteUri, storageAccount.Credentials)
  8.     {
  9.         this.storageAccount = storageAccount;
  10.         this.tableName = tableName;
  11.         var tableStorage = new CloudTableClient(storageAccount.TableEndpoint.AbsoluteUri, storageAccount.Credentials);
  12.         tableStorage.CreateTableIfNotExist(tableName);
  13.     }
  14.  
  15.     public void Add(T entityToAdd)
  16.     {
  17.         AddObject(tableName, entityToAdd);
  18.         SaveChanges();
  19.     }
  20.  
  21.     public void Update(T entityToUpdate)
  22.     {
  23.         UpdateObject(entityToUpdate);
  24.         SaveChanges();
  25.     }
  26.  
  27.     public void Delete(T entityToDelete)
  28.     {
  29.         DeleteObject(entityToDelete);
  30.         SaveChanges();
  31.     }
  32.  
  33.     public IQueryable<T> Load()
  34.     {
  35.         return CreateQuery<T>(tableName);
  36.     }
  37. }
* This source code was highlighted with Source Code Highlighter.

Далее создаём сущности записей:
  1. public enum ElementType
  2. {
  3.     Obj = 1,
  4.     ObjCategory = 2
  5. }
  6.  
  7. public class BaseObjTableEntity : TableServiceEntity
  8. {
  9.     public virtual int Type { get; set; }
  10.     public string Name { get; set; }
  11. }
  12.  
  13. public class ObjTableEntity : BaseObjTableEntity
  14. {
  15.     public string Description { get; set; }
  16.     public string CategoryId { get; set; }
  17.  
  18.     public override int Type
  19.     {
  20.         get { return (int)ElementType.Obj; }
  21.         set { }
  22.     }
  23. }
  24.  
  25. public class ObjCategoryTableEntity : BaseObjTableEntity
  26. {
  27.     public override int Type
  28.     {
  29.         get { return (int)ElementType.ObjCategory; }
  30.         set { }
  31.     }
  32. }
* This source code was highlighted with Source Code Highlighter.

В нашей задаче необходимо хранить для каждого пользователя объекты по категориям, поэтому решено было объекты и категории объектов хранить в одной таблице (а такую возможность предоставляет нам Azure Table Storage). Для того, что бы отличать записи в таблице, у объектов есть поле, которе хранит информацию о типе объекта. Для удобства работы это сделано в виде enum ElementType. Базовый класс для записей BaseObjTableEntity и классы для категорий и объектов соответсвенно ObjCategoryTableEntity и ObjTableEntity.

Абстрактный класс TableServiceEntity содержит 3 свойства: PartitionKey, RowKey и Timestamp. Первые два используются как составной ключ, записи с одинаковым PartitionKey будут всегда расположены в пределах одного сервера, поэтому решено было в это свойство передавать ID пользователя. RowKey в категории — это Guid категории, а RowKey объекта — Guid, который является именем Blob-а. Соответственно сами объекты хранятся в Blob-ах, для работы с ними создадим простенький класс:
  1. public class ObjBlob
  2. {
  3.     public string Content { get; set; }
  4.     public string Name { get; set; }
  5. }
* This source code was highlighted with Source Code Highlighter.

Ну и наконец, класс, для работы со всем этим:
  1. public class ObjAzureRepository
  2. {
  3.     private const string ObjContainerName = «objlibrary»;
  4.     private const string ObjTableName = «ObjLibrary»;
  5.     private const string DefaultPartitionKey = «Default»;
  6.     private readonly string userID;
  7.  
  8.     public ObjAzureRepository(string userId)
  9.     {
  10.         this.userID = userId;
  11.     }
  12.     
  13.     private string azureStorageConnectionString;
  14.     protected string AzureStorageConnectionString
  15.     {
  16.         get
  17.         {
  18.             if (string.IsNullOrWhiteSpace(azureStorageConnectionString))
  19.             {
  20.                 azureStorageConnectionString = WebConfigurationManager.ConnectionStrings[«AzureStorageConnectionString»].ConnectionString;
  21.             }
  22.             return azureStorageConnectionString;
  23.         }
  24.     }
  25.  
  26.     private CloudStorageAccount azureStorageAccount;
  27.     protected CloudStorageAccount AzureStorageAccount
  28.     {
  29.         get
  30.         {
  31.             return azureStorageAccount ?? (azureStorageAccount = CloudStorageAccount.Parse(AzureStorageConnectionString));
  32.         }
  33.     }
  34.     
  35.     private DynamicTableServiceContext<ObjCategoryTableEntity> categoryContext;
  36.     private DynamicTableServiceContext<ObjCategoryTableEntity> CategoryContext
  37.     {
  38.         get
  39.         {
  40.             if (categoryContext == null)
  41.             {
  42.                 categoryContext = new DynamicTableServiceContext<ObjCategoryTableEntity>(AzureStorageAccount, ObjTableName);
  43.             }
  44.             return categoryContext;
  45.         }
  46.     }
  47.     
  48.     private DynamicTableServiceContext<ObjTableEntity> ObjContext;
  49.     private DynamicTableServiceContext<ObjTableEntity> ObjContext
  50.     {
  51.         get
  52.         {
  53.             if (ObjContext == null)
  54.             {
  55.             ObjContext = new DynamicTableServiceContext<ObjTableEntity>(AzureStorageAccount, ObjTableName);
  56.             }
  57.             return ObjContext;
  58.         }
  59.     }
  60.     
  61.     public List<ObjCategoryTableEntity> GetObjCategories()
  62.     {
  63.         return CategoryContext.Load().Where<ObjCategoryTableEntity>(e => e.PartitionKey == userID && e.Type == (int)ElementType.ObjCategory).ToList();
  64.     }
  65.     
  66.     public List<ObjTableEntity> GetObjsByCategory(ObjCategoryTableEntity category)
  67.     {
  68.         return ObjContext.Load().Where(s => s.PartitionKey == userID && s.CategoryId == category.RowKey && s.Type == (int)ElementType.Obj).ToList();
  69.     }
  70.     
  71.     public ObjBlob GetObjById(string id)
  72.     {
  73.         CloudBlobClient client = AzureStorageAccount.CreateCloudBlobClient();
  74.         CloudBlobContainer container = client.GetContainerReference(ObjContainerName);
  75.         container.CreateIfNotExist();
  76.         CloudBlob Obj = container.GetBlobReference(id);
  77.         return new ObjBlob() { Name = id, Content = Obj.DownloadText() };
  78.     }
  79.  
  80.     public void AddCategory(ObjCategoryTableEntity category)
  81.     {
  82.         CategoryContext.Add(category);
  83.     }
  84.  
  85.     public void ChangeCategory(ObjCategoryTableEntity category)
  86.     {
  87.         CategoryContext.Update(category);
  88.     }
  89.     
  90.     public void RemoveCategory(ObjCategoryTableEntity category)
  91.     {
  92.         CategoryContext.Delete(category);
  93.     }
  94.     
  95.     public void AddObj(ObjTableEntity Obj, string ObjContent)
  96.     {
  97.         ObjContext.Add(Obj);
  98.         CloudBlobClient client = AzureStorageAccount.CreateCloudBlobClient();
  99.         CloudBlobContainer container = client.GetContainerReference(ObjContainerName);
  100.         container.CreateIfNotExist();
  101.         CloudBlob ObjBlob = container.GetBlobReference(Obj.RowKey);
  102.         ObjBlob.UploadText(ObjContent);
  103.     }
  104.  
  105.     public void ChangeObj(ObjTableEntity Obj)
  106.     {
  107.         ObjContext.Update(Obj);
  108.     }
  109.     
  110.     public void RemoveObj(ObjTableEntity Obj)
  111.     {
  112.         ObjContext.Delete(Obj);
  113.     }
  114. }
* This source code was highlighted with Source Code Highlighter.

Работа с категориями весьма тривиальна, отдельного внимания заслуживает лишь метод добавления объекта AddObj, где помимо добавления записи в таблицу, добавляется ещё и Blob с контентом объекта, а получить контент можно по его ID.

Таким образом мы используем преимущества и таблиц (возможность делать выборку записей на стороне облака), и Blob-ов (хранение практически неограниченного по объёму элемента), при этом обходя их недостатки.

Вот в принципе и всё, чем хотелось поделиться, возможно, кому-то это покажется очевидным и он уже сто раз так делал, но для кого-то это будет хорошим и интересным материалом и ещё одним пунктиком в пользу перехода в «облако».
Tags:
Hubs:
Total votes 20: ↑12 and ↓8+4
Comments10

Articles