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

в 15:52, , рубрики: .net, entity framework, orm, Программирование, метки: , ,

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!

Возможно, этот подход окажется кому-нибудь полезным.

Автор: viklequick

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js