Pull to refresh
31
0
Антон Куранов @Throwable

Пользователь

Send message

Это все понятно, но я не понимаю, если брать например JPA, чем EntityRepository.findByXXX() концептуально лучше EntityManager.createQuery(...).getResultList()? Когда количество сущностей в модели данных близится к нескольким десяткам, а запросы становятся не такими тривиальными, SD уже начинает мешать, а потом сильно мешать. Количество объектов-репозиториев стремится также к нескольким десяткам, а некоторые из них быстро становятся общей помойкой всевозможных методов из совершенно разных бизнес-задач.


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

Совершенно согласен, коллега. Силами пиарщиков у программеров создается стойкое впечатление, что любой проект на Java — это обязательно Spring. На самом деле Spring (Boot) — это про то, как максимально быстро и кратко написать приложение "Hello world" с кучей подключенных технологий. Когда же дело доходит до реальной бизнес-логики, вся эта подковерная магия начинает сильно мешать, а программирование превращается в рыскание по stackoverflow в поисках магических рецептов и заклинаний. В частности, вместо магии SpringData гораздо удобнее испльзовать type-safe обертки типа Morphia + QueryDSL или Hibernate OGM, нежели репозитории с магическими findBy.

Масштаб, конечно, поражает — положил либерийский сервер и оставил страну без интернета. К слову о том, что такое Либерия: https://www.tema.ru/travel/liberia-1/

Это не совсем так. В текущей имплементации ThreadLocals реально хранятся локально в каждом треде как Thread.threadLocals: Map<ThreadLocal, Object>. Доступ к ним действительно очень быстрый, однако есть проблемы с очисткой — если не был въявную вызван ThreadLocal.remove(), то слот может болтаться в Thread-е до следующего рехэша.

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

Основная разница в том, что AOP-фреймворк создает объект динамически, инъектируя поведение в зависимости от текущиего контекста и его конфигурации. А вот в функциональном подходе поведение линкуется статически. Для обеспечения гибкости поведение можно также делегировать контексту, но ссылку на него придется указывать дополнительно. То есть либо придется всегда передавать контекст в качестве одного из параметров, либо создавать глобальный объект-синглтон для контекста, ну или как вариант инъектировать его через ThreadLocal.

Все это хорошо пока не дойдешь до динамических запросов с несколькими параметрами

Основная проблема в том, что мы хотим по-возможности иметь type-safe queries. А все эти искусственные findBy*() или Query не решают проблемы. Интерфейс Repository начинает мешать, когда число сущностей становится несколько десятков, а модель данных не такая тривиальная. Кроме того, эстетически findFirst5ByFirstNameStartsWithOrderByFirstName() читается в десять раз хуже, чем обычный хорошо оформленный JPQL-запрос, особенно если для этого используется какой-нибудь QueryDSL или Criteria API.


QueryDSL, как я понял, малопопулярен

Очень даже популярен, до сих пор живет и релизится. Правда не так активно. Criteria API уродливый и жутко неудобный (как впрочем и большинство стандартизированных API). Могу порекомендовать замечательную библиотеку Jinq, где критерии задаются ввиде обычных джавовских лямбд. Не надо кодогенерации и искусственного DSL.


Тогда зачем еще один уровень абстракции

А это политика Spring-а — влезть посредником во все, что только возможно, там где нужно и ненужно, чтобы пользователи вообще не мыслили разработку без спринга.

Анализ и рефакторинг кода — если понимать как работает фреймворк — то ни капельки не затрудняет.

Очень даже. Возьмите чужой код, и попробуйте сразу разобраться, как он работает. Для анализа так или иначе вам придется использовать функции поиска вашей IDE типа "Find Usages...". При использовании фреймворка, где объекты не создаются въявную в коде и не содержат статических ссылок, такой анализ станет более трудоемким.


Ослабляет контроль над выполнением — каким образом?

Последовательность инициализации бинов не определена (бывают случаи, где это важно, например инициализация сервисов при стартапе), еще хуже обстоят дела с финализацией — не определено когда стопаются объекты и в какой последовательности. Концепция "областей живучести" (scopes — не знаю как перевести) работает не всегда так, как ожидается, например если вы инъектируете прототайп-бин в синглтон, то он такаже становится синглтонам, что критично для не thread-safe объектов. Использование AOP позволяет динамически модифицировать поведение объекта, что иногда запутывает (некоторые умудряются засандалить в AOP бизнес-логику).


Компиляцию и запуск кода с невалидными зависимостями — для этого есть тесты.

Для юнит-тестов используется как правило специально заряженный тестовый контекст, урезанный и мокированный. Опробовать реальный контекст Вы сможете только, подняв полное приложение, в окружении, наиболее приближенном к продакшну. А это уже интеграционное тестирование, и цикл у него несоизмеримо бОльший.


Зачем? Просто зачем писать руками то, что написано давно за нас?

А что такого супер магического написано-то? Как я уже сказал, типичный DI-фреймворк автоматически, то есть неявно, создает контекст приложения на основании большого числа факторов: модулей, правил, файлов конфигурации, аннотаций объектов, сканирования classpath… Поэтому на этапе чтения кода очень трудно определить как именно будет построен контекст. И это есть явный антипаттерн. А если убрать автоконфигурацию, то от вашего фреймворка ничего и не останется.


P.S. я не против DI-фреймворков, сам давно использую Guice. Но меня жутко бесит, когда для связи трех бинов сразу тащат в проект какой-нибудь Spring.

Третий способ: пусть кто-нибудь ещё обрабатывает зависимости вместо нас

Недостатки

Неа. Основной недостаток всех DI-фреймворков — неявное динамическое связывание в рантайме. Это затрудняет анализ и рефакторинг кода, ослабляет контроль над выполнением, и делает возможным компиляцию и запуск кода с невалидными зависимостями. Хотя есть фреймворки типа Dagger, которые делают связывание при компиляции.


Для простого DI вообще не нужно никакого фреймворка. Делаете класс-контекст ручками так, как вам это заблагорассудится, и определяете в нем все зависимости. Например так:


// лучше назвать его ApplicationContext
class OurThirdPartyGuy {
  fun provideClassC(){
    return ClassC() //just creating an instance of the object and return it.
  }

  // каждый метод должен возвращать уже полностью настроенный объект и не содержать параметров
  fun provideClassB(){
    return ClassB(provideClassC())
  }

  fun provideClassA(){
    return ClassA(provideClassB())
  }
}

Разница лишь в том, что в фреймворк создает класс контекста автоматически на основании расставленных аннотаций и правил. И фреймворк зачастую делает несколько больше, нежели просто DI — например, управление конфигурациями, управление жизненным циклом объектов (scopes), AOP и проксирование методов. Но при необходимости все это так же не составит труда сделать ручками.

Негодование не поэтому. Впечатление от посыла вашей статьи — это "используйте Version и будет вам сериализуемое счастье", тогда для статьи требуется более глубокий анализ: расписать общий принцип работы локов и типичные нюансы испльзования Optimistic Locking в реальных программах.


Например, задайтесь вопросом, если у вас в Entity есть отношение @OneToMany, и вы добавляете/удаляете дочернюю сущность (пусть все сущности имеют Version), будет ли гарантия корректного параллельного выполнения? Поменяется ли Version для родительской сущности при удалении дочерней? Сразу скажу, что в JPA с каскадным локингом не все так просто, и многие вендоры добавляют свой функционал для обеспечения оного.
https://www.eclipse.org/eclipselink/documentation/2.5/jpa/extensions/a_optimisticlocking.htm#BCGIACHD


Что? «Многие...» А можно примеры? Вы наверно имели ввиду «многие реализации этого стандарта»

Вот пример в самой популярной ORM: http://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#locking-optimistic-versionless


Хм… но как наша транзакция узнает, что entity была изменена другой транзакцией без сравнения полей Version

А никак. LockModeType.OPTIMISTIC после чтения уже ничего не проверяет. Проверка и инкремент делается только, если entity была изменена текущей транзакцией. Если нужен более строгий контроль, используйте LockModeType.OPTIMISTIC_FORCE_INCREMENT — он всегда после чтения инкрементирует Version и делает в конце проверку, даже если entity не была изменена в текущей транзакции.


Если сильно все упростить, то Version — это версия данных

Не совсем. См. выше про LockModeType.OPTIMISTIC_FORCE_INCREMENT, там это тупо счетчик чтений.

Давайте разбираться, дабы не ввести добрых людей в заблуждение сим опусом.


Сначала отделим мясо от костей. JPA работает поверх RDBMS леера, и практически все сказанное относится к последнему, и к JPA не имеет никакого отношения. Транзакционность и изолированность данных обеспечивается в первую очередь самой базой данных, а не JPA, которая играет роль лишь обертки ввиде меппера данных и генератора SQL. Теперь:


READ UNCOMMITED — решается с помощью аннотации Version в JPA(об этом как раз и статья)

Это еруйня. Потерянные обновления не возникают в модели RDBMS уже на самом низком уровне READ UNCOMMITED. JPA Version здесь абсолютно ни при чем. К тому же JDBC и в большинство RDBMS по дефолту работают на уровне READ COMMITED.


REPEATABLE READ — при повторном чтении получаются те же данные, что и в начале транзакции. Однако возможна пропажа/добавление рядов от другой транзакции при SELECT… WHERE ..., когда данные удовлетворяют критерию WHERE. Плюс другие транзакции все-же могут изменять прочитанные данные (но не измененные).


SERIALIZABLE — последовательное выполнение транзакций

Симуляция последовательного выполнения с возможностью спонтанного отката ввиду нарушения блокировки. И тем не менее, некоторые вендоры, которые используют схему MVCC, понижают SERIALIZABLE уровень до т.н. "SNAPSHOT" isolation, в котором возможен неприятный феномен типа write-skew.


Теперь вернемся к JPA.


Version была придумана во-первых для того, чтобы можно было отслеживать изменения в Extended Persistence Context, в котором unit-of-work живет дольше одной транзакции, и где Version имплементирует классическую схему MVCC.


два типа блокировок: оптимистичные и пессимистичные. Разница в том, что первые ориентируются на ситуации, в которых множество транзакций пытаются изменить одно поле в одно и тоже время, возникают крайне редко

Разница во-первых в том, что пессимистичные блокировки проверяют соответствие на момент получения блокировки, и как правило для этого исполользуют средства RDBMS (SELECT FOR UPDATE) а оптимистичные — делают все проверки при коммите при помощи сравнения данных или маркеров изменений типа Version. А то, что вы сказали — уже следствие.


В оптимистичных блокировках при коммите в базу данных производится сравнивание значения поля, помеченного как version, на момент получения данных и на данный момент.

Тем не менее поле Version не обязательно. При его отсутствии многие JPA делают полное/частичное сравнение данных полей объектов.


LockModeType.OPTIMISTIC_FORCE_INCREMENT… Вопрос. Зачем? Если после коммита мы хотим еще «поколдовать» над этими же данными, и нам не нужны сторонние транзакции

Вообще не для этого. Это аналог WRITE LOCK для optimistic locking. Гарантирует, что прочитанная Entity не была изменена другой транзакцией до самого коммита, даже если данная транзакция ничего не меняла. Используется для каскадного трекинга изменений, когда поля самой Entity не меняются, но зато меняются ее дочерние entities. При LockModeType.OPTIMISTIC поле Version меняется и проверяется только, если entity была изменена.

Возьмите GCJ чтоли и будет вам счастье. GraalVM как бы не для этого.

Бесполезен. Рефлексия очень глубоко вросла в Java и сильно савязана с Java runtime. Любой фреймворк или практически любая библиотека так или иначе завязана на рефлексию, а без своей экосистемы Java практически бесполезна. Рефлексия не позволяет компилятору вычислить статически все дерево вызовов и слинковать статически только используемый код, как это делает С например. Именно поэтому в Java 9 придумали модули — чтобы хоть как-то обязать пользователя объявлять статически границы, и чтобы финальный бандл можно было порезать с 200-300Мб до 30-40Мб.


Помимо рефлексии фреймворки еще делают такие безобразные вещи как classpath-scan, annotation processing и кодогенерация в рантайме, custom classloaders, etc… Так что много еще что отвалится.

Был когда-то больше 10 лет назад вполне рабочий GCJ. Писали на нем и утилиты и серверы. Все летало, особых багов замечено не было. А 20 лет назад был Microsoft JVM, который поддерживал нативную линковку с DLL-ями, win32 API и ActiveX. Еще есть отличный Excelsior…
GraalVM ждет та же участь — мутное целевое использование, сильная завязанность на Java и ее ограничениях, запоздалость. Сейчас грядет WebAssembly, компиляторы в него из разных языков, затем его выпустят как отдельную VM.

Ну и как бы у get(i) тоже никто не гарантирует константную сложность, но не в этом суть. Плохого то, что этот код еще нужно читать и поддерживать. В Java 8 коллекции итерируются при помощи стримов или при помощи цикла for-each, который был введен еще в 2004 году, а до этого использовались итераторы вместо индексов. Индекс при итерировании используется только с ArrayList, когда итерация идет не по-порядку, и в тех случаях, когда значение индекса используется в цикле.

В данной ситуации я обычно огораживаюсь от человека, который делает дичь.

Отгородиться не удастся, ибо код и проект общий. А таск разбираться с любой проблемой на продакшне поставят тебе, ибо "ты есть лид" и ответственный. Вот и ловишь потом ликинги ресурсов и памяти в коде, к которому ты не имеешь никакого отношения и разбираешься с проблемой падений или проседания перформанса. А по завершении говорят "спасибо" и закрывают таск без всякого разбора полетов (а зачем, все ведь заработало, проблема решена, какая разница кто виноват — мы же все в одной команде?!).


Со скандалистами соглашаются работать такие же скандалисты, а результат такой команды редко выходит хороший.

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

Критика постоянно высказывается, особенно в пулл-реквестах, после работы QA, и пользователями.

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


Вежливостью — да, улыбкой — нет. А потом просто вежливо попросить написать по собственному.

А если это не в вашей компетенции? Причем ответственный за проект ты, но задачи раздает и принимает начальник по принципу "работает — и ладно". Если человек коммитит код на Java в 201x году, где коллекции итерируются: for (int i = 0; i < list.size(); i++) list.get(i). А на просьбу переписать как надо жалуется начальнику, что я придираюсь. И кто тут разводит токсичность?


Ненависть коллектива сложно игнорировать, хочу я вам сказать.

Как показывает практика, ненависть и шум в основном разводят хреновые специалисты, как попытку оправдаться. И она всегда направлена против тех, кто в проекте что-то делает ввиде жалоб типа: "он придирается к моей работе", "он не объясняет мне как делать", "он не делится информацией", "он меня ничему не учит". HR, апелируя к жалобам, в итоге в токсичном поведении винят совсем других людей. Поэтому автор здесь прав — IT не детский сад, а спецы не няньки. Если человек некомпетентен, он баласт в проекте и будет только ухудшать климат и портить проект.

Если не вдаваться в детали, описанные автором, то можно согласиться с общим месседжем: код на Kotlin-е становится вцелом менее понятный, чем на Java, особенно при наличии определенного знакомства с языком. При первом знакомстве все пишут как на Java, поэтому все достаточно просто. Но узнав немного язык, разработчик начинает теряться в возможностях, код становится более плотным и лаконичным, но менее структурированным. В частности постоянно возникают вопросы:


  • Один класс на файл или несколько классов в файле?
  • Как правильно использовать пакеты?
  • Когда использовать статические определения вне классов, а когда делать их в компаньонах?
  • Использовать экстеншн-методы, или методы класса?
  • Делать ли инициализацию полей при объявлении, или выносить отдельно в init{} — блок? И вообще в Котлине нет четкой границы между определением и поведением. При определении поля можно его и проинициализировать объектом, и сконфигурировать при помощи .apply(), и там же навесить хендлеров и листенеров. Структура класса превращается в кашу.
  • Нагромоздить однострочник или разбить все по действиям?
  • Для операции с объектом переопределить сеттер на свойство или создать отдельный вменяемый метод?
  • Общие рекомендации по стилю.

Я бы не сказал, что это проблемы.


  1. Уже не помню с какой версии давно как летает.
  2. Так никто и не заставляет ее использовать. Даже в .gwt.xml отключается. Обычно хватало GwtQuery. Есть Elemental. Есть форк реакта под GWT.
  3. Он и сейчас кривой. В J2CL его так и оставили. Основная проблема — нет dynamic types, то есть возможности напрямую общаться с JS-объектами.
  4. Это все же к области экзотики. Пишите сервер на Scala/Kotlin, а API и фронт на Java.

Настоящие проблемы, которые точно также остались у J2CL:


  1. Отсутствие динамического доступа в JS: window.getObject("JSON").invoke("stringify", myjs) как GwtQuery. Писать типизированные обертки для каждого типа напрягает. Есть же JavaScriptObject, но нету методов для динамического доступа — идиотизм.
  2. Отсутствие интеграции с Node и экосистемой. Че бы J2CL не пускаться как Babel plugin?
  3. Как бы есть Definetly Typed. Вон в Kotline написали-таки тулзу ts2kt для генерации тайп враперов.
  4. Java плохой язык для декларативного программирования и DSL-ей. В нем фактически отсутствует возможность читабельно написать иерархические структуры, такие как DOM. Как выход либо прикручивается сторонний темплейтер с кучей не type-safe байндингов, либо структура городится императивно.
  5. В Java много специфической семантики, отличной от JS. С одной стороны мы хотим сделать по стандарту, как в GWT, с другой мы хотим сделать язык интероперабельным, и придется ей жертвовать, как в JSweet.

Согласен, хорошие примеры.
Абсурдность метрик состоит в том, что статистической выборки просто недостаточно для того, чтобы вообще о чем-то можно было говорить. Ну какие выводы можно сделать по 100 коммитам или issues в трекере? Если посчитать доверительную вероятность, то можно смело заявить, что это все ни о чем. Тем не менее многие дяди с важным видом рассматривают месячные отчеты с красивыми графичками, которые они называют метриками, рассуждая почему произошел тот или иной скачок.


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

Information

Rating
3,873-rd
Location
Madrid, Испания
Registered
Activity