О некоторых неочевидных хаках при работе с entity framework и unique constraints

image
Пару лет назад, когда деревья были большие и зеленые, ко мне пришли злые дотнетчики, и сказали — ага, попался! пришлось мне помочь коллегам в одном весьма странном проекте.

А именно — представьте себе пачку цифирей, которые аналитики составляют раз в месяц, в любимом ими пакете MS Office. И вот раз в месяц появилась необходимость эти цифры пережевывать и загружать в БД под управлением MS SQL.

И конечно же — этот мега-тул надо было сделать быстро. Чтобы потом передать на суппорт дешевым то ли малайцам, то ли индусам. Так что еще и рекомендовалось делать максимально понятно.

Как начали решать задачу

image
Злые дотнетчики решили упростить себе жизнь — не составлять же insert into руками, если в соответствующем отчете колонок столько, что имена им эксель дает трехбуквенные. А в древней БД огромная стопка таблиц, в некоторых из которых количество колонок просто потрясает воображение.

Поэтому, было сделано так — БД подсунули вижуалстудии, и получили огромную портянку кода для entity framework. Вместо пиления лобзиком и составления prepared statement с сотней вопросиков в values(…) — обычное заполнение entity objects с последующим context.SaveChanges().

Замечу — правильное решение. А premature optimizations как известно — зло.

На что наступили с таким подходом

image
Чую запах горелого. Срочно бегу на кухню, вынимаю мясо из латки. Обрезаю горелое двуручным мечом и тут же проглатываю, так как Уголек врывается в комнату. Горячо! Но изображаю.
— Что это, Мастер?
— Парная китятина. Последний писк моды!
— Не заливай — про моду ты знаешь только из словаря, — пробует. — Хотя действительно вкусно!

Гладко было на бумаге… Unique constraints далеко не все циферки соглашались кушать.

Потрясающий факт — если entity framework получает database level exception, то становится в позу бегущий кабан по рекомендации микрософта у нас есть только один путь — этот контекст пристрелить и создать следующий. В данном случае это означает, что надо master из старого контекста вытащить, или пересоздать, и желательно не копированием всех полей в catch().

И конечно же чтобы было интереснее — объекты из разных контекстов смешивать нельзя.

Подпорка с пересозданием во многих случаях оказалась нетривиальной, да и как эти хаки будут поддерживать малайцы — большой вопрос.

И вот в этот момент эти злыдни принесли утюг и паяльник! дошли до меня.

Что пришлось сделать

image
Карапет возмущен.
— Вредины! Просили энергии сто килограмм, а ухнули сколько! Ты мне так и скажи — надо много, зачем обманывать Карапета? Мне же не жалко, надо пять тонн — так и скажи, Карапет, дай нам пять тонн… Вредины они вредины и есть!

Как выяснилось, сущности CancelChanges в entity framework не предусмотрено. А ее наличие дало бы шанс не усложнять.

Как легко догадаться, пришлось изобрести.

Для начала, определяем подход — пошарить по контексту, и выловить тот самый больной зуб элемент, который приводит к такому печальному результату. И — выкинуть его из контекста. Данные конечно от этого в БД не появятся — но контекст останется рабочим, что нам и требуется. А с кривыми данными пусть аналитики разбираются, наша задача написать им — где такое нашлось.

Куды ложить?

image
— Афа, возьми себя в руки!
Обхватываю себя руками, отрываюсь от пола на биогравах.
— Взял. Куды ложить?

В примерах я сосредоточился для 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(); 

и приступить к сеансу экзорцизма — а что это у нас тут такое странное приползло, и главное — куда его девать.

Не дома тоже не ори

image
От давления лопается экран. Беру веник и собираю осколки — я же руководитель экспедиции. Угол что-то хочет сказать — какой у него писклявый голос на глубине в три километра. Говорю ему
— Дома не ори.
Подумав, добавляю
— И не дома тоже не ори.

Первое что нам надо сделать — это учесть 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/

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

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