Интро
Уже давно хотелось пощупать главное нововведение .NET 4 – DLR, да и RavenDB выглядит очень аппетитным, а собранные вместе в одном месте дают возможность реализовать очень интересные штуки. Да и на чём потренироваться у меня есть – мой список дисков, который я веду в Excel. Давно уже хочется отказаться от таблиц и использовать что-то более гибкое.
Итак, далее последует примерный конспект моих мытарств в освоении всего нового, а именно – как работать с приложениями MS Office напрямую, основы использования динамических объектов и работа с RavenDB. На всякий случай, я использовал MS Office 2010 RTM x64, Visual Studio 2010 RTM и RavenDB build 101 (начал я с 100, но там был баг с загрузкой индексов).
Как оседлать Excel
Итак, как же нам получить все интересующие на данные из исходного файлика в формате xlsx? Побродив немного по MSDN и форумам, я заметил два основных подхода: работа как с БД через Jet OLE DB (пример на CodeProject), либо напрямую через компоненты автоматизации (COM-объекты).
Поскольку у меня много мета-информации хранится в комментариях к ячейкам, первый вариант отпадает сразу. Второй вариант тоже не сахар, ибо работа с COM – не самое благодарное занятие.
Компонентная модель
Итак, работаем с COM. Очень поверхностно, работа заключается в создании объекта нужного приложения (в моём случае – Excel), открытия интересующего нас файла и последующего взаимодействия с этим самым файлом. Самое главное – не забыть потом всё это закрыть (и нет, using здесь не помощник).
using Microsoft.Office.Interop.Excel;
<...>
private static void Main()
{
var excel = new Application();
try
{
Workbook workbook = excel.Workbooks.Open("CDDB.xlsx", ReadOnly: true, Editable: false);
Worksheet sheet = workbook.Sheets["Игры"];
foreach (Range row in sheet.Rows)
{
var objetToStore = GameConverter(row.Cells);
<...>
}
workbook.Close();
}
finally
{
excel.Quit();
}
}
Для сравнения, вот как выглядело открытие файла раньше:
Workbook workbook = excel.Workbooks.Open("CDDB.xlsx", null, true, null, null, null, null, null, null, false, null, null, null, null, null);
Достаём данные
Собственно, обработка данных. В данном примере функция GameConverter принимает одну строку-запись из файла и конвертирует её в некий объект, который будет более наглядно отражать её суть. И тут возникает вопрос, – как это сделать. Лично мне в данном случае не хотелось привязываться к какой-то конкретной объектной модели, поэтому первое, что пришло в голову – использовать анонимные объекты.
private static object GameConverter(dynamic gameRow)
{
return new
{
type = "game",
title = new
{
localized = GetField(gameRow, 1),
original = GetField(gameRow, 2),
},
publisher = GetField(gameRow, 3),
disks = GetField(gameRow, 4),
architecture = GetField(gameRow, 5),
media = GetField(gameRow, 6),
protection = GetField(gameRow, 7),
language = GetField(gameRow, 8),
shippedVersion = GetField(gameRow, 9),
latestPatch = GetField(gameRow, 10),
borrowedBy = GetField(gameRow, 11),
purchaseDate = GetField(gameRow, 12),
price = GetField(gameRow, 13),
};
}
private static object GetField(dynamic row, int idx)
{
return new { value = row[idx].Value2, comment = GetComment(row[idx].Comment) };
}
private static string GetComment(dynamic cell)
{
return cell == null ? null : cell.Shape.AlternativeText;
}
Хм… выглядит неплохо, но смущает явный копипаст. Ну да ладно, пока что сойдёт. Самое главное – уже сейчас видны преимущества использования динамических объектов, ибо я так и не разобрался, как же просто можно обратиться к полям с данными (что к чему надо кастовать на каком шаге), даже зная, как называются интересующие меня поля.
После прогона с отладчиком, видно, что объект формируется как надо:

Работа с RavenDB
Сначала, почему RavenDB? Потому что это объектное хранилище, да к тому же написанное на .NET для .NET. Идеальный вариант. Работать с ним проще простого: запускаем сервер, инициализируем клиент, подключаемся, работаем.
using Raven.Client;
using Raven.Client.Document;
using Raven.Database.Data;
<...>
private static void Main()
{
<...>
using (IDocumentStore store = new DocumentStore {Url = "http://localhost:8080/", Credentials = CredentialCache.DefaultNetworkCredentials}.Initialize())
using (IDocumentSession session = store.OpenSession())
{
Worksheet sheet = workbook.Sheets["Игры"];
foreach (Range row in sheet.Rows)
{
var o = GameConverter(row.Cells);
session.Store(o);
}
session.SaveChanges();
}
<...>
}
Вроде бы всё просто: делаем объекты, пихаем их в базу. Но что это? Падение при попытке сохранить первый же объект?

Хм… раз мы не управляем генерацией идентификаторов объектов вручную, RavenDB пытается генерировать их сам на основе имени класса сохраняемого объекта и у нашего анонимного типа оно несколько длинновато. Выхода два: делать дополнительную работу по генерации идентификаторов объектов, либо генерировать объект по-другому. Поскольку лень – двигатель прогресса, мы пойдём вторым путём.
Как очистить базу
Самым неожиданным моментом было обнаружить, что вот так просто нельзя дропнуть всю БД и начать с нового листа. Возможно, есть способ и попроще, но самый простой выход, до которого дошёл я, – это создать индекс, возвращающий все документы, а потом вызвать метод массового удаления, используя этот самый индекс.
private static void InitDb(IDocumentStore store)
{
try
{
store.DatabaseCommands.GetIndex("AllDocuments");
}
catch (InvalidOperationException)
{
store.DatabaseCommands.PutIndex("AllDocuments", new IndexDefinition {Map = "from doc in docs select new {doc}"});
}
store.DatabaseCommands.DeleteByIndex("AllDocuments", new IndexQuery {PageSize = int.MaxValue}, false);
}
Конечно, чтобы удалить все документы, придётся сначала дождаться полной индексации и, возможно, более корректно обрабатывать paging, но для прототипа сойдёт и так.
Как создавать объекты. Попытка №2
Да, анонимные объекты хороши не всегда. Следующий логичный шаг – создание объектной модели. Ничего особенно умного, просто разбить наш анонимный класс на подклассы и дать им имена.
private class Field
{
public dynamic value;
public string comment;
}
private class Title
{
public Field localized;
public Field original;
}
private class Game
{
public string type;
public Title title;
public Field publisher;
public Field disks;
public Field architecture;
public Field media;
public Field protection;
public Field language;
public Field shippedVersion;
public Field latestPatch;
public Field borrowedBy;
public Field purchaseDate;
public Field price;
}
Вот, теперь всё работает.
{
"type":"game",
"title":{
"localized":{
"value":"Fate/stay night",
"comment":null
},
"original":{
"value":"フェイト/ステイナイト",
"comment":"FEITO / SUTEI NAITO"
}
},
"publisher":{
"value":"Type-Moon",
"comment":null
},
"disks":{
"value":1,
"comment":null
},
"architecture":{
"value":"PC",
"comment":null
},
"media":{
"value":"DVD",
"comment":null
},
"protection":{
"value":"CD",
"comment":null
},
"language":{
"value":"я",
"comment":"есть патч англификации"
},
"shippedVersion":{
"value":null,
"comment":null
},
"latestPatch":{
"value":null,
"comment":null
},
"borrowedBy":{
"value":null,
"comment":null
},
"purchaseDate":{
"value":39764,
"comment":null
},
"price":{
"value":2559.05,
"comment":null
}
}
Только что-то как-то слишком много лишнего. Сплошные null’ы. Что-то мне подсказывает, что этого можно избежать, а ключ лежит в использовании DLR и конструировании объектов по мере надобности.
Как создавать объекты в runtime
Итак, ExpandoObject. Новый класс, призванный в паре с dynamic дать разработчиком большую гибкость по работе с объектами во время выполнения программы. Вещь, доступная в таких языках, как Python, Ruby, JavaScript, PowerShell и прочих. Нас же интересует добавление новых полей по мере надобности.
private static dynamic GameConverter(dynamic gameRow)
{
dynamic game = new ExpandoObject();
game.type = "game";
game.title = new ExpandoObject();
dynamic field = GetField(gameRow, 1);
if (field != null) game.title.localized = field;
field = GetField(gameRow, 2);
if (field != null) game.title.original = field;
field = GetField(gameRow, 3);
if (field != null) game.publisher = field;
field = GetField(gameRow, 4);
if (field != null) game.disks = field;
<...>
return game;
}
private static dynamic GetField(dynamic row, int idx)
{
dynamic value = row[idx].Value2;
if (value == null || value.ToString().Trim() == "") return null;
dynamic field = new ExpandoObject();
field.value = value;
dynamic comment = GetComment(row[idx].Comment);
if (comment != null) field.comment = comment;
return field;
}
private static string GetComment(dynamic cell)
{
return cell == null ? null : cell.Shape.AlternativeText;
}
Запускаем. И… это именно то, что надо 🙂
{
"type":"game",
"title":{
"localized":{
"value":"Fate/stay night"
},
"original":{
"value":"フェイト/ステイナイト",
"comment":"Подпись: FEITO / SUTEI NAITO"
}
},
"publisher":{
"value":"Type-Moon"
},
"disks":{
"value":1
},
"architecture":{
"value":"PC"
},
"media":{
"value":"DVD"
},
"protection":{
"value":"CD"
},
"language":{
"value":"я",
"comment":"Подпись: есть патч англификации для 2 из 3 путей"
},
"purchaseDate":{
"value":39764
},
"price":{
"value":2559.05
},
"Id":"expandoobjects/15"
}
Только вот смотрю я на весь этот копипаст и кажется, что можно это дело улучшить. Глянем в MSDN, ага, так и есть – ExpandoObject реализует интерфейс IDictionary. Отлично!
private static dynamic GameConverter(dynamic gameRow)
{
dynamic game = new ExpandoObject();
game.type = "game";
game.title = new ExpandoObject();
AppendFieldIfNotNull(game.title, "localized", GetField(gameRow, 1));
AppendFieldIfNotNull(game.title, "original", GetField(gameRow, 2));
var fields = new[]
{
"publisher",
"disks",
"architecture",
"media",
"protection",
"language",
"shippedVersion",
"latestPatch",
"borrowedBy",
"purchaseDate",
"price"
};
for (int i = 3; i < 14; i++)
AppendFieldIfNotNull(game, fields[i - 3], GetField(gameRow, i));
return game;
}
private static void AppendFieldIfNotNull(ExpandoObject obj, string field, dynamic value)
{
if (value == null) return;
if (value.ToString().Trim() == "") return;
// ReSharper disable RedundantCast
(obj as IDictionary<string, object>)[field] = value;
// ReSharper restore RedundantCast
}
Здесь, кстати, нашёлся небольшой баг в решарпере (кстати, буквально за день до этого я нашёл другой баг – в компиляторе c#, который был гораздо неприятней).
Дальнейшая работа уже не так интересна – написание однотипных конвертеров для каждой страницы, перенос дополнительной мета-информации (цвет записи), правильное конвертирование данных (даты) и перенос сортировок в виде индексов, но в этом уже нет ничего нового.

Leave a Reply