Интро
Уже давно хотелось пощупать главное нововведение .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#, который был гораздо неприятней).
Дальнейшая работа уже не так интересна – написание однотипных конвертеров для каждой страницы, перенос дополнительной мета-информации (цвет записи), правильное конвертирование данных (даты) и перенос сортировок в виде индексов, но в этом уже нет ничего нового.
Добавить комментарий