Пару лет назад, когда деревья были большие и зеленые,
А именно — представьте себе пачку цифирей, которые аналитики составляют раз в месяц, в любимом ими пакете MS Office. И вот раз в месяц появилась необходимость эти цифры пережевывать и загружать в БД под управлением MS SQL.
И конечно же — этот мега-тул надо было сделать быстро. Чтобы потом передать на суппорт дешевым то ли малайцам, то ли индусам. Так что еще и рекомендовалось делать максимально понятно.
Как начали решать задачу
Злые дотнетчики решили упростить себе жизнь — не составлять же insert into руками, если в соответствующем отчете колонок столько, что имена им эксель дает трехбуквенные. А в древней БД огромная стопка таблиц, в некоторых из которых количество колонок просто потрясает воображение.
Поэтому, было сделано так — БД подсунули вижуалстудии, и получили огромную портянку кода для entity framework. Вместо пиления лобзиком и составления prepared statement с сотней вопросиков в values(…) — обычное заполнение entity objects с последующим context.SaveChanges().
Замечу — правильное решение. А premature optimizations как известно — зло.
На что наступили с таким подходом
Чую запах горелого. Срочно бегу на кухню, вынимаю мясо из латки. Обрезаю горелое двуручным мечом и тут же проглатываю, так как Уголек врывается в комнату. Горячо! Но изображаю.
— Что это, Мастер?
— Парная китятина. Последний писк моды!
— Не заливай — про моду ты знаешь только из словаря, — пробует. — Хотя действительно вкусно!
Гладко было на бумаге… Unique constraints далеко не все циферки соглашались кушать.
Потрясающий факт — если entity framework получает database level exception, то становится в позу бегущий кабан по рекомендации микрософта у нас есть только один путь — этот контекст пристрелить и создать следующий. В данном случае это означает, что надо master из старого контекста вытащить, или пересоздать, и желательно не копированием всех полей в catch().
И конечно же чтобы было интереснее — объекты из разных контекстов смешивать нельзя.
Подпорка с пересозданием во многих случаях оказалась нетривиальной, да и как эти хаки будут поддерживать малайцы — большой вопрос.
И вот в этот момент эти злыдни принесли утюг и паяльник! дошли до меня.
Что пришлось сделать
Карапет возмущен.
— Вредины! Просили энергии сто килограмм, а ухнули сколько! Ты мне так и скажи — надо много, зачем обманывать Карапета? Мне же не жалко, надо пять тонн — так и скажи, Карапет, дай нам пять тонн… Вредины они вредины и есть!
Как выяснилось, сущности CancelChanges в entity framework не предусмотрено. А ее наличие дало бы шанс не усложнять.
Как легко догадаться, пришлось изобрести.
Для начала, определяем подход — пошарить по контексту, и выловить тот самый больной зуб элемент, который приводит к такому печальному результату. И — выкинуть его из контекста. Данные конечно от этого в БД не появятся — но контекст останется рабочим, что нам и требуется. А с кривыми данными пусть аналитики разбираются, наша задача написать им — где такое нашлось.
Куды ложить?
— Афа, возьми себя в руки!
Обхватываю себя руками, отрываюсь от пола на биогравах.
— Взял. Куды ложить?
В примерах я сосредоточился для insertions как наиболее неочевидных. Аналогичным образом разбирается и update. Я не привожу портянку, так как она мало чем отличается от вышеприведенной, да и отслеживать updates проще. Delete у нас как раз нету — это ж паранойя товарищей банкиров, как это из БД что-то удалить? Ни-ни!
Покурив мануалы и как следует погуглив, определим такую структурку данных:
class MyEntry { public EntityObject entity; public string name; public Dictionary<string, EntityKey> refmap; 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():
// added relationships // t means derived class from EntityContext var added = t.ObjectStateManager.GetObjectStateEntries(EntityState.Added);
так что мы теперь можем определить — что это такое было, и переложить себе для детального разбирательства. Примерно так.
List<MyEntry> allDataToProceed = new List<MyEntry>(); List<ObjectStateEntry> refs = new List<ObjectStateEntry>(); foreach (var a in added) { if (a.IsRelationship) { var aaa = a.EntitySet.Name; var from = ((AssociationSet)a.EntitySet).AssociationSetEnds[0]; var to = ((AssociationSet)a.EntitySet).AssociationSetEnds[1]; } else { MyEntry e = new MyEntry(a.EntitySet.Name, a.Entity); allDataToProceed.Add(e); IEnumerable<IRelatedEnd> relEnds = ((IEntityWithRelationships)a.Entity).RelationshipManager.GetAllRelatedEnds(); foreach (var rel in relEnds) { 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 { 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 сломать можно.
// now we need to remove objects already referenced by FK to not add the same // objects twice in collection 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 { IEnumerable<IRelatedEnd> relEnds = ((IEntityWithRelationships)a1.entity).RelationshipManager.GetAllRelatedEnds(); foreach (var rel in relEnds) { var relname = rel.RelationshipName; EntityKey key = null; if (a1.refmap.ContainsKey(relname)) key = a1.refmap[relname]; EntityObject o = key != null ? o = (EntityObject)t.GetObjectByKey(key) : null; 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(); // now we can move back to excel cheet and unform which data is wrong } }
Вуаля. We did it!
Возможно, этот подход окажется кому-нибудь полезным.
ссылка на оригинал статьи http://habrahabr.ru/post/160897/
Добавить комментарий