Пару лет назад, когда деревья были большие и зеленые,
А именно — представьте себе пачку цифирей, которые аналитики составляют раз в месяц, в любимом ими пакете MS Office. И вот раз в месяц появилась необходимость эти цифры пережевывать и загружать в БД под управлением MS SQL.
И конечно же — этот мега-тул надо было сделать быстро. Чтобы потом передать на суппорт дешевым то ли малайцам, то ли индусам. Так что еще и рекомендовалось делать максимально понятно.
Как начали решать задачу
Злые дотнетчики решили упростить себе жизнь — не составлять же insert into руками, если в соответствующем отчете колонок столько, что имена им эксель дает трехбуквенные. А в древней БД огромная стопка таблиц, в некоторых из которых количество колонок просто потрясает воображение.
Поэтому, было сделано так — БД подсунули вижуалстудии, и получили огромную портянку кода для entity framework. Вместо пиления лобзиком и составления prepared statement с сотней вопросиков в values(...) — обычное заполнение entity objects с последующим context.SaveChanges().
Замечу — правильное решение. А premature optimizations как известно — зло.
На что наступили с таким подходом
Чую запах горелого. Срочно бегу на кухню, вынимаю мясо из латки. Обрезаю горелое двуручным мечом и тут же проглатываю, так как Уголек врывается в комнату. Горячо! Но изображаю.
— Что это, Мастер?
— Парная китятина. Последний писк моды!
— Не заливай — про моду ты знаешь только из словаря, — пробует. — Хотя действительно вкусно!
Гладко было на бумаге… Unique constraints далеко не все циферки соглашались кушать.
Потрясающий факт — если entity framework получает database level exception, то
И конечно же чтобы было интереснее — объекты из разных контекстов смешивать нельзя.
Подпорка с пересозданием во многих случаях оказалась нетривиальной, да и как эти хаки будут поддерживать малайцы — большой вопрос.
И вот в этот момент
Что пришлось сделать
Карапет возмущен.
— Вредины! Просили энергии сто килограмм, а ухнули сколько! Ты мне так и скажи — надо много, зачем обманывать Карапета? Мне же не жалко, надо пять тонн — так и скажи, Карапет, дай нам пять тонн… Вредины они вредины и есть!
Как выяснилось, сущности CancelChanges в entity framework не предусмотрено. А ее наличие дало бы шанс не усложнять.
Как легко догадаться, пришлось изобрести.
Для начала, определяем подход — пошарить по контексту, и выловить тот самый
Куды ложить?
— Афа, возьми себя в руки!
Обхватываю себя руками, отрываюсь от пола на биогравах.
— Взял. Куды ложить?
В примерах я сосредоточился для insertions как наиболее неочевидных. Аналогичным образом разбирается и update. Я не привожу портянку, так как она мало чем отличается от вышеприведенной, да и отслеживать updates проще. Delete у нас как раз нету — это ж паранойя товарищей банкиров, как это из БД что-то удалить? Ни-ни!
Покурив мануалы и как следует погуглив, определим такую структурку данных:
class MyEntry
{
public EntityObject entity; // entity object, натурально
public string name; // имя в терминах EF
public Dictionary<string, EntityKey> refmap; // foreign keys - имя - значение, если есть
// А вот эти коллекции нужны если мы значение FK из контекста получить не можем
public Dictionary<string, EntityObject> objmap; // собственно объекты
public Dictionary<EntityObject, string> keymap; // и имена этих объектов
public MyEntry(string s, EntityObject o)
{
entity = o;
name = s;
refmap = new Dictionary<string, EntityKey>();
objmap = new Dictionary<string, EntityObject>();
keymap = new Dictionary<EntityObject, string>();
}
}
Теперь мы можем заняться магией. Вот так определяется — что было добавлено в контекст после последнего SaveChanges():
// t это derived class от EntityContext
var added = t.ObjectStateManager.GetObjectStateEntries(EntityState.Added);
так что мы теперь можем определить — что это такое было, и переложить себе для детального разбирательства. Примерно так.
List<MyEntry> allDataToProceed = new List<MyEntry>(); // список объектов для анализа
foreach (var a in added)
{
// у EF в контексте есть еще и отношения как отд. сущность
// а они нам не нужны
if (!(a.IsRelationship))
{
// тут понятно - запоминаем нечто вроде "Foo", Foo a
MyEntry e = new MyEntry(a.EntitySet.Name, a.Entity);
allDataToProceed.Add(e);
// а теперь можно собрать foreign keys к нашему объекту
IEnumerable<IRelatedEnd> relEnds =
((IEntityWithRelationships)a.Entity).RelationshipManager.GetAllRelatedEnds();
foreach (var rel in relEnds)
{
// вот тут будет список FKs
List<EntityObject> fks = new List<EntityObject>();
foreach (var obj in rel)
fks.Add((EntityObject)obj);
// этот список собрали и даже как связь называется - нашли
var relname = rel.RelationshipName;
if (fks.Count == 1)
{
// нам повезло - значения ключа в наличии, просто запомним
if (fks[0].EntityKey.EntityKeyValues != null)
e.refmap[relname] = fks[0].EntityKey;
else
{
// а тут другой случай - ключей нету,
// нам их надо собрать. Случай - к примеру
// добавлен мастер к нашему слейву
// и запомнить нам надо и как FK зовут и его содержимое
e.keymap[fks[0]] = fks[0].EntityKey.EntitySetName;
e.objmap[relname] = fks[0];
}
}
}
}
}
Осталось собственно дело за малым — вернуть контекст к жизни
// Из контекста удаляем все что зависло
foreach (var a1 in added)
a1.Delete();
t.SaveChanges();
и приступить к сеансу экзорцизма — а что это у нас тут такое странное приползло, и главное — куда его девать.
Не дома тоже не ори
От давления лопается экран. Беру веник и собираю осколки — я же руководитель экспедиции. Угол что-то хочет сказать — какой у него писклявый голос на глубине в три километра. Говорю ему
— Дома не ори.
Подумав, добавляю
— И не дома тоже не ори.
Первое что нам надо сделать — это учесть foreign keys. Если какой-либо объект создан как часть цепочки master-slave — то не надо master и slave складывать по отдельности, а то так и referral integrity сломать можно.
// Нам надо проигнорить те объекты которые у нас добавятся по FK
// а то попытаемся добавить два раза
List<EntityObject> usedInRefs = new List<EntityObject>();
foreach (var a1 in allDataToProceed)
{
foreach (var dup in a1.objmap.Values)
usedInRefs.Add(dup);
}
// собственно удаляем
for (int j = 0; j < allDataToProceed.Count; ++j)
{
if (usedInRefs.Contains(allDataToProceed[j].entity))
{
allDataToProceed.RemoveAt(j);
--j;
}
}
И наконец дело за малым — осталось запихать в БД то что можно запихнуть. Для этого восстанавливаем нашей копии foreign keys и добавляем в контекст. Удалось? отлично, нет — значит это и есть наш больной зуб.
// из того что выгребли - пробуем закинуть в БД поштучно
foreach (var a1 in allDataToProceed)
{
try
{
// не забываем восстановить FKs для master/slave
IEnumerable<IRelatedEnd> relEnds =
((IEntityWithRelationships)a1.entity).RelationshipManager.GetAllRelatedEnds();
foreach (var rel in relEnds)
{
var relname = rel.RelationshipName;
EntityKey key = null;
// если у нас есть ключ и его значение от EF
// то мы собственно объект из контекста и берем
if (a1.refmap.ContainsKey(relname)) key = a1.refmap[relname];
EntityObject o = key != null ? o = (EntityObject)t.GetObjectByKey(key) : null;
// А если не нашли - то значит это случсай добавления
// master/slave одновременно
if (o == null && a1.objmap.ContainsKey(relname))
{
o = a1.objmap[relname];
if (a1.keymap.ContainsKey(o)) t.AddObject(a1.keymap[o], o);
else o = null;
}
// если его нашли - то восстанавливаем связь
if (o != null) rel.Add(o);
}
// теперь можно просто добавить в БД
t.AddObject(a1.name, a1.entity);
t.SaveChanges();
}
catch (Exception e2)
{
// добавить именно этот объект нам не удалось - но мы его нашли
// очистим контекст от неудачной попытки
added = t.ObjectStateManager.GetObjectStateEntries(EntityState.Added);
foreach (var a2 in added) a2.Delete();
// и можем что-то с этим уже сделать, хотя бы в лог записать
}
}
Вуаля. We did it!
Возможно, этот подход окажется кому-нибудь полезным.