Pull to refresh

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

Reading time 5 min
Views 10K
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;  // 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();


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

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

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


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

Возможно, этот подход окажется кому-нибудь полезным.
Tags:
Hubs:
+1
Comments 9
Comments Comments 9

Articles