Pull to refresh

Comments 38

Также необходимо определить суперкласс для ограничения множества бизнес объектов, с которыми будет работать ORM.

Это сразу плохо. У вас инфраструктурный слой (репозиторий) просочился в виде давления в бизнес-слой. Нет никакой причины, почему все бизнес-классы должны наследовать от одного базового.


public abstract int Id { get; set; }

А если у меня не целочисленные идентификаторы?


Осуществить привязку бизнес объекта к таблице в базе данных на основе атрибутов

… и опять просочилась зависимость в бизнес-слой. Теперь я не могу создать бизнес-объект, не имея ссылки на сборку с атрибутами маппинга.


//в качестве аргумента базовому классу передаем название строки подключения

Зачем?


Бизнес-транзакция должна выполняться в рамках одного потока

Это почему? Все async запретили? И почему не использовать уже существующий TransactionScope?


var uow = new UnitOfWork();
using (Session.Create(uow))
{
var profileRepo = new ProfileRepository();
//Вызов методов репозитория
uow.Commit();
}

Нелогично. Вы диспозите и коммитите разные вещи. Совершенно не понятно, почему нельзя просто писать


using(var uow = new UnitOfWork())
{
  uow.Commit();
}

(без учета DI, но он и у вас не учтен)


public override bool Equals(object obj)
{
return obj.GetHashCode() == GetHashCode();
}
public override int GetHashCode()
{
//в пределах одной бд, тип объекта и идентификатор однозначно определяют его уникальность
var code = Key.GetHashCode() + Value.Id.GetHashCode();
//хэш код должен быть положительным числом
return code > 0 ? code : (-1) * code;
}

Колизии на хэшкодах от типов вас не смущают? А еще более смешной вариант "хэшкоды типа и идентификатора взаимно совпадают"?


Ну и зачем вообще это структура?


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

Почему?

Необходимо учесть, что бизнес объекты одной транзакции могут быть из разных бд. Логично предположить что для каждой бд должен быть определен свой экземпляр трекинга
Почему?
Там два предложения. «Почему» — к какому предложению задано?
А почему нет?
  1. Все равно необходимо будет переключаться на другую бд, при выполнении запроса, так почему бы сразу не скомпоновать их по базам?
  2. Объекты трекинга для одной бд(в простейшем случае, если нет зависимостей) не зависят от объектов в другой.

Потому что порядок выполнения зависит не от базы.

public override bool Equals(object obj)
{
return obj.GetHashCode() == GetHashCode();
}

Это же абсолютное зло.
Лучше разобраться с базовыми вещами, и только потом писать ORM или любые другие библиотечные вещи.
var code = Key.GetHashCode() + Value.Id.GetHashCode();

Если вы по каким то причинам решили именно так (сложением) вычислять хеш-код, то лучше явно указать unckecked mode:
var code = unchecked(Key.GetHashCode() + Value.Id.GetHashCode();)

//хэш код должен быть положительным числом
return code > 0 ? code : (-1) * code;

Во-первых, почему хеш-код должен быть положительным?

Во-вторых:

  • зачем (-1) * code, если можно написать просто -1?
  • зачем пытаться сменить знак для возможного нуля, если можно написать «return code >= 0? code: ...»?
  • что будете делать, если в code окажется Int32.MinValue? — при включенной проверке переполнения при умножении на "-1" получите OverlowException, при выключенной получите «Int32.MinValue» (а как же исходное «хеш-код должен быть положительным»).
  • Да, с unckecked, конечно, правы.
  • Положительный хэш нужен для корректного формирования текста и параметров sql запроса:

using (var command = conn.CreateCommand())
                {
                    command.CommandText = cmdBuilder.Invoke(command, objs);
                    command.CommandType = CommandType.Text;
                    conn.Open();
                    result = command.ExecuteListReader<T>();
                }
Из статьи неясно, хеши объектов будут подставляться в запрос как идентификаторы? Если да, то это неверное решение. Если нет, то в для чего они используются при построении запроса?

Сам хеш в вашем случае нужно, для минимизации коллизий (а также для получения неотрицательного значения), вычислять примерно так:
public override GetHashCode() =>
(unchecked(someConstant * Key.GetHashCode()) ^ Value.Id.GetHashCode())
& Int32.MaxValue; // & 0x7FFFFFFF


И разберитесь с матчастью в части реализации Equals.
Положительный хэш нужен для корректного формирования текста и параметров sql запроса
Не знаю про какой sql сервер вы говорите, но в MS SQL Server, отрицательный int в PK никто не запрещал.
+Более общее соображения, что использовать хеш в качестве ключа в БД нельзя
Об этом прямо здесь прямо написано:
A hash code is not a permanent value. For this reason:
Do not serialize hash code values or store them in databases.

То же самое и в других платформах — Java и любой другой, вследствие природы хеш-кода объекта.
Приходит новый человек на проект, а там вместо привычных EF или NHibernate — свой велосипед, со своими багами, со своими ограничениями. Очень позитивно.
Я и не писал, о необходимости своих велосипедов. Я о том, что лучше дать новому человеку больше инфы о принципах лежащих в основе работы, дабы избежать любой неопределенности.
А заодно со своими возможностями, некоторыми из которых в EF не пахнет.
Сделать действительно всеобъемлющий ORM невозможно в силу принципиальной разности концепций, всегда что-то приносится в жертву.
Не знаю насчет EF, но NHibernate мне хватает по самое не могу даже на крупных проектах. Боюсь представить, что вы там изобрели такого, что вам не хватает.
Автоаудит в отдельные таблицы, CTE, инлайнинг вьюшек, оконные функции.
Я если честно из этих терминов встречал только «оконные функции» и те в другом контексте. Сможете пояснить, как это примерно хотелось бы использовать и зачем?
Автоаудит — это когда кто-то где-то что-то поменял, и в специальную табличку (в моём случае — почти каждая таблица с данными имеет свою собственную таблицу аудита) пишется, кто, что и когда. Можно сделать триггерами, но, во-первых, триггеров нужно катастрофически много, во-вторых — сложно определить юзера.

CTE — это такая фича T-SQL, когда можно в рамках запроса объявить что-то вроде вьюшки и потом пользоваться ей. В духе
WITH someCTE AS (
  SELECT a, b FROM (
    SELECT a, b FROM table1
    UNION 
    SELECT a, b FROM table2
  ) un LEFT JOIN table3 t3 ON (t3.a = un.a)
)
SELECT c
FROM table4
LEFT JOIN someCTE cte on  (cte.a = table4.c)
LEFT JOIN (
  SELECT * FROM table5 INNER JOIN someCTE cte ON (cte.a = table5.a)
)
-- любая другая хтонь


Оконные функции — такая штука, позволяющая агрегировать (и делать некоторые другие вещи) данные без группировок. Или, например, опираться на значение из нескольких строк. Скользящие средние, номера строк внутри групп, средние/суммы/чтоУгодноЕщё по группам,… you name it.

Инлайнинг — это когда вьюшка подставляется в запрос путём собственно вставки её текста в подзапрос. Зачем?
Допустим, у нас есть некоторая очень тяжёлая вьюшка, которую нельзя проиндексировать (лефт джойны, недетерминированные функции, таблицы из разных баз, да что угодно). При этом на неё накладываются достаточно жёсткие условия, уменьшающие её на несколько порядков:
SELECT * FROM matters m
LEFT JOIN view1 vw ON (vw.matterId = m.id)
WHERE vw.UserId = 150


Часто можно это самое условие наложить где-нибудь внутри:
SELECT * FROM matters m
LEFT JOIN (SELECT * FROM users /*много-много джойнов*/ WHERE vw.UserId = 150) vw -- текст вьюшки
ON (vw.matterId = m.id)

И получить эти самые порядки прироста производительности. Table-valued функции при этом использовать не представляется возможным, потому что фильтры заранее неизвестны.
Почти все перечисленное — только для голого sql в условиях проседания производительности. Слабо верится, что можно на это дело адекватно ОРМ написать.

Некоторые вещи имхо просто не нужны в крупных приложениях с ОРМ, например первая =)
Десять миллионов строк, тридцать терабайт данных и Пепси, GM и Дисней в клиентах — достаточно крупное приложение? :)

И все они интегрированы в кастомную ОРМ.
Так ОРМ для упрощения работы с базой, а не для производительности.

Ессна, что типовые решения не подойдут на большие нагрузки, но и начинать с кастомной ОРМ ради этого точно не стоит.

ПС: пилите на здоровье, раз кто-то готов оплачивать. Ура, как говорится =)
EF и Hibernate, изначально не создавались для вашего проекта.
Выбор ORM должен быть продиктован задачами вашего бизнеса.
Например, если у вас на проекте или на какой-то его части, большинство запросов легкие, read only, тогда использование тяжелого EF никак не улучшит перформанс — это стрельба из пушки по воробьям. В этом случае стоит написать свое(как и сделали в Stack Overflow).
Зачем для использования временных таблиц или CTE, писать ORM?) В EF или Nhibernate никто не запрещает вам создать обертку ХП, в которой можете развлекаться с sql как угодно.
Данное решение for fun, не более того, конечно.
Это популизм. В первой строчке статьи, я написал о об этом. Этот проект типа for fun.
Конечно, нет спора лучше чем свой костыль vs EF vs Hbn vs Dapper и т.д.
Сразу определимся с терминами: под трекингом будем понимать отслеживание изменений бизнес объектов в рамках одной транзакции, для дальнейшей синхронизации данных, хранящихся
в оперативной памяти и содержимого бд.

Слово синхронизация на мой взгляд не подходит, так как предполагает двухстороннее взаимодействие DB<->RAM.
То есть когда стороннее приложение пишет в БД, трекинг БД кидает сообщение в ORM, вызывая обновление объекта в RAM, как в ADO.NET SqlDependency (ALTER DATABASE <db_name> SET ENABLE_BROKER;).

В вашем случае лучше как то так — «для дальнейшего сохранения данных, хранящихся в оперативной памяти в содержимое бд».
Слово «синхронизация» подходит полностью. Причем тут инструкции типа ALTER?
//регистрируем «чистые» объекты путем копирования полученных из бд,
//исходные считаем «грязными»

Обычно чистые те которые получены из бд.
Как только чистые-БДшные сущности изменили, их называют грязными, до тех пор пока их не сохранили в БД.
Я о том и писал, что «dirty objects», это те, кто сохранили свою ссылочную целостность до сохранения в бд.
Потом, когда и если доведете ORM до ума, в метод ChangeDirtyObjs можно добавить построение и кэширование Expression Trees, что бы долго не ходить рефлексией по var props = type.GetProperties();
Первый раз получил type, построил Expression Tree, закэшировал, следующий раз если есть в кэше есть type, вызвал Expression
Да, конечно, https://habrahabr.ru/post/269699/, знаем. Пока было глупо описывать такие дебри.
Вы замахнулись на очень сложную задачу и допустили в ней очень много ошибок проектирования и пропустили антипатернов.
Начинать всегда стоит с простых вещей, построение запросов и Change Tracking это не первоочередного порядка вещи в ОРМ, а плюшки вендора скорее.

У вас очень много ошибок с тем как вы выстраиваия уровни абстракций и интерфейсы для разных задач. Реализация так же подводит, работа с блокировками, dispose обьектов, object equality и асинхронностью.

Асинхронные методы возвращающие void и вызывающие синхронный i\o api — грубейшие ошибки.
public void Commit()
{
SaveChanges();
}
private async void SaveChanges()
{
await Task.Run(() =>
{
}
);
}


https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Этот код не дает возможности обработать ошибки вызывающему коду, на тред пуле выполняете блокирующую i\o операцию, да еще и потом синхронизируеться неявно в конце через execution контекст выбрасывая silent ошибки.

Если говорить про Asp.Net например, ваш Save changes выполниться уже когда пользователю прийдет ответ в браузере.
Мне не совсем понятно зачем там используется Task.Run — но он там используеться неправильно.
Да, спасибо. Я так понимаю надежней для библиотечных вещей использовать ConfigureAwait(false) чтобы сказать системе, что она не должна выполнять фоновую задачу в вашем контексте http/GUI.
Sign up to leave a comment.

Articles