Облачные вычисления всё прочнее занимают свои позиции. Всё большее количество людей осознаёт этот факт и уже сейчас предпринимает шаги для переноса своих приложений в «облако». Так случилось и с нашим проектом, заказчик сказал, что хочет «витать в облаках». Т.к. приложение разрабатывалось на Silverlight, то никаких сомнений в выборе Cloud-а не возникло и решено было использовать Windows Azure.
В числе прочих задач при переходе в «облако» возникла задача структурированного хранения шаблонов приложения. Windows Azure предоставляет несколько возможностей хранения информации в рамках Windows Azure Storage Services:
На первый взгляд Table подходит как нельзя лучше, но это только на первый взгляд. Мы можем выполнять примитивные запросы и получать только те строки, которые нас интересуют, мы можем организовать иерархию объектов, размещая в свойствах записи Guid другой записи. По мере углубления знаний в этой области выясняется, что каждая запись не может содержать более 255 полей (что для нас не критично), не может быть размером более 1 мБ (что на текущий момент нас устраивает, но в процессе эволюции системы может выйти боком) и, наконец, размер каждого свойства не может превышать 64 кБ, а вот с этим мириться мы уже не можем, так как на данном этапе уже существует необходимость хранить текст размером до 300 кБ.
А что же с Blob-ами? Blob-ы позволяют хранить записи со смешным для текущей задачи ограничением по размеру в 200 ГБ, позволяет структурировать данные, размещая их в разных контейнерах и используя виртуальные папки, так же каждый Blob может сопровождаться метаданными общим размером 8 кБ. Казалось бы, однозначно Blob-ы? И снова нет! Мы не можем делать выборку Blob-ов, нет (во всяком случае, мы его не нашли) механизма осуществить поиск в соответствии с каким-либо запросом. Вы спросите: «А как же метаданные? Там ведь можно разместить информацию о структуре!» И будете почти правы, можно, можно получить метаданные без содержания Blob-а и даже LINQ-запросом сделать выборку, но это придётся делать уже получив все метаданные всех Blob-ов в контейнере. А при большом количестве элементов это будет затратная операция.
Так что, при помощи Windows Azure нельзя решить нашу задачу? Конечно, можно! И мы нашли способ, может он не идеален, но нас полностью устраивает. Было принято решение, хранить сами данные в Blob-ах, а информацию о структуре данных в таблицах. Таким образом мы компенсируем недостатки и используем преимущества за счёт использования этих двух технологий в комплексе.
Ну и напоследок немного кода, который проиллюстрирует всё выше написанное. Для начала сделаем класс-обёртку, который позволит более просто обращаться к Table Storage.
Далее создаём сущности записей:
В нашей задаче необходимо хранить для каждого пользователя объекты по категориям, поэтому решено было объекты и категории объектов хранить в одной таблице (а такую возможность предоставляет нам Azure Table Storage). Для того, что бы отличать записи в таблице, у объектов есть поле, которе хранит информацию о типе объекта. Для удобства работы это сделано в виде enum ElementType. Базовый класс для записей BaseObjTableEntity и классы для категорий и объектов соответсвенно ObjCategoryTableEntity и ObjTableEntity.
Абстрактный класс TableServiceEntity содержит 3 свойства: PartitionKey, RowKey и Timestamp. Первые два используются как составной ключ, записи с одинаковым PartitionKey будут всегда расположены в пределах одного сервера, поэтому решено было в это свойство передавать ID пользователя. RowKey в категории — это Guid категории, а RowKey объекта — Guid, который является именем Blob-а. Соответственно сами объекты хранятся в Blob-ах, для работы с ними создадим простенький класс:
Ну и наконец, класс, для работы со всем этим:
Работа с категориями весьма тривиальна, отдельного внимания заслуживает лишь метод добавления объекта AddObj, где помимо добавления записи в таблицу, добавляется ещё и Blob с контентом объекта, а получить контент можно по его ID.
Таким образом мы используем преимущества и таблиц (возможность делать выборку записей на стороне облака), и Blob-ов (хранение практически неограниченного по объёму элемента), при этом обходя их недостатки.
Вот в принципе и всё, чем хотелось поделиться, возможно, кому-то это покажется очевидным и он уже сто раз так делал, но для кого-то это будет хорошим и интересным материалом и ещё одним пунктиком в пользу перехода в «облако».
В числе прочих задач при переходе в «облако» возникла задача структурированного хранения шаблонов приложения. Windows Azure предоставляет несколько возможностей хранения информации в рамках Windows Azure Storage Services:
- Windows Azure Blob – обеспечивает хранилище больших элементов данных.
- Windows Azure Table – обеспечивает структурированное хранилище состояний сервиса.
- Windows Azure Queue – обеспечивает диспетчеризацию асинхронных заданий для реализации обмена данными между сервисами.
На первый взгляд Table подходит как нельзя лучше, но это только на первый взгляд. Мы можем выполнять примитивные запросы и получать только те строки, которые нас интересуют, мы можем организовать иерархию объектов, размещая в свойствах записи Guid другой записи. По мере углубления знаний в этой области выясняется, что каждая запись не может содержать более 255 полей (что для нас не критично), не может быть размером более 1 мБ (что на текущий момент нас устраивает, но в процессе эволюции системы может выйти боком) и, наконец, размер каждого свойства не может превышать 64 кБ, а вот с этим мириться мы уже не можем, так как на данном этапе уже существует необходимость хранить текст размером до 300 кБ.
А что же с Blob-ами? Blob-ы позволяют хранить записи со смешным для текущей задачи ограничением по размеру в 200 ГБ, позволяет структурировать данные, размещая их в разных контейнерах и используя виртуальные папки, так же каждый Blob может сопровождаться метаданными общим размером 8 кБ. Казалось бы, однозначно Blob-ы? И снова нет! Мы не можем делать выборку Blob-ов, нет (во всяком случае, мы его не нашли) механизма осуществить поиск в соответствии с каким-либо запросом. Вы спросите: «А как же метаданные? Там ведь можно разместить информацию о структуре!» И будете почти правы, можно, можно получить метаданные без содержания Blob-а и даже LINQ-запросом сделать выборку, но это придётся делать уже получив все метаданные всех Blob-ов в контейнере. А при большом количестве элементов это будет затратная операция.
Так что, при помощи Windows Azure нельзя решить нашу задачу? Конечно, можно! И мы нашли способ, может он не идеален, но нас полностью устраивает. Было принято решение, хранить сами данные в Blob-ах, а информацию о структуре данных в таблицах. Таким образом мы компенсируем недостатки и используем преимущества за счёт использования этих двух технологий в комплексе.
Ну и напоследок немного кода, который проиллюстрирует всё выше написанное. Для начала сделаем класс-обёртку, который позволит более просто обращаться к Table Storage.
- public class DynamicTableServiceContext<T> : TableServiceContext where T : TableServiceEntity
- {
- private CloudStorageAccount storageAccount;
- private string tableName;
-
- public DynamicTableServiceContext(CloudStorageAccount storageAccount, string tableName)
- : base(storageAccount.TableEndpoint.AbsoluteUri, storageAccount.Credentials)
- {
- this.storageAccount = storageAccount;
- this.tableName = tableName;
- var tableStorage = new CloudTableClient(storageAccount.TableEndpoint.AbsoluteUri, storageAccount.Credentials);
- tableStorage.CreateTableIfNotExist(tableName);
- }
-
- public void Add(T entityToAdd)
- {
- AddObject(tableName, entityToAdd);
- SaveChanges();
- }
-
- public void Update(T entityToUpdate)
- {
- UpdateObject(entityToUpdate);
- SaveChanges();
- }
-
- public void Delete(T entityToDelete)
- {
- DeleteObject(entityToDelete);
- SaveChanges();
- }
-
- public IQueryable<T> Load()
- {
- return CreateQuery<T>(tableName);
- }
- }
* This source code was highlighted with Source Code Highlighter.
Далее создаём сущности записей:
- public enum ElementType
- {
- Obj = 1,
- ObjCategory = 2
- }
-
- public class BaseObjTableEntity : TableServiceEntity
- {
- public virtual int Type { get; set; }
- public string Name { get; set; }
- }
-
- public class ObjTableEntity : BaseObjTableEntity
- {
- public string Description { get; set; }
- public string CategoryId { get; set; }
-
- public override int Type
- {
- get { return (int)ElementType.Obj; }
- set { }
- }
- }
-
- public class ObjCategoryTableEntity : BaseObjTableEntity
- {
- public override int Type
- {
- get { return (int)ElementType.ObjCategory; }
- set { }
- }
- }
* 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-ах, для работы с ними создадим простенький класс:
- public class ObjBlob
- {
- public string Content { get; set; }
- public string Name { get; set; }
- }
* This source code was highlighted with Source Code Highlighter.
Ну и наконец, класс, для работы со всем этим:
- public class ObjAzureRepository
- {
- private const string ObjContainerName = «objlibrary»;
- private const string ObjTableName = «ObjLibrary»;
- private const string DefaultPartitionKey = «Default»;
- private readonly string userID;
-
- public ObjAzureRepository(string userId)
- {
- this.userID = userId;
- }
-
- private string azureStorageConnectionString;
- protected string AzureStorageConnectionString
- {
- get
- {
- if (string.IsNullOrWhiteSpace(azureStorageConnectionString))
- {
- azureStorageConnectionString = WebConfigurationManager.ConnectionStrings[«AzureStorageConnectionString»].ConnectionString;
- }
- return azureStorageConnectionString;
- }
- }
-
- private CloudStorageAccount azureStorageAccount;
- protected CloudStorageAccount AzureStorageAccount
- {
- get
- {
- return azureStorageAccount ?? (azureStorageAccount = CloudStorageAccount.Parse(AzureStorageConnectionString));
- }
- }
-
- private DynamicTableServiceContext<ObjCategoryTableEntity> categoryContext;
- private DynamicTableServiceContext<ObjCategoryTableEntity> CategoryContext
- {
- get
- {
- if (categoryContext == null)
- {
- categoryContext = new DynamicTableServiceContext<ObjCategoryTableEntity>(AzureStorageAccount, ObjTableName);
- }
- return categoryContext;
- }
- }
-
- private DynamicTableServiceContext<ObjTableEntity> ObjContext;
- private DynamicTableServiceContext<ObjTableEntity> ObjContext
- {
- get
- {
- if (ObjContext == null)
- {
- ObjContext = new DynamicTableServiceContext<ObjTableEntity>(AzureStorageAccount, ObjTableName);
- }
- return ObjContext;
- }
- }
-
- public List<ObjCategoryTableEntity> GetObjCategories()
- {
- return CategoryContext.Load().Where<ObjCategoryTableEntity>(e => e.PartitionKey == userID && e.Type == (int)ElementType.ObjCategory).ToList();
- }
-
- public List<ObjTableEntity> GetObjsByCategory(ObjCategoryTableEntity category)
- {
- return ObjContext.Load().Where(s => s.PartitionKey == userID && s.CategoryId == category.RowKey && s.Type == (int)ElementType.Obj).ToList();
- }
-
- public ObjBlob GetObjById(string id)
- {
- CloudBlobClient client = AzureStorageAccount.CreateCloudBlobClient();
- CloudBlobContainer container = client.GetContainerReference(ObjContainerName);
- container.CreateIfNotExist();
- CloudBlob Obj = container.GetBlobReference(id);
- return new ObjBlob() { Name = id, Content = Obj.DownloadText() };
- }
-
- public void AddCategory(ObjCategoryTableEntity category)
- {
- CategoryContext.Add(category);
- }
-
- public void ChangeCategory(ObjCategoryTableEntity category)
- {
- CategoryContext.Update(category);
- }
-
- public void RemoveCategory(ObjCategoryTableEntity category)
- {
- CategoryContext.Delete(category);
- }
-
- public void AddObj(ObjTableEntity Obj, string ObjContent)
- {
- ObjContext.Add(Obj);
- CloudBlobClient client = AzureStorageAccount.CreateCloudBlobClient();
- CloudBlobContainer container = client.GetContainerReference(ObjContainerName);
- container.CreateIfNotExist();
- CloudBlob ObjBlob = container.GetBlobReference(Obj.RowKey);
- ObjBlob.UploadText(ObjContent);
- }
-
- public void ChangeObj(ObjTableEntity Obj)
- {
- ObjContext.Update(Obj);
- }
-
- public void RemoveObj(ObjTableEntity Obj)
- {
- ObjContext.Delete(Obj);
- }
- }
* This source code was highlighted with Source Code Highlighter.
Работа с категориями весьма тривиальна, отдельного внимания заслуживает лишь метод добавления объекта AddObj, где помимо добавления записи в таблицу, добавляется ещё и Blob с контентом объекта, а получить контент можно по его ID.
Таким образом мы используем преимущества и таблиц (возможность делать выборку записей на стороне облака), и Blob-ов (хранение практически неограниченного по объёму элемента), при этом обходя их недостатки.
Вот в принципе и всё, чем хотелось поделиться, возможно, кому-то это покажется очевидным и он уже сто раз так делал, но для кого-то это будет хорошим и интересным материалом и ещё одним пунктиком в пользу перехода в «облако».