RavenDB, dynamic и Excel

Интро

Уже давно хотелось пощупать главное нововведение .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();
		}
<...>
}

Вроде бы всё просто: делаем объекты, пихаем их в базу. Но что это? Падение при попытке сохранить первый же объект?

2_exception

Хм… раз мы не управляем генерацией идентификаторов объектов вручную, 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#, который был гораздо неприятней).

Дальнейшая работа уже не так интересна – написание однотипных конвертеров для каждой страницы, перенос дополнительной мета-информации (цвет записи), правильное конвертирование данных (даты) и перенос сортировок в виде индексов, но в этом уже нет ничего нового.


Comments

2 комментария на ««RavenDB, dynamic и Excel»»

  1. Аватар пользователя Void

    Здравствуйте, у меня такой вопрос, можно ли использовать тип dynamic для работы с Excel? если да то как?

  2. Это вопрос из той же категории что и «А можно на C# программы писать? И если да, то как?».

    Конечно можно. Всё зависит от сценария. Надо только понимать, что dynamic ― это тяжёлая артиллерия, которая тянет за собой вопрос производительности и невозможность статической проверки типов при компиляции.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *