Місце ORM у загальній архітектурі застосунку
Колись на співбесіді мене спитали: «А як ти вчишся?». І нічого іншого, як відповісти «На практиці», я не придумав. У цій статті хочу поділитись певним своїм практичним досвідом і сподіваюсь прочитати про схожий досвід від колег.
tldr
Ця стаття — спроба порефлексувати про місце ORM у загальній архітектурі програмного застосунку. Стандартний варіант — інжектити екземпляр об’єкта, сконструйованого ORM-класом, у бізнес-логіку та оперувати ним. Альтернатива — зробити жорстке розділення рівня сховища та об’єкта сутності через абстракцію — Datagateway. Якшо ти на реальному продакшн-проєкті з таким заморочувався — поділись досвідом у коментарях.
Два підходи до архітектури
Отже, уявімо що ми починаємо будувати пет-проєкт. Застосунок для реєстру безпритульних собак. Що в нас є? Два епіка, 75 юзерсторі, пʼять розпланованих спринтів, скрам-мастер і гора ідей, концепцій, гіпотез усіх кольорів... А фактично ми або починаємо малювати таблиці бази даних з полями, або розписувати, хто які дії може виконати в застосунку. І вже далі від цього продовжуємо розробку. І зрештою отримуємо два різні підходи:
- схема даних диктує, як ми зможемо прописувати логіку;
- логіка диктує, як і що ми можемо зберегти.
Звісно, життя не двокольорове, воно є градієнтом сірого. Але від фокусу (концентрації) на одному або іншому підході не втекти. Думаю, всі знають про MVС, де model йде першою, тому з неї і починають. Не знаю, чому зараз учать в університетах, але я пам’ятаю час, коли для MVС-підходу взагалі не було альтернатив. І тому не було місця і для роздумів про ORM.
- Створити базу.
- Через ORM отримати доступ до даних у зручному вигляді.
- Проманіпулювати даними в контролері.
- Показати результат у представленні.
- ???
- Profit.
То що там з альтернативами у вашому
- Описати об’єкти.
- Описати, як ними маніпулювати, щоб отримати користь.
- Підключити джерела отримання / відправлення даних об’єктів.
- ???
- Profit.
Якшо там profit і там profit, то яка різниця? У першому випадку в нас ORM відповідає за трансфер об’єкту в дані, а в іншому — datagateway. Хоча що одне абстракція, що друге абстракція, і взагалі ми не бул б інженерами, якби не намагалися все переускладнити.
Що за ORM, про що взагалі тут мовиться
ORM — object relational mapper. Приблуда, яку придумали ліниві розробники, які не хотіли вчити SQL і писати: Insert into dogs (status, size, color, lat, lon) values (‘wild’, ‘small’, ’чорне’, 1, 1) а хотіли просто зберегти безпритульного собачку в базу: new Dog({ status:‘wild’, size: 'small',color:’чорне’, lat:1, lon:1 }).save() І в цей момент у нас з’явився дуплет: бібліотека для роботи з базою даних і бібліотека для зручної роботи з базою даних. Наприклад, Mongo Driver та Mongoose (для SQL теж щось знайдеться в гуглі). І тепер джуна можна ганяти не однією, а двома бібліотеками на співбесіді. Хочу зазначити, що в цій статті я не засуджую використання ORM: той факт, що вони існують, масово використовуються та розвиваються, означає, що їхнє існування необхідне. Питання: як їх застосовувати і коли? І якщо свою розробку вести від даних, структура таблиці диктує, які поля може мати об’єкт собачки. То екземпляр ORM проникає в усі файли, де йдеться про собачок. І ніяких проблем у цьому нема. Хочеш внести зміну — онови базу й користуйся новим об’єктом. А можна зробити тролейбус із хліба?
Datagatway
А якщо я веду розробку від бізнес-логіки? То мене не цікавить, як саме імплементовано рівень сховища (persistence layer). Я ще не планував його, я не знаю, які мені треба таблиці. Я знаю, що в мене є собачки, телеграм і бажання їх реєструвати та показувати в результаті пошукових запитів. Отже, мені треба абстрагуватися від сховища, ввівши сутність, що буде робити лише затребувані дії зі сховищем у бізнес-логіці, інкапсулювавши в собі реальну реалізацію цих дій. І що важливо: не буде давати доступу до всього, що логічно пов’язано із цим сховищем. Описано складно, а в коді це може виглядати так:
Class Storage{
getDogs():Dog[]{ .... }
saveDog(dog:Dog):Dog{....}
updateDog(dog:Dog):Dog{....}
deleteDog(dog:Dog):void{.....}
}Class Dog{
constructor({ status?:PET_STATE, size:PET_SIZE, color:PET_COLOR, location:[number,number] })
}Dog — об’єкт представляє сутність собачки, його структура не залежить від структури таблиці. Ми описали його незалежно. Усе, що стосується того, як і де він може зберігатися, описано в Storage. І саме це є тим жорстким розділенням, про яке я згадував на самому початку. З одного боку можна сказати: «Пффф, раніше я писав dog.save(), а тепер буду storage.saveDog(dog). Навіщось додав ще один клас для дублювання CRUD». Але зупинись і подумай, що ти втрачаєш і що набуваєш, коли в об’єктів пропадає .save()? Так взагалі можна щось напрограмувати? У рамках скоупу пет-проєкту практика показує, що можна. А на продакшні? Та хто ж тобі дозволить. Ти мені про це розкажи, пробував щось подібне? А поки я приведу інсайди, які отримав на практиці.
Тестування
Як на мене, головна перевага підходу з datagatway. Відокремлення джерела отримання об’єкта означає, що цих джерел може існувати нескінченна кількість і вони можуть мати будь-які реалізації. Також це означає, що об’єкт не несе в собі багаж структур, повʼязаних із цим сховищем. Навіть якщо ти майстерно використовуєш dependecy injection — мокати окремий клас легше, ніж цілу ORM-бібліотеку з її інстансем. Приклад юніт-тесту з моком інстансу монгуса я наводити не буду, бо не хочу вчити поганому (їхав any через any бачить any разом з any). А ось тест через реалізацію з datagatway. Я спеціально скопіпастив складніший тест. Зверни увагу, ми імпортуємо реальні об’єкти, а мокаємо лише «зовнішній» для нашої бізнес-логіки storage. GramyDriver — тут інтерактор, кейс — тест його реакції на dog-delete callback. У тесті ми визначили об’єкти, інтеракції яких ми тестуємо. Перезадали (замокали) шляхи їх отримання. Та після виклику методу processMessage, що тестуємо, перевірили результат.
import dialogues from "../../index"
import { GrammyDriver, GrammyDriverOptions } from "@drivers/telegram/grammy"
import { Chat } from "@entities/chat"
import { User } from "@entities/user"
import { Dog } from "@entities/dog"
jest.mock("@drivers/storage")const mockStorageDriver = StorageDriver as jest.MockedClass<typeof StorageDriver> const mockStorageDriverImpl = mockStorageDriver.prototype
const constructorOptions: GrammyDriverOptions = {
storage: mockStorageDriverImpl,
routers: dialogues
}describe("dog delete", () => {
it("should react on dog-delete-yes callback", async () => {
const dog = new Dog({
location: [1, 2],
size: "size-s",
color: ["біле", "сіре"],
furType: "short",
ageGroup: "adult"
})
const tempChat = new Chat({
chatID: 100,
dialogueStep: ["dog", "delete"],
stepData: {
dog: dog.toDTO()
}
})
const tempUser = new User({ telegramID: 100, pets: [dog.id] })mockStorageDriverImpl.getChatBy.mockResolvedValue(tempChat) mockStorageDriverImpl.getUserBy.mockResolvedValue(tempUser)
const grammyDriver = new GrammyDriver(constructorOptions)
await grammyDriver.processMessage(createCallbackQuery("dog-delete-yes")) expect(mockStorageDriverImpl.saveChat).toHaveBeenNthCalledWith(1, expect.objectContaining({ dialogueStep: [], stepData: {} }))
expect(mockStorageDriverImpl.dellPet).toHaveBeenNthCalledWith(1, dog.id)
})
})Строга типізація дивиться на цей код ніжним поглядом. Другим бонусом розділення є те, що можна тестувати імплементацію Storage без бізнес-логіки. Юніт-тести стають ну справді юніт.
describe("dellPet", () => {
it("should delete pet and updated all users who has it in pets", async () => {
const dog = new Dog()
const otherID = Dog.makeObjectId()
const user = new User({ pets: [dog.id, otherID] }) await mongoDriver.rawDB.collection(mongoDriver.coll.pets).insertMany([dogToMongoMapper(dog)], { ignoreUndefined: true })
await mongoDriver.rawDB.collection(mongoDriver.coll.users).insertMany([userToMongoMapper(user)], { ignoreUndefined: true })await mongoDriver.dellPet(dog.id)
expect(await mongoDriver.rawDB.collection(mongoDriver.coll.pets).estimatedDocumentCount()).toEqual(0)
const userFromDb = await mongoDriver.rawDB.collection(mongoDriver.coll.users).findOne({ _id: user.id })
expect(userFromDb.pets).toEqual([otherID])
})
})Тут я використовую бібліотеку testcontainers, яка піднімає реальний інстанс монги, з яким взаємодіє мій класс datagateway — mongoDriver. Ніяких моків, лише жорстка реальність. Еее, а де ORM? Майоре Пейн, я не бачу ORM?!!!! Ну вона є. Чесно-чесно. Просто вона десь усередині реалізації datagateway. Загалом нам все одно, хто і як перекладає дані з властивостей об’єкту Dog у поля документу в монзі. Працює — не чіпай.
High cohesion low coupling
Фраза, яку сивочолі архітектори бачать у вологих снах. Куди вже далі зчеплення (coupling) може бути слабшим за відсутність .save() у об’єкта? Змінюй бізнес-логіку, як хочеш, вводь властивості, які взагалі не залежать від бази даних. Тестуй бізнес-логіку, коли частини застосунку взагалі ще нема (якщо таке тобі треба. А чи треба?). А що там зі згуртованістю (cohesion)? Datagateway чітко описує всі можливі сценарії використання сутностей, погрупуй їх гарно, й ось тобі — self descriptive code. Хоча згуртованість може перерости всі межі здорового глузду та перетворитись у щось таке:
export interface IStorage {
getUserBy(filter: Partial<Pick<IUser, "id" | "telegramID">>): Promise<IUser>
saveUser(user: IUser): Promise<void>
getChatBy(filter: Partial<Pick<IChat, "id" | "chatID">>): Promise<IChat>
saveChat(chat: IChat): Promise<void>
getDogBy(filter: {id?:IDog["id"], avatarFileID?:string}): Promise<IDog>
saveDog(dog: IDog): Promise<void>
dellPet(id: IPet["id"]): Promise<void>
getPetsBy(filter: { id?: IPet["id"][], type?: PetTypes, near?: { coordinates: [number, number], maxDistance: number } }): Promise<Array<IDog>>
listPetsBy(params: {
filter?: {
near?: { coordinates: [number, number], maxDistance: number },
status?: PetState,
ownedBy?: ObjectId,
isValidated?: boolean,
},
limit?: number,
next?: ObjectId
}): Promise<{ list: IDog[], next?: ObjectId }>,
listClosetPets (near: { coordinates: [number, number], maxDistance: number }, limit?:number, page?:number): Promise<{list:IDog[], total:number}>,
listForValidation(params?: { next?: ObjectId, limit?: number, id?: ObjectId }, withTotal?: boolean): Promise<{ list: { pet: IDog, owners: { telegramID: number }[] }[], total?: number, next?: ObjectId }>,
markPetValidated(id: IPet["id"]): Promise<void>,
markPetAdoptionValidated(pet: IPet): Promise<void>,
countNotValidatedPets (ids: ObjectId[]): Promise<number>,
searchDuplicatesDog(dogData: {
location: IDog["location"],
size: IDog["size"],
color: IDog["color"],
furType: IDog["furType"],
ageGroup: IDog["ageGroup"],
}): Promise<IDog[]>
getDBStatistics():Promise<{
notValidatedPets:number,
totalPets:number,
totalPetsWild:number,
adoptedPetsToday:number
registeredPetsToday:number,
totalUsers:number,
registeredUsersToday:number,
chatsToday:number
}>
}Зоопарк методів отримання та збереження даних. Та й опис загальної схеми ORM теж може виглядати жахаючи, або ні. У IStorage-інтерфейсі також можна помітити ще один антипатерн. Методи markPetValidated, markPetAdoptionValidated — це можна вважати розмиттям бізнес-логіки за межі інтерактора у сферу бази даних. Cohesion уже не така й high. Але тут це легко помітити, усю кодову базу перечитувати не треба.
Abstraction leak
Це теж трохи задроцька тема. З підходом використання ORM вона зазвичай інжектиться та протікає всюди. Це проявляється в імпортах чи прописуванні інтерфейсів навіть там, де нам взагалі не треба зберігати чи отримувати об’єкт. Але якщо це його невід’ємна частина — нікуди дітися. Datagateway, як дамба, стоїть і блокує собою все, що належить до рівня сховища. Але як і з будь якою додатковою абстракцією, її треба запрограмувати. А це вже не те саме, що просто зробити імпорт.
Flexibility
Як на мене, слабкий аргумент, але він є. Межа абстракції між бізнес-логікою та сховищем дозволяє легше змінювати все, що стосується сховища. Хрестоматійний приклад — використовуйте два різних гейтвея, що підтримують дві різні бази даних, але мають ідентичні інтерфейси. Виходить, що така надскладна операція, як зміна бази даних у проєкті, зводиться до переписування і тестування одної бібліотеки. Але як часто у реальному житті ти міняв базу на ходу? Зробити можна, але робити ніхто не буде.
Сковування сутностями
Datagateway повертає не дані (LOL), він повертає сутності, якими оперує інтерактор. Ніяких тобі частин собачок, чи просто масиву якихось полів. Завжди якусь визначену сутність: Dog, User, Chat. Але ж ми орієнтуємося на бізнес-логіку, тому ці сутності в нас уже створені. А якщо ні, то спочатку доведеться добре подумати над тим, чим ти оперуєш, а вже потім, — як це зберігати. Теоретично такий підхід зробить загальній дизайн застосунку кращим, дозволить виявити проблему на найперших етапах продумування реалізації бізнес-логіки. Або ні, і знову буде якесь нечитабельне месиво, наприклад type PetWithOwners={pet:IDOG, owners:{telegramID:number[]}}.
А що там з оптимізацією
Це гримучий коктейль з розмиття бізнес-логіки та сковування сутностями. За прямого доступу до ORM ми обмежені лише уважністю код-ревʼю колег. Там, де нам треба, можемо робити супероптимізований SQL-запит, який буде використаний лише раз, тут і зараз. Благо ORM дає нам доступ до найприхованіших методів драйверу бази даних. Якшо треба пʼять джойнів — ну, стадіон так стадіон. В методах datagatewey нам теж ніхто не забороняє зліпити агрегацію на 10 кроків. Але тут уже доведеться подумати про визначення вхідних параметрів методу, узгодити, щоб він зрештою повернув якісь визначенні сутності. Міністерство швидких рішень таке не підпише.
Отже
Остаточної відповіді нема. Хотілося б почитати ще PRO-CON від тебе. Від себе скажу, що якщо доведеться проєктувати застосунок від бізнес-логіки, то як це зробити просто без datagateway — я не знаю.
Не прихований продакт-плейсмент
Якщо хочеш спробувати, як працює написаний за такою схемою застосунок, ось посилання. А якщо ти знаєш тих, кому такий інструмент може знадобитися, — поділись цим посиланням з ними. Зима вже на порозі.
93 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарівПочну ще один тред. Бо в обговореннях куча теорії та маловато конкретики. А до кучі ще й вагон різноманітних термінів які чи то синоніми чи то ні.
На вашому реальному досвіді скільки було проєктів де доступ до бази був через жорску абстракцію без протікання ORM?
В мене 2.
Бо теорію всі знають та статті читають. А на практиці часто виходять шо як навчились MVC штампувати то так все життя і штампують.
От тобі непогана ввідна стаття з конкретикою www.baeldung.com/...l-architecture-ddd-spring. Чітко виділений домен, окремо інтерфейс до БД, місце ORM — там, де ти пропонуєш. Все гарно, хоба — монгу на касандру поміняли, взагалі вогонь!
А у мене питання — що, і на SQL сховище можна перейти? Та, блін, очевидно, що так — просто репозиторій інший підставити!
А питання, власне, те саме. Там в ордері є колекція
public class Order { private UUID id; private OrderStatus status; private List<OrderItem> orderItems; .... }Хто і в який момент її завантажувати буде? Зразу, незалежно від того, що буде далі?
private Order getOrder(UUID id) { return orderRepository .findById(id) .orElseThrow(RuntimeException::new); }А з перформансом тоді що?
З NoSql-то такої проблеми нема, можна читати Order з усіми деталями за раз
Повна шиза:
Це просто квітуча шизофазія, вирішення неіснуючих проблем.
Двукратна імплементація репозиторія боже яка діч.
І сам дизайн доменних об’єктів до речі теж гівняний.
Привіт, Дмитро
Не до того прискіпуєшся, дозволь я поясню.
Не перший раз чую навколо і на ДОУ зокрема дві пов’язані думки:
1) домен має бути відв’язаний від персістанса. ДДД, гексональна архітектура, оце все. Власне ТС про це саме питає.
2) якщо персістанс робиться окремо, то чи потрібен там взагалі ОРМ (знову ж таки, що таке ОРМ? Який ОРМ? Hibernate та MyBatis — це, мяко кажучи, різні речі)
Стосовно першої — я в неї тупо практично не вірю. Не буває такого ідеального домену, складнішого за хелло-ворд, якому що в SQL, що в S3, що в графовій БД зберігатися — однаково. Ти гарно висловився, я можу тільки ППКС. Але от стаття на baeldung — як раз про це. Коментарі там, нажаль, закриті, а так я би спитав: а ну перейдіть з NoSQL на SQL, подивимось на перформанс eager-loading
orderItems. Чому eager-loading? Ну, а який, домен-то відокремлений.Якщо припустити, що репозиторій окремо від домена, можна починати спеціальну олімпіаду «Hibernate тармазіт і ваабще ні нужєн».
Ці думки, строго кажучи, різні, але я їх об’єдную в один підхід, тому що в мене (і в тебе, здається, теж) — так, домен прибитий до persistance, але я від цього не страждаю, а насолоджуюсь.
Тим не менш, щоб був не просто срач, а якийсь конструктив, я не полінувався написати приклад рекурсивного lazy-loading-у і всіх їм дістаю, щоб показали мені світло істини.
ТС, молодець, не посоромився, високовчені архітектори і прирівнені до них — до таких примітивних кодерських задач не опускаються.
І тобі привіт )
Звичайно прибитий )
Лейзі-вс-ігер лоадінг взагалі бездонна тема.
В окремих випадках я взагалі розв’язую один віж одного об’єкти і підвантажую їх зв’язки окреми селектами типу ListChild findAllWhereParentIn(ListParent)
а потім уже в пам’яті дроблю загальних ліст чайлдів на мапу
Прочитав статтю і схоже, щось не зрозумів. Чим це відрізняється від Spring Data?
В мене викристалізовалось питання до тих хто ще читає цю статтю.
Чистий проект.
Треба прицняти рішення зразу, використовувати протікання ORM чи починати писати абстракцію Datagateway\Data Access Layer. Скоуп проєкту взагалі не зрозумілий. Не ясно окупляться чи ні затрати часу на написання Datagateway.
З закритими очима, який підхід обиреш?
залежить від домену.
В якому домені робив би, в якому ні?
Чим більше схоже просто на CRUD — тим більш ORM доречний
Чим більше відбувається перекладання даних між таблицями, більш довгі flow, перерахунки чогось, потреба в агрегованих даних, пахне event-driven тим більш доречний Datagateway\Data Access Layer
Умовно приклад:
Пишемо CRM — ORM доречний
Пишемо складську систему з елементами бухгалтерського і податкового обліку та інтеграціями з зовнішніми системами — Datagateway\Data Access Layer
На мусорке. Это уже больше 10 лет обсуждают и ORM может работать для каких-нибудь CRUDов, но не более
А шо на замість?
Data Access Layer, написанные с любовью SQL запросы, PODы
І як побороти перевантаження Data Access Layer навалою різних методів типу:
getTask
getTaskWithSubs
getTaskForAutocompit
getAggergatedTask
.....
і ТД.
мож знаєшь гарні статті про best-practices як його структурвати?
шо за акронім?
Разбивкой на подфункции которые из строки выгребают нужные данные:
getTaskHeaders
getTaskSubs
getTaskAutocompitInfo
и т.д.
А потом уже в публичных методах вызываешь нужную комбинацию над результатами запроса.
Plain Old Data, структура без методов, без геттеров-сеттеров и т.д. и т.п.
Скажите, пожалуйста, а с update (в широком смысле слова) как быть?
Если позволите, скопирую из соседнего треда.
Для мене основна супер-сила ORM Hibernate не в селектах, а в апдейтах, тобто
@Transactional class Service { public void someMethod(Changes changes) { CompexEntity compexEntity = repository.findById(id).orElseThrow(); complexEntity.updateBy(changes); } }CompexEntityможе бути яким завгодно складним об’єктом з внутрішніми колекціями і колекціями колекцій. В update(...) може відбуватись складна логіка, яка потребує (або ні) дозавантажання внутрішніх пропертей, створює або видаляє нові елементи тощо. Тестується це все добро unit-тестом без жодних контекстів, а решту робить ORM.А тестировать как
этот процедурный стиль? Моками?Как раз апдейты — самое слабое место в ОРМах.
Ну когда надо проапдейтить несколько таблиц, с разным набором блокировок.
а с чего он процедурный?
для
используется свой или какой-то QueryBuilder. В котором что методы, что их аргументы приближены к домену, а не к схеме базы данных.
и нафик они нужны, когда имеется сложная работа с персистентными данными в сложной схеме.
и делает это — плохо. без дополнительных танцев в обход ORMа.
для MVP только и хорош, и для книжек для начинающих.
Расскажите, пожалуйста, подробнее, какую проблему решаете «разным набором блокировок»? Write Skew?
Ок. А как и чем вы апдейты делаете?
Ок. А как вы бизнес-логику тестируете?
Если проект под страшным NDA, могу предложить свою задачу.
Есть таски с сабтасками и сабтасками сабтасок, иерархия не ограничена.
Статусы: new -> in progress -> done
Юзкейсы: в done можно переводить, только если сабтаски закрыты.
Но с force=true можно перевести в done весь подграф.
Проблемы юзкейсов: сложно отделить DAO от бизнес-логики.
Без всяких блокировок.
Вот моё решение, вот подход ТС-а, который я называю процедурным, вот моя критика этого подхода. Расскажите своё видиние, пожалуйста, поделимся опытом.
С вашего позволения, процитирую прямо из профиля с минимальными изменениями.
«Топовый способ свести дискуссию о программировании к демагогическому и догматическому холивору — ввернуть аргумент о „
(не)продуманной архитектурепригодности только для MVP и начинающих“. Произносить надо спридыханиемнебрежным презрением, и можно смело предавать анафеме сомневающегося. Сам же за умного сойдешь.»Постоянную, когда есть конкурентный доступ.
Кодом ессно. ОРМы тоже им, кодом делают :)
Функциональными тестами, ессно.
О каком из проектов что делал речь?
Давно говорю
На задаче вывода «Hello world!» никак не показать необходимость в слоенной архитектуре, паттернах ООП и т.п.
Потому что для такой примитивной задачи что вы описали — можно обойтись парой php скриптов, и третий для PHPunit,
Хотя, парой хранимок — тоже можно.
На кой черт для такой примитивной задачи что вы описали вообще нужны эти ормы, дата лаеры, и прочая астронавтика?
Понимаете в чем проблема?
На мелком масштабе и задачки с литкода — невозможно показать решение проблем, которых там не существует, в силу — малого масштаба.
Рассказал выше. В обсуждении способа вывода Hello world — пусть джуны делятся опытом.
начинающим копать тему об ОРМах давно рекомендую с статьи
«Вьетнам компьютерной науки».
Чтобы можно было продвинуться в обсуждении еще более сложных фундаментальных вопросов в ней не упомянутых.
Все мы начинали, с Hello world’ов. И постоянно:
Снова весна.
Приходит новая глупость.
Старой на смену.
(Исса)
И каждый год, даже на ДОУ, уже второе десятилетие очарованные ОРМ делятся опытом как классно выводить им «Hello world».
Я же давно сталкиваюсь с необходимостью обходить ОРМы, лезть им в кишки, реорганизовывать код, и т.п.
И, не только я. На ДОУ таких тем уже было полно, и постоянно об этом и речь.
Да и в инете. Более общее название этого холивара между начинающими и битыми — Rich model vs Anemic model
Ну, это не ответ. Вы б ещё написали — update-ом.
Я предложил, например, подход
Предложите какой-то другой, пожалуйста.
Именно необходимость, конечно, не показать, но проиллюстрировать любой подход, мне кажется, можно и на простом примере.
Конечно, её можно решить по-разному. И посравнивать подходы. Конечно, на примитивной задаче о преимуществах какого-либо подхода судить невозможно, плюсы-минусы проявляются в масштабе.
Поэтому с
... не согласен. Где-то в закладках валялась (я не Ops, поэтому не пригодилась) статья про три микросервиса, с helm-ами, K8S, horizonal scaling и прочим, два из которых передавали единицы третьему, который их суммировал. 1+1 = 2, ага.
В коментариях благодарили за готовый шаблон для развёртывания. Это я к тому, что для сложения двух чисел три микросервиса вряд ли нужны. Но показать, как оно может выглядеть в микросервисной архитектуре — можно.
Извините, пока слышу только общие рассуждения.
Это интересно, спасибо, наскоро глянул, хотя, конечно, аналогия, конечно, ух!
Покажите, пожалуйста, хоть одну статью. А то у меня ощущение, что за ОРМ топлю я один, остальные только негативно отзываются.
Я, хотя и начал с признания, что являюсь фанатом Hibernate, прекрасно понимаю сложность и невозможность 100% object-relational-mapping, и могу поиграть и в противника технологии.
Поэтому и интересны проблемы, альтернативы и подходы.
Вот вам в каких случаях приходится обходить ОРМ, реорганизовывать код и т.п. Можете поделиться примером проблемы, пожалуйста?
так ваш пример — им и делает.
Прикалываетесь? ;)
Ваше предложение звучит примерно так:
Я вывожу «Hello world»
System.out.println("Hello World!«);
А как выводите вы?
А мне кажется что:
В простых системах нет сложных проблем, чтобы на них продемонстрировать методы решения сложных проблем.
Да, мой метод давно во всем — от общего к частному.
Последнее обсуждение что участвовал было где-то в этом году.
Не :) За ОРМ топит большинство. Потому что это «эффект учебника».
В учебниках рассматриваются прежде всего, чаще всего создание CRUD приложений. Потому что они проще. А для них использование ORMа — вполне ок.
Проектирование более сложных приложений рассматривается гораздо реже. Потому что само изложение проблем — больше. Авторов желающих и могущих описывать — меньше. у них потому что на своих проектах куча работы, некогда им много писать. Спрос на такие книги — тоже меньше, особо и не продашь.
Это примерно как с популярной литературой и профессиональной. Первой издается гораздо больше.
Самый частый, сразу, с порога — SELECT N+1
С коробки есть упомянутые eager/lazy
Но — поможет в простых — CRUD системах.
Если же вложенность коллекций объектов глубже, если поля у доменных объектов — агрегированные значения, то — схему БД и работу с ней приходится проектировать вручную, а не как в учебниках:
описали классы, описали отношения, а схему сам ОРМ сделает.
Еще раз, для CRUD, или MVP версии — самое то. быстро и дешево. потому что о базе данных не думаем вообще. Некогда о ней думать, да и не нужно.
В том-то и проблема, что каждая догрузка пропертей — это проход в базу и тормоза.
Основная проблема ORM состоит в том, что база данных выгружает не объекты (данные + поведение), а только лишь данные.
Что именно тестировать?
Это не так, вот в меру своих скромных сил пытаюсь объяснить.
Бизнес-логику, вестимо.
А как по-другому?
Чтобы не перебрасываться общими фразами, предлагаю задачу-иллюстрацию, копи-паст из соседнего треда.
Есть таски с сабтасками и сабтасками сабтасок, иерархия не ограничена.
Статусы: new -> in progress -> done
Юзкейсы: в done можно переводить, только если сабтаски закрыты.
Но с force=true можно перевести в done весь подграф.
Проблемы юзкейсов: сложно отделить DAO от бизнес-логики.
Вот моё решение, вот подход ТС-а, который я называю процедурным, вот моя критика этого подхода. Расскажите своё видиние, пожалуйста, поделимся опытом.
Обратный порядок загрузки.
Потому что для БД удобней такой, а не тот который получается когда мы юзаем ОРМ по вводному гайду по нем.
И у ОРМа — нет возможности построить оптимальный порядок загрузки, без специальных усилий (которые обычно — в обход ОРМа, до полного его удаления из проекта)
Не то чтобы я не понимаю, о чём вы говорите, но давайте, пожалуйста, на конкретном примере.
Почему в сложных проектах доменная модель и персистентная модель — не тождественны?
...On an even higher level of abstraction, ORM systems, which isolate object-oriented code from the implementation of object persistence using a relational database, still force the programmer to think in terms of databases, tables, and native SQL queries as soon as performance of ORM-generated queries becomes a concern.
en.wikipedia.org/wiki/Leaky_abstraction
Пишем в слое бизнес логики
foreach(x of X) doV(x)
Часто оно неявно будет описано. А есть объекты у которых есть коллекции объектов, а у тех — свои коллекции.
И есть действие — когда нужно для каждого объекта коллекции нужно выполнить действие, которое приведет к вложенным обходам коллекций.
У ОРМа, без специальных ухищрений нет возможности предсказать загрузку самых глубоких данных.
Далее, сама эта загрузка глубоко вложенных данных зависит от doV. Для doW она должна быть другой, например потому что будут срабатывать другие ограничения бизнес-правил.
Сохранение данных, нередко тоже нужно перевести в Bulk режим, а не дергать save (у каждого/для каждого) объекта.
И когда таких «пояснений» ОРМу становится больше какого-то разумного количества — и возникает вопрос — а только в миграциях в БД от него и польза.
Попробую тут всё собрать.
Кстати, характерная оговорка, не
x.doV(), аdoV(x). Ну, это так к слову, не начинать OOP vs PP vs FP )Именно это я считаю киллер-фичей Hibernate.
Имеено это я имел в виду, когда писал
complexEntity.updateBy(changes);Это не sql update, а неизвестно какая логика.Именно об этом мой пример. Вот этот рекурсивный обход вложенных коллекций.
Именно так. И Hibernate позволяет сосредоточиться на логике, предполагая неявную дозагрузку данных, если они нужны.
Вряд ли база ляжет от 5 * 300 = 1500 простых
select/udpate... where id = ...;И да, задачу типа «в департаменте Х поднять всем з/п на 10%» буду по возможности делать именно в стиле
полностью осознавая минусы этого подхода, но в моей сфере плюсы перевешивают. Когда я шучу про кровавый энтерпрайз, то вкладываю, естессно, своё субъективное ощущение — сложная логика, которая постоянно меняется, а объёмы данных и нагрузка — сравнительно невелики. Если хотите померяться цифрами — 10^6 пользователей, 10^2 req/s — можно считать MVP для новичков.Так вот, главный плюс — это инкапсуляция и консистентность данных. Это сейчас кажется, что поднять з/п на 10% — это
з/п *= 1.1. А потом начнутся нюансы округления, налоговые классы, граничные значения, кол-во детей, алименты и т.д.Второй плюс — это внезапный аудит, который либо уже есть, либо обязательно будет.
Какие тут могут быть альтернативы? Очевидно
update employee set salary = salary * 1.1 where department_id = x;или сразуcall sp_salary_update(10);Или что вы предлагаете?
Как я предполагаю, у вас другой энтерпрайз, настоящий.
Конечно, если начинается логика построенная на агрегированных значениях ORM не подходит.
Расскажите, если желаете, о проекте. Что делаете, какими фрейморками, какие нагрузки?
Более того, я работал в одном, по-своему, отличном проекте, пришёл, говорю «ну, покажите мне базу, чем с ней работаете и т.д.», а базисты мне удивлённно «зачем это тебе? База закрыта, твоё дело данные сюда, в одну специально открытую stored procedure, класть». В общем, джава там была сугубо для интеграции — хттп-запросы, слушание очередей, парсинг всякого разного, а вся логика была в БД, ворочались какие-то многочасовые фиды и т.д. Базисты не лезли ко мне, а я — к ним, и всем было хорошо.
И боль, когда начали с ORM, потом немного подтюнили bulk-запросами, потом добавили SP и теперь логика размазана везде — я тоже понимаю.
Чем вас Hibernate так укусил, что вы в него везде плююете?
Давайте обсудим. Что не так с N+1? Какой контекст/какую задачу решаем?
да, разница между «в лоб» и «по лбу» — огромная
ну да, об этом же и речь — ОРМ — это киллер БД :)
а в жизни кроме идеалов логики есть ограничения физического мира.
ложится. даже нетворк внутри дата центра — ложится :)
а в куче других сфер — вообще все что вы пишите — и с доплатой не нужно :)
забавный тогда у вас энтерпрайз :D
Первая черта энтерпрайза — это система для офисных пользователей. Не обязатально, но обычно. 10^6 офисных пользователей в системе? хм... это что за ТНК такая, с таким количеством персонала, где просто столько офисных работников... и — 10^2 req/s — как-то маловато для такого количества работников.
Для чего, для вывода «Hello world»?
ну точно не тот смешной что вы описываете. Я такой и представить не могу, чтобы 10^6 пользователей, и никаких проблем с физичискими ограничениями вычислительной техники. а «чистая красота логики» :)
О как оказывается. а в энтрепрайзе значит такое — агрегированные значения редкость. всякие — остатки на начало периода. оборот за период. средняя цена, ...
Да. БД как сервер приложений в энтерпрайзе — частая история.
Да просто постоянно его курочить приходилось. Влезать в кишки и обходить. И подобные.
А так — классная штука :)
Это не ко мне. а к азбучным истинам работы с РСУБД.
У древовидных, типа MUMPS на которой тоже писал в двух разных компаниях такой проблемы нет. Там правда другие проблемы.
как из O(n) сделать O(1)
в общем виде:
то есть чтобы количество запросов было равно количеству foreach
а не сумме количества элементов во всех коллекциях.
Гм, надо попробовать тест написать, сколько мой M1, допустим, с Postgres-ом на дефолтных настройках выдерживает. Или поискать готовый.
Я когда с базистами начинал кеширование обсуждать, тема не встретила интереса, сказали, что с их точки зрения БД не загружена. Отчёты на отдельной реплике, естессно.
Конкретику, заметим, никакую не приводите.
Как говорится, и вам не хворать. Сферы разные бывают, да.
Да, для хелло ворд. Да, для задачи типа «в департаменте Х поднять всем з/п на 10%». Напишите хоть строчку псевдо-кода, а то флейм поднадоел немного.
Прям за моим именем написано где я работаю.
Я нигде не говорил, что ОРМ — это затычка на все случаи жизни. Но заходы «ОРМ на помойку» меня триггерят, как видите. Но вообще надо заканчивать, наверное, я уже отвёл душу.
Вы уже второй раз эту фразу пишите, а конкретную проблему не приводите.
Да.
Ответ тогда тоже в общем виде.
Никак.
Смотря для чего foreach.
Если для последующих апдейтов, то см. пример с поднятие з/п на 10%.
Если для селектов, агрегации и прочих репортов — это противоречие между самой сутью объектов и агрегатов из реляционных БД. Тогда просто отложить ORM в сторону (ну или максимум маппинг из него использовать). Не обязательно плевать в инструмент не подходящий для задачи.
Вот эти примеры из учебников, есть parents, есть children и N+1 проблема(!), а можно-то было
parents LEFT JOIN(!) childrenодним запросом сделать! — это ерунда на практике.Ещё могу multiset предложить, но у него очень ограниченный диапазон применимости.
в смысле вы предлагаете запостить интернет?
ее же, конкретики немеряно в инете.
какая именно нужна?
Ок. Итак, есть две таблички. Департаменты и сотрудники.
Колонка salary у сотрудников. Ну и увеличиваем ее.
Задача даже для литкода смешная, ниже категории easy.
Какие у вас проблемы с такой задачкой, что вам нужен ОРМ?
Rakuten Kobo?
У любой даже некрупной компании — куча софта.
У вас не написано каким именно ПО вы занимаетесь.
Может рабочим местом табельщиков, для ведения учета штатных офисных уборщиков.
Я лишен дара телепатии.
Я стопицот раз уже писал какую :)
За умение знать как — я и получаю деньги :)
Пофик для чего.
Вопрос обсуждается — сколько запросов будет сгенерировано в зависимости от «способа записи» бизнес алгоритма.
Плюют в него потому что декларируемые возможности — не подходят для задач для которых декларируются :)
Ну ок, тогда просто в разных вселенных живем.
Я в основном в мире финансово-экономического ПО, предназначенного для офисных пользователей. С 90ых.
Проблема постоянная. Меняются базы, ормы, были АРМы с GUI, теперь с WebUI — та же история. На разных ЯП. что на Дельфи, что на Джаве, что на Пыхе, сейчас на Ноде.
Немного был в сфере и для хоум юзверей, где тоже расчетная часть большая, плюс реалтайма немного. Та же проблема сразу в лоб. Или по лбу, да, есть нюанс между «в» и «по».
В вашей вселенной такой проблемы с РСУБД в энтерпрайзе нет.
Ок. Нет предмета дискуссии тогда.
Ваш личный опыт. По такому шаблону.
1. Решал такую-то задачу.
2. Взял Hibernate, сделал так, результат не устраивает.
3. Полез в кишки (что это значит, кстати? Вы патчили хибернейт?) и обходил (надо полагать, изменяли какую-то конфигурацию). Результат всё равно не устраивает, тесты падают. Надеюсь, вы в курсе, как, например, смотреть количество запросов, которое Hibernate генерит?
4. Вот ссылка на гитхаб с примером иллюстрирующем проблему, вот код, вот тесты. ЧЯДНТ?
Я тоже лишен. А вот код посмотреть могу.
Тогда будет техническая дискуссия, я люблю такие вопросы. Это, конечно, если вам нужен ответ, а не троллинг.
Вы же пишете «я — д’Артаньян, а Hibernate — кака, кто его трогает — фу». Что это за детская подначка была?
Да, именно так.
Вы не стесняйтесь написать строчку кода, пожалуйста, даже если это хелло-ворд. Тогда будет что обсудить. Я привёл три варианта решения, у каждого есть свои плюсы-минусы.2-ой потенциально хуже, я предпочитаю 1-ый.
1)
repo.findAllBy(department).forEach(e -> e.increaseSalaryByPercent(10));2)
update employee set salary = salary * 1.1 where department_id = x;3)
call sp_increase_salary(x, 10);Мой опыт говорит, что
Что вы предлагаете? Просто увеличить? Кодом? Код напишите-то или больше словами?
Я написал выше, но вы либо не читаете, либо не понимаете, либо просто троллите.
Отвечаю второй и последний раз (больше, кстати, для ТС-а, если он это сообщение увидит).
Основых — две. Инкапсуляция и аудит. Поскольку у меня нет уверенности, что мы одинаково понимаем эти термины, особенно первый, разъясню.
Это только кажется, что измение з/п — это
employee.salary = employee.salary + 10%. Либо потом добавится, либо сразу самоочевидно имелось в виду, что это только для тех, кто прошёл испытательный срок, а ещё нужно применить правила округления. Если вы работаете в финансово-экономическом мире, то представляете сколько там «если, при условии, но». Как, впрочем, и везде.Я предпочитаю всю эту логику по возможности держать в
increaseSalaryByPercent()методе.Дальше. Инкапсуляция — это прекрасно, но возникает вопрос «а как теперь отобразить изменения объекта employee в БД?». Это может быть сложный объект, с коллекциями и коллекциями коллекций, в которых что-то поменялось/добавилось/убавилось. На этот вопрос есть разные ответы, я выбираю Hibernate, чтобы не писать лишний код.
Теперь аудит. В моём мире либо сразу, либо, гораздо веселее, потом, когда уже много кода написано, бизнесу захочется смотреть изменения — а, собственно, кто и когда поменял з/п Васе Пупкину с какого значения на какое. В том числе из-за этого я предвзят к подходу
update employee set salary = salary * 1.1 where department_id = x;Опыт, к сожалению, не всегда приходит с возрастом. Бывает, что возраст приходит один.
Весь это зоопарк ничего не значит. Покажите код.
Вот вам пример как надо делать. Код, бенчмарки, выводы.
ну так отркройте туториал по интересующему вас вопросу.
там есть и код, и пояснения.
Или спросите чатгпт. У вас же нубские вопросы, они не требуют специального ответа.
Я не стесняюсь, а не понимаю какой смысл в копипасте кода с туториала?
Вы вот копируете. Нафига? Вы считаете что только вам доступны туториалы или что?
Знаете ли, у меня честный сертификат есть, полученный в уч центре 1С — по «Зарплата и Кадры Управление Персоналом».
Потому что когда был в 1Сном мире — у меня специализация была такая была.
Вы же поставили задачу не про зарплату. А нубскую:
Есть табличка в БД с полем которое надо увеличить.
Никакой «зарплаты» в вашей постановке задачи нет.
Это задачка на знание оператора UPDATE в SQL, если табличка в РСУБД. или аналогичного, если другой тип БД.
Это задача — на вывод строки «Hello world»
Тут нечего обсуждать. откуда у вас для такой задачки берется
я х.з.
в вашей задачке ничего о бизнес требованиях не указано.
Когда приведете хотя бы описание домена как бизнес-аналитика, тогда можно будет хотя бы к ТЗ перейти.
а кода конечно будет — такие комменты читать никто не будет.
простейший учет что пишут себе команды разработчиков, когда не хочется какому-нить Toggl платить, куда свои часы трекать чтобы билить — уже много кода будет.
Только нуб думает что он по куче кода — бытенько что-то поймет.
ну да, ну да, вы ставите задачки для трейни, и уверенно рассуждаете о чьем-то опыте :)
туториалы есть в гугле.
если вам самим сложно, например по :
www.postgresqltutorial.com
Ну и можете проверить себя потом на leetcode.com/studyplan/top-sql-50
Если нубу надо доказывать о проблеме SELECT N+1 — то ничего ему не надо доказывать. Он же даже гуглить не умеет.
Технической дискуссии, увы, не получилось, продолжаем обмениваться «любезностями». Но, сразу скажу, меня вряд ли хватит надолго, поэтому последнее слово будет за вами.
Знаете, я никогда не понимал и не одобрял насмешки над какими-либо языками программирования, считая, что суть одна, а на каком языке пишется, не так важно, хотя нюансы, конечно, есть. Но теперь мне начинает казаться, что шутки про бывших 1С-ников не на пустом месте появились.
Я так не говорил. Я говорил
Вы понимаете, что такое объекты? В контексте первой буквы из ORM? Инкапсуляция? Прямой доступ к полю? Сеттер? ООП? Понимаете, что строчка
update employee set salary = salary * 1.1 where department_id = x;... это 1) та самая Leaky Abstraction, про которую вы ссылку приводили? 2) нарушение инкапсуляции?
Понимаете, что Hibernate и leetcode.com/studyplan/top-sql-50... как бы это сказать... ортогональны немного? Хотя знание SQL безусловно полезно.
Напишите, пожалуйста, какую-то ответную колкость, но так, чтобы было видно, что вы понимаете. Не «вопрос нубский, вот ссылка на википедию».
Ну вот, указал задним числом. Нужен аудит. Как его сделать?
Вы реально в таком мире живёте, что пока аналитики всё не разжуют и не опишут, и ТЗ не составлено, кодить не начинаем?
Могу только усомниться и позавидовать.
У меня странное ощущение.
С одной стороны, вы вроде бы кинули неглупую статью про Вьетнам, цитата про Rich vs Anemic Model была уместна, в кишки Hibernate, говорите, лазили.
Я подумал, вы в теме, поделитесь опытом и нюансами.
С другой — вы же не свои приключения описываете, а ссылки кидаете, предлагаете гуглить, туториалы читать и т.д. Код не показываете.
Может вы просто где-то услышали «Hibernate, ORM, N+1, куча селектов, киллер БД, ко-ко-ко», а какие трейд-оффы за этим стоят и как в каких случаях решаются — не понимаете, потому что на практике не сталкивались?
Это точно не пранк? Я с ChatGPT разговариваю? Ну, повёлся, конечно, признаю.
общаясь с вами я понимаю что разделение на кодеров и програмистов — не шутка, а реалии и доныне.
А шутки про джавистов — да, тоже ходят. Особенно классные в среде функциональщиков. Одна из них даже в общий мемасик превратилась — «ООП головного мозга».
Я же упомянул — чтобы вы в своей телепатии как-то попустились. А то ишь ты, мне о домене зарплата решили рассказать :D
огласите весь список. интересно посмотреть как вы его всунете в форумный формат :)
А то что вы называете аудитом — делается кучей способов. И правильный — зависит от общей архитектуры системы.
пока только тупые задачки озвучиваете, уровня — если задали такую на собесе — стоит подумать — а не встать ли и уйти.
вообще-то я бывал в роли не только аналитика.
И видел и как — давайте код писать, ничерта не понимая к чему и зачем. И как без подписанного ТЗ — ни строчки.
Всякое видел.
«Если кто-то кого считает кретином — один из них точно кретин.
Возможно и оба»
Поделитесь, пожалуйста, идеями.
Ну вот, например, ТС сделал свой DataGateway, а к нему бизнес пришёл с таким вопросом.
Только, пожалуйста, без ожидания детального ТС на 100500 страниц. Всё на ваше усмотрение. Табличка со столбцами «кто, что, когда, старое_значение, новое_значение». Или сразу «нубский вопрос, сами гуглите», встаёте и уходите?
диапазон решений от
пишем все действия в лог файлик, и когда надо читаем
до
поднимаем какой-нить Drools который вообще провалидирует допустимость действия.
Идей — тьмы. А как надо то? покажите если не ТЗ, то хотя бы бизнес хотелки :)
Или найдите телепата, который догадается, сколько у вас фонового контекста к вопросу.
за Hello worldaми — идите в гугл. Там тьмы примеров, от небольших статей, до книг в сотни страниц с описанием как проектируется такое, для вашего (я х.з. какого) домена. Потому что в разных доменах — немного, а то и много разные бестпрактики.
Совершенно верно. Только нуб верует что существует какая-то Одна Идея, да еще такая что в 5 строчек ее можно описать.
Мало мальски опытный уже понимает что код все пишется и пишется, потому что контекст — меняет все.
и когда контекст требует — то и я вкручу самым тупым образом какой-нить ОРМ и глазом не моргну.
Причина догматизма у людей, обычно от скудости опыта. Черта юношества потому — максимализм и идеализм.
Ура! Таки началась техническая дискуссия, спасибо.
Табличка со столбцами «кто, что, когда, старое_значение, новое_значение».
Записывать сюда всё, что приложение меняет
в х.з. каком домене (сорри, слишком обще сформулировал)в других таблицах (лучше так сказать).Варианты — 1) требование такого аудита известно сразу 2) появилось, когда уже написано пол-приложения.
Поэтому и хочется понять нелюбовь и нападки — без всякого контекста — на Hibernate
Ок, если так надо, если у вас так записано, а чаще не записано а витает в воздухе «дев гайд» — так и делайте.
Я ж ничего не знаю о проекте где такое надо.
Такое может быть и лишним, если изменения осуществляются сущностями аля Документ.
1 — понятия не имею «как надо». Неизвестно мне — ни-че-го.
2 — еще хуже чем 1. Потому что к 1 добавляется — а как УЖЕ написано остальное. Потому что весьма желательно, чтобы реализация была в том же стиле. Даже если он такой себе. «Безобразно — но единообразно» лучше разнообразия подходов в реализации.
Я тоже не знаю «как надо». Но, навскидку, могу предложить несколько вариантов.
1) если там «ООП или ДДД головного мозга», то, может, летают какие-то ивенты про изменения. Ну, и записывать их в аудит табличку. Но это не звучит как надёжный подход
2) триггеры на базе. Бронебойное решение, единственно, не соображу как туда прокинуть «кто» (делал изменение)
3) раз это Java (вы полиглот, я на других языках не пишу), то там скорее всего Spring Boot c Spring Data JPA. И как бы неправильно его не использовали, если только без sql update-ов, то любые фантазии аудита решаются на раз.
Конечно, хуже. Потому что если Hibernate (или другой state ORM), то всё хорошо, см. пункт выше, а если нет — миллион SQL DML команд в коде — кроме триггеров ничего не могу придумать, чтобы развернуть
update employee set salary = salary * 1.1 where department_id = x;в отдельные аудит-записи по каждому человеку.Предложите, мне интересно.
ну, я самое главное не услышал — а зачем он нужен, аудит этот. какая его бизнес ценность.
без этого оценить «правильность» вариантов — тоже не берусь.
Они все правильные.
Как в дзен-притче:
Рынок
— А какой кусок мяса у вас лучший?
— У меня каждый кусок — лучший!
может. и тогда такой и тотальный аудит может получиться полным враньем. Потому что пользователь делал одно, а какая-то подсистема внесла изменения согласно бизнес логике о том, о чем этот пользователь вообще не в курсе. Просто по его должности, роли — это изменение вне зоны его ответственности. И тогда — такой «простой аудит» — в мусорку.
может. а может вообще заюзать возможности БД, и не писать ничего самому. Правда — думаю придется иметь тождественность пользователей системы и пользователей БД. Но — может оказаться выгодней такое решение.
и с sql update решаются. зависит от того как организовано размещение и использование SQL кода.
Если нудить, исходя из моих познаний домена зарплата — такое неверно в любом исполнении.
Минимум что обычно требуется для зарплатных алгоритмов — история изменений зарплаты.
Но, зависит от конкретных потребностей и бизнес требований.
Для примитивной системы расчета ЗП, уровня курсовой работы первых курсов сгодится вот так изменять зарплату.
да, и у сотрудника в его таблице не зарплата, а ставка, оклад, и прочая. если опять же — нудить.
Зарплата — это результат ее расчета :)
Ну, вот так бизнесу захотелось.
Делали
CRUD-приложение, со логикой изменения всякого разного, а потом бизнес такой «стоп, нужен учёт и контроль. Это значение, вроде бы, правильное, но надо понять, откуда оно здесь взялось, что было до этого, кто и когда».И да, и нет. Триггернул изменение все-таки этот пользователь, считаем, что он. Но бизнес может быть изобретателен в аудите, типа: поменялось значение в свойстве подствойства — но запишите как изменение парента.
Теоретически — да, это отвечает на вопрос «кто», практически, боюсь, малореально до невозможности.
Как?
Приведите, пример, пожалуйста.
Я не возражаю, это ж просто пример.
s/зарплата/оклад/gко всему, что было выше.я не понял что именно ему захотелось.
Если бы бизнес умел рефлексировать и формулировать свои желания — такая штука как «Сбор и управление требованиями» была бы бессмысленной
Ессно. Он всегда так говорит.
Эти слова ни о чем. затерты до потери смысла. Я не собираюсь гадать что именно имеет ввиду конкретный бизнес.
Я — спрашиваю бизнес об этом.
ну ок, значит так.
«Кораблю которому все равно куда плыть — любой ветер попутный»
Зависит от существующего кода.
Какой именно из ста пицот примеров с гитхаба?
Если отсутствуют критерии даже на уровне хотелок бизнеса, то какие у вас критерии к техническим требованиям отбора «примера»?
Ну да.
И я вам о том что список таких примеров — не влезет даже в статью для ДОУ. без кода. с кодом — книжечку можно написать, со всеми этими «просто примерами»
Свой. Не надо гитхаб. Я рассказываю свои решения, вы — свои.
Или вы про свой репозиторий на гитхабе? Тогда, наоборот, показывайте скорее.
Типа: я sql update-ы организовываю и размещаю так. Поэтому если нужен такой странный аудит, сделаю то-то.
С какого из своих проектов?
Мои решения проистекают из требований и кучи намеками указанных условий.
Не увидел ни одного, которого не видел в туториалах :)
Чем они — «ваши»?
Древнее правило, еще с процедурных языков:
стягивайте SQL код в один, или несколько модулей.
Оборачивайте его в процедуры.
И дальше, если надо, рефакторите эти процедуры, и работайте с ними.
Только на очень простых проектах я видел практику когда SQL код лежит где попало.
Так что вам рассказывать, зачем в языках программирования процедуры? Как их применять, и прочее?
Из любого, на ваш выбор.
Я не претендую на новаторство, только стараюсь правильно использовать инструменты.
Ок, есть модуль, есть процедура, в ней.
Если не так, или не правильно понял, поправьте всё что угодно.
Как из этого sql кода получить тот аудит, о котором у нас вроде бы начал получаться разговор? Т.е. чтобы при вызове процедуры также летели insert-ы в табличку аудита «кто, что, когда, старое_значение, новое_значение»?
не вижу смысла напрягаться делать выбор.
работа программиста в том немало состоит — постоянно делаешь выбор. вы мне работы решили отсыпать? :)
зайдите в эту процедуру и добавьте INSERT в предложенную вами табличку.
выше сами написали и другие варианты.
Если нужны такие insertы а их нет, то их где-то нужно написать. или что-то откунфигурировать.
Само по себе — то чего не было — не появляется. нужно что-то сделать чтобы появилось.
Вот по ссылке классический пример — геттеры и сеттеры (а также добавление и удаление из коллекций) не делают из куска данных объект.
Тогда моками. Ну или базу подключать.
База данных (или storage layer в общем) возвращают ответ на вопрос. Поэтому нужно делать иерархию PODов, которые будет возвращать layer.
Проблема в том, что у CRUDов почти всегда нет бизнес логики, вообще. Круд — это когда интерфейс (REST, GUI, CLI, etc) стыкуется почти напрямую с DAO (плюс какие-то базовые проверки валидации).
Есть несколько методов представления дерева в SQL, например методом отрезков. Некоторые СУБД позволяют рекурсивные запросы: learnsql.com/...recursive-tree-traversal.
Таким образом метод который будет переводить таски пачкой в done будет уникальным для каждого стореджа.
Не понимаю. Вот есть объект (уточняем, энтити, конечно). Hibernate может сохранить его (и достать) в/из БД. Почему вы утверждаете, что
?
Это, извините, не понял. Не затруднит псевдокодом проиллюстрировать, пожалуйста?
Наверное, я не так понимаю определение. Для меня CRUD — это, когда есть БД, с данными которой происходит это самое CRUD. Короче, записываю сюда весь кровавый энтерпрайз.
Вопрос был не про собственно граф тасок — это просто пример, чтобы не плодить многие сущности — а про дозагрузку данных.
Т.е. бизнес-логика утекла в сторедж? Или ей норм там быть?
Как шутил один в меру известный программист, неплохой блог у него:
Вот же удивительно, устраивался на работу Java программистом, а с каждым годом пишу все больше на PL/SQL
Oracle/Postgre как сервер приложений — вполне часто в сложных и нагруженных системах.
базы ентерпрайза характерны тем, что размер данных в них сгенерированных системой на порядки больше чем введенных человеком.
А CRUD — это когда мы работаем с БД практически как с электронной таблицей.
На одном из проектов чтобы не морочиться с админкой — я так и сделал, ввод кучи данных был в гугл таблицах. По несложным правилам описывались там же отношения.
И по кнопочке все это затягивалось. с валидациями на обновление,создание и т.п.. Сэкономили на разработке UI знатно :)
Это и была — CRUD часть базы. Самая маленькая по количеству таблиц и занимаемых данных. И самая простая в плане бизнес логики.
А если их 100 то 100 раз.
А если при сохранении одного объекта есть запись еще в две таблицы то будет 100+100+100 запросов.
5 пользователей — и база легла.
Если у вас есть Java «object» который состоит из геттеров-сеттеров, которые можно довольно легко выкинуть (просто вывернув в паблик внутренние члены), то это не «объект» в смысле ООП, а просто структура (запись, struct, record). У такого «объекта» нет инкапсуляции, т.к. через геттеры-сеттеры все кругом ковыряются в его кишках, и никаких других «собственных» действий этот объект делать не может.
struct TaskHeader { int id; int parent; TaskStatus status; }; struct Task { TaskHeader header; std::vector<TaskHeader> children; }; class IStorage { public: virtual Task getTaskById(int id) = 0; };Нет, CRUD — это впервую очередь софт, который заточен на редактирование данные в БД в специализированном интерфейсе. Например: форум, новостной сайт, соцсеть и т.д. и т.п. Грубо говоря, если отбросить валидацию данных и прав доступа, то клиенты могли напрямую гонять запросы в базу и потеряли бы только в графической части.
Это не бизнес-логика. Это сторедж логика. Бизнес-логика была бы в обсчете и создании новых данных на базе выгруженных. Например, из базы выстаскиваются данные для настройки сети и пользователей. Вот «настройка» — это и есть бизнес-логика.
Не очень понимаю, зачем вы мне это пишите, я полностью согласен и категорически против использования геттеров-сеттеров в коде.
Более того, я считаю чудовищной проблемой в джава-мире и не только, что создателям Java Beans конвенции было, видимо, очевидно, для чего они будут использоваться, а огромное число разработчиков видят в них «инкапсуляцию», к сожалению.
У меня стоят @Getter-ы на классами, да. Это для MapStruct-а. Не для бизнес-логики.
Если хотите, можете с ТС-ом поговорить на эту тему.
Извините, я не понял идею. Сначала вы вроде бы за ООП (и я тоже), потом говорите, что с БД читаете структуру.
Это я так предполагаю, что-то вроде интерфейса.
И как теперь это всё соединить и прийти к имплементации юзкейсов?
?
Я не очень люблю спорить про терминологию (предпочитаю смотреть на код), но, видимо, придётся, чтобы говорить на одном языке. Могу принять любое ваше определение.
Вот когда с одной стороны REST API, с другой — БД, а посреди джава-код со всяким разным, это что? CRUD, bloody enterprise, web-application, backend? Я согласен на любое название.
А вот этот момент давайте ещё раз пройдём, пожалуйста.
Требование, что таску нельзя переводить таску нельзя переводить в DONE, пока сабтаски не в DONE — это что? Бизнес-логика, какая-то другая логика, констрейнт, инвариант? Как это правильно называть? Приложение, реализующее это требование — это ещё CRUD или уже нет?
В принципе, если дать пользователю доступ в БД, он и сам может, конечно, проверять это условие, перед редактированием статуса. Но тогда и я мог бы в Privat24 честно обновлять баланс счёта после транзакций, но почему-то нет такой возможности.
Ну и чтобы два раза не вставать про терминологию. Вот это, я так понимаю, вам недостачно класс, наверное, потому что вы в шапке видите @Getter. Покажите, пожалуйста, настоящий класс (энтити), и я сразу говорю, что Hibernate может сохранять/загружать его экземпляр в/из базу. Это я про ваше
На мой взгляд, такой проблемы нет. Ну, если, конечно, вы только сами специально структуры с БД не читаете, но тогда странно обвинять в этом ORM.
Я же говорю — data access layer возвращает структуры, а не объекты, именно потому что у этих структур нет поведения и инкапсуляции. ООП возникает когда на основе этих структур создаются объекты, у которых как раз есть поведение.
В терминах MVC, это — контроллер. И да, это всё ещё CRUD.
Авторизацию и аутентификацию юзеров сложно назвать бизнес-логикой.
Вы из БД в принципе ничего другого получить не можете. БД отвечает на запросы, оно не создает объекты. Создавать объекты должен кто-то другой.
Наверное, последняя отчаянная попытка донести мысль.
Ну, технически, конечно, да. И из файлов и JSON-ов тоже объекты не получить, поскольку это байты.
Ну, вот Hibernate это и делает. Читает данные из БД, создаёт экземляр класса и засовывает данные в него. Для этого ему, кстати, и нужны пустой конструктор с сеттерами. Да, это прямое нарушение инкапсуляции, поэтому никто другой их трогать не должен. Поэтому, чтобы не смущать слабых духом коллег, лучше их не добавлять. Hibernate-у нормально без сеттеров. И даже без пустого конструктора.
Поэтому, считаю, здесь читаются объекты из БД.
Ну, вот вы считаете, что это контроллер, ТС считает, что это интерактор (вы, кстати, открываете ссылки которые я вставляю?) и его абсолютно не смущает прямой доступ к кишкам таски, а я считаю, что это бизнес-логика (пофиг на название), главное что это метод класса Task. Я инкапсуляцию понимаю именно так, неважно кто и откуда будет этот метод вызывать, бизнес-консистентность не нарушится.
Что вы предлагаете делать после чтения структуры
... я так и не понял.
Ну це класичний holy war «anemic vs rich domain model», Sergey Lysak вже згадував.
Є прихильники як одного підходу, так і іншого, багато матеріалу на цю тему в мережі. На dou також були теми.
Ось навіть картинки якісь Гугл видає — dev-to-uploads.s3.amazonaws.com/.../9k46k3h6tli86azv7ry7.png
З власного досвіду, якщо проект не дуже великий, нехай абстрактно до 50 людино/років, є код рев’ю, тех.лід не міняється кожні пів року, то байдуже, anemic (з логікою в сервісах) чи rich model. Якщо якийсь здоровий ентерпрайз де важко за всім угледіти і дати по руках, то rich мабуть краще.
как пример, таких текстов немеряно
RSDN.ru, 2009 год,
Anemic Domain Model vs Rich Domain ModelGlebZ
Преимущества Anemic:
1. Простота построения. Это большой плюс, ибо главная задача архитектора, реализация функциональности за меньшие деньги. 90 процентов решений, могут быть построены в данной модели и это будет на порядок дешевле.
2. Бизнес-объекты — отчуждаемы. В результате, бизнес-объект может быть спокойно пронесен от DAL, к фасаду и даже далее. Беспрепятсвенно и идентично сериализован/десериализован между физическими слоями. В большинстве, бизнес-объект как бизнес-сущность не меняется при переносе между слоями.
3. Прогнозируемость и управляемость вплоть до DAL. Что под этим понимается. В случае, если вы ворочаете большими объемами данных, в Anemic вам проще управлять запросами к базе данных, чем в Rich. База данных была и остается самой тяжелой частью бизнес приложений. Оптимизация в основном достигается за счет повышения эффективности работы с базой данных(и зачастую не обычным редактирование SQL). В Anemic, вы спокойно можете провести тот, или иной сценарий через соседний сервис, который будет работать именно над этом тяжелым сценарием. В Rich — проблема как с прогнозом результирующего запроса, и с его оптимизацией.
4. Бизнес-объект проще управлять объектами, которые еще не полностью в валидированном состоянии. В Rich — наличие валидаторов обязывает держать валидированное состояние. Что иногда выливается в проблемы(например, когда объект только что создан, не имеет идентификатора, и не имеет тех, или иных ссылок). Во многом, бизнес-объект больше похож на данные, чем на объект в стиле объектного программирования.
Преимущества Rich:
1. Простота использования. Объекты всегда под рукой. Средства обработки объектов, также под рукой. Использовать Rich — значительно проще, особенно когда в проекте новичок.
2. Инкапсуляция. Программисту, использующему данный объект, недоступно его состояние, кроме как определенный интерфейс. Это локализует некоторые изменения в логике. Но тут следует упомянуть, что те изменения, которые касаются состояния, также отслеживаются компиляцией со статической типизацией в Anemic, что покрывает большее количество таких проблем.
3. Меньшее количество мусора. В Anemic приходится отслеживать, чтобы в ворохе сервисов не делали свои велосипеды. В Rich c этим меньше проблем в силу более сильной локализации логики.
Действительность.
Чисто Anemic, а тем более Rich в абсолютно чистом виде не бывает. Бизнес-объект как минимум содержит типизацию, что является частью бизнес-логики, в Rich — по любому приходится делать более высокоуровневые сервисы.
Показания к использованию.
Во многом, IMHO, но.... Если вы ворочаете большими данными, то Anemic по любому. Если ваше приложение простое, то anemic. Если у вас большое количество сущностей, большее чем вы можете запомнить, и это не разбить на модули/компоненты/SOA, в которых будут бегать сотня программистов, то Rich. Но для Rich, нужно сразу запастись инструментальными средствами, типа Hibernate(у которого есть чудный кэш), и средства сериализации/десериализации. Это сгладит недостатки модели.
(конец цитаты)
И, каждый год, вчерашний джун выросший до мидла — уверен что он то наконец осознал выгоды подхода Х, и как открывший для себя Истинного Бога — с взором горящим начинает евангализацию неверующих, требует от атеистов, агностиков, иноверцев доказательств, объяснений, и прочая.
Когда же ты делал и по подходу X, и по подходу Y — светлая вера в Единого и Истинного Бога — когда умиляет, когда — «о, очередной великий»...
так я знаю, читано-перечитано в своё время и из практики для себя выводы сделал.
от це іде в розріз з думкою більшості.
адже загальний підхід: якшо в тебе простий круд то використовуй Rich Domain Model
чи я не вірно зрозумів шо є шо?
Так, про це й писав — «ефект підручника».
А цей GlebZ написав свою особисту думку, з практики.
На практиці для CRUDа я рекомендую ActiveRecord, а не Hibernate-Doctrine. Додає зручності, але не такою ціною як в тру ормах.
і у ActiveRecord зазвичай легко залізти на рівень нижче, як треба.
і легко як втулити бізнес логіку у моделі AR, так і витягти з них.
Прикол в тому шо з моєї практики написання простого телеграм боту Anemic теж виявилась єффективнішою.
я реалізовував тільки те шо було потрібно у моменті, легко вносив суттеві зміни бо елементи системи прості та слабко залежать один від одного.
...а ще є
10. Оптимізуйте розмір іміджів, щоб вони завантажувалися швидше
...видалення з іміджу зайвих даних та пакетів;
20 способів оптимізації витрат на AWS, які легко допоможуть заощадити 80+ % бюджету
dou.ua/forums/topic/46648
І от, для лямбди обробки таблиці з тасками — затягуємо в неї «Hibernate». щоб православний код був.
Хорошая цитата, почти со всем согласен, но это
... не понял. Это как? Как статическая типизация связана с состоянием объекта? Что через в сеттер числа строку не подложить? Такая себе защита состояния. Или как это понять?
Может честнее писать «Инкапсуляция —
её нетна совести разработчиков в интеракторе (в терминах ТС)»?Вот как практически предлагается это делать? На код-ревью? В проекте сидит гуру, знающий всю кодовую базу?
... сначала бросает «ОRM на помойку» чтобы пофлеймить, хотя вроде бы
а в другом месте — it depends on.
Так, звісно. На мою думку, перця додає той факт, що чи то немає чіткого визначення цих понять, чи все одно кожен чує щось своє.
От rich це обов’язково active record
entity.save()? Якщоrepo.save(entity)то це ще rich чи вже anemic?Чи різниця між rich чи anemic в тому хто state виставляє?
Чи rich це коли при зміні цього state сама «модель» може листа, наприкрад, відправити?
Я зразу чесно зізнаюсь, що термінологією не володію, і тому не впевнений в якості комунікації.
Хіба позаздрити можу. Для мене це поруч з країною рожевих поні. Все життя в кривавому ентерпрайзі (а що таке ентерпрайз? яке визначення?). Тому я тяжію до rich та ORM.
Ну і безвідмовний спосіб розпочати флейм це накинути «фреймворк Х — на смітник». Приклад1, приклад2, приклад3 (а, ні, тут відносно нормальна дискусія).
Звучит примерно как — не люблю спорить об идеях изложенных в книгах. Предпочитаю подсчитывать количество букв, слов и страниц — потому что это — точная, объективная информация! а идеи — мутные любые все...
А что такое REST API, а что такое не REST API? ;)
что означают эти буквы REST, и как глядя в код увидеть что он реализует именно REST API, если «не люблю спорить про терминологию»?
REST ведь это — идея, та самая «терминология». Не имея, не зная этой идеи, ее в коде невозможно увидеть :)
это что? соленое, тяжелое, красное, дорогое, длинное?
То есть вот такое вы поставили через запятую «или»
Не совсем. Но в обсуждении недетерминированной машины Кузьмина я не участвую, не мой уровень, я до таких абстракций не дорос.
Гусары, молчать!!
P.S. Чуть разгребусь и отвечу на ваши другие сообщения, где есть что обсудить по технической части.
Давайте, вдруг напишите что-то новое с апологетики ОРМов что не читал еще за последние лет10-15 :)
Ну почему сразу на мусорке :) Внутри Data Access Layer, для многих случаев вполне себе может жизнь упростить. Там же его можно и комбинировать с написанными с любовью SQL запросами )) Главное не светить всей этой радостью наружу интерфейсов репозиториев ни в явном ни в неявном виде. Может разве что за исключением рид моделей, и то я бы поспорил, слишком неявно когда один наделает цепочек свойств с той же ленивой загрузкой, а другой гуляет по ним без задней мысли что бомбардирует базу десятками запросов.
Дисклеймер. Я палкий прихильник Hibernate, і коли чую ORM, насамперед маю на увазі цю імплементацію. Я в курсі, що є інші, з не таким багатим функціоналом.
Аналогічно.
Стадіон так стадіон.
Перепрошую, не mapper, а mapping. Це я не прискіпуюсь до слів, а хочу сказати, що ORM — це більш широка абстракцію/технологія.
Це все гарно і добре, я закликаю до того самого.
А от стосовно цього у мене певні сумніви.
Взагалі, аргументи статті дуже схожі на ДДД, який, з одного боку, мені симпатичний, з іншого — ні, бо не знайшов відповіді на практичні питання.
Якщо ліньки читати треди в іншому топіку, повторюсь тут.
Як тільки згадується SQL, питання, що робити з lazy-loading-ом. Як будуть виглядати оці всі
getUser,getDogпри умові, що там будуть більш-менш складні структури, а не навчальні три поля з ID? Будемо витягати з бази все одразу (страждає перформанс) або при потребі (ускладнюється код, змішується власне бізнес-логіка з DAO)?Я з NoSQL не працював, схоже, там такої проблеми нема, або вона не така гостра.
Щоб краще проілюструвати проблему, навіть наваяв маленький проект з ієрархією тасок. З нетерпінням чекаю його кращої реалізації без ОРМ, але з DataGateway (хоча б на рівні ідеї).
С втруктурі вашого проєкту все завязано на визначенні Таски. Більше 120 рядків коду. А репозиторій — 2 рядка. От і виходить проєкт з захмарно високим cupling, це прям монолітний моноліт.
Бо підход проектування через бізнес логіку це він і є. Саме в ньому використання розділення сховища і сутностей більш природнє, але не обовязково тільки там.
З ДДД проблема в тому що це не річ яку приймає окремо розробник, це не те як називають класи чи групують файли у проєкті. Це методологія ведення розробки програмного продукту у яку залучені усі хто дотичні до розробки програмного продукту.
Але якшо чесно мене менше цікавить ДДД. А більше цікавить окреме інжененерне рішення — дозволяти ORM протікати в бізнес-логіку чи ні. І я наївно сподівався шо хтось з читачв ДОУ під цею статтею напише шось на кшалт: «О, а ми в себе теж використовували datagatewy, тіки в нас то repository. Норм штука всім раджу, був тіки косяк з .... але ми пофіксили так ....».
Ех :(
Те саме шо робили5-10-15 років тому: використовувати або курсор або офсети.
Увімкни лог на базі і в живу побачиш як @ManyToOne(fetch = FetchType.LAZY) данні підтягує.
ORM в такому випадку це просто внутрішня складова абстракції DataGateway.
ох уж ці стадіони ... ну ладно.
class Task { id:number //тут в твому коді циклічна залежність яку на проді жоднен адекватний архітектор не дозволить, тому замість обєкту таски просто ID parent:number status:TaskStatus } class TaskWithSubs extends Task{ subtasks:Task[] } //це вже інтерктор, в тій статті про ДДД репозиторій має бути за сервісом, і інжектитись в ного. Class Service{ constructor(storage){інджектимо сторадж} createTask(parentID?:number){ new Task(..., parentID, ...) this.storage.saveTask(Task) } showTaskOnly(id){ return this.storage.getTaskOnly(id) } showTaskWithSubTasksPages(count=10,offset=0){ return this.storage.getTaskWithSubTasks(depth,count,offset) } } Class Storage{ saveTask(Task){ taskDTO = this.mapTaskToDTO(Task) this.ormInstance.saveToTaskTable(taskDTO) } getTaskOnly(id){ taskDTOorSmthigFromORM =this.ormInstance.getFromTaskTable(id) return this.mapDTOtoTask(taskDTOorSmthigFromORM) } getTaskWithSubTasks(depth,count,offset){ //логіка яка розуміє як з бази дістати джойн тасок з сабтасками заданої глибини через рекурсію і відступом конкретної сторінки якшо вам потрібна супер оптимізація return this.mapDTOtoTaskWithSubs(dataFromORM) } private mapper....() //логіка маперів данних в обєкти сутностей і назад }З такми підходом файл Task.js тепер короткий а storage.js здоровий. Закон збереження енергії в дії.
І зверніть увагу на послідовність дій: описали структуру таски, описали дозволені діїї над ними у сервісі, описали як storage надасть нам обєкти для наших маніпуляцій. Спочатку бізнес логіка, потім імплементація джерел данних.
Я ж розмітив код тегом.
Чому ДОУ зїв ентери?
Use <pre> , Luke
class Task { id:number //тут в твому коді циклічна залежність яку на проді жоднен адекватний архітектор не дозволить, тому замість обєкту таски просто ID parent:number status:TaskStatus } class TaskWithSubs extends Task{ subtasks:Task[] } //це вже інтерктор, в тій статті про ДДД репозиторій має бути за сервісом, і інжектитись в ного. Class Service{ constructor(storage){інджектимо сторадж} createTask(parentID?:number){ new Task(..., parentID, ...) this.storage.saveTask(Task) } showTaskOnly(id){ return this.storage.getTaskOnly(id) } showTaskWithSubTasksPages(count=10,offset=0){ return this.storage.getTaskWithSubTasks(depth,count,offset) } } // а це вже драйвера та ітакфструктура Class Storage{ private ormInstance:Hibernate constructor(){ this.ormInstance=initORM(), connectToDB() } saveTask(Task){ taskDTO = this.mapTaskToDTO(Task) this.ormInstance.saveToTaskTable(taskDTO) } getTaskOnly(id){ taskDTOorSmthigFromORM =this.ormInstance.getFromTaskTable(id) task:Task= this.mapDTOtoTask(taskDTOorSmthigFromORM) return task } getTaskWithSubTasks(depth,count,offset){ //логіка яка розуміє як з бази дістати джойн тасок з сабтасками заданої глибини через рекурсію і відступом конкретної сторінки якшо вам потрібна супер оптимізація //або просто таска з джойном сабтасок taskWithSubtasks:TaskWithSubs =this.mapDTOtoTaskWithSubs(dataFromORM) return } private mapper....() //логіка маперів данних в обєкти сутностей і назад } // рівень основного файлу застосунку storage = new Storage() service = new Service(storage) http.createServer((req, res) => { on GET/task { return service.showTaskOnly(req.id) } on POST/task { return service.createTask(.....) } on GET/taskWithSubs { return service.getTaskWithSubTasks(.....) } })Таке враження, що у нас сильно відрізняються світи, це буде або цікава або безглузда дискусія.
Ем, а що тут поганого-то? Я так розумію ООП — є клас, його проперті та методи. Що не так?
(витріщає очі) Чому моноліт?!
Я вже боюсь питати, як ти розумієш coupling, бо невідомо куда зайдем.
З цим категорично згоден.
Мені так само цікаво, як з Datagateway(DAO?) вирішується питання, коли в залежності від бізнес-логіки треба дозавантажувати (або ні) додаткові дані з SQL DB.
(знову витріщає очі) По-перше, це не циклічная залежність, а ієрархічна. А хай би і циклічна залежність — у доменній моделі може бути що завгодно, якщо так є по бізнесу. Ніяких обмежен. У чому проблема?
І чому архітектор рев’юіть код? ;-)
Для чого два класи на одну бізнес-сутність?
Що робить сервіс? Це просто проксі до storage?
Тобто Task.js — це тільки структура даних? А бізнес-логіка де? В сервісі? Чи в сторажі?
Ага. А тестуємо що? Storage? З моками? Чи з in-memory DB?
А що робити, коли в залежності від бізнес-логіки треба (або не треба) завантажувати дані?
В якому шарі це робити?
Не зовсім вдало сформулював, спробую ще раз. Для мене основна супер-сила
ORMHibernate не в селектах, а в апдейтах, тобто@Transactional class Service { public void someMethod(Changes changes) { CompexEntity compexEntity = repository.findById(id).orElseThrow(); complexEntity.updateBy(changes); } }CompexEntityможе бути яким завгодно складним об’єктом з внутрішніми колекціями і колекціями колекцій. Вupdate(...)може відбуватись складна логіка, яка потребує (або ні) дозавантажання внутрішніх пропертей. Тестується це все добро unit-тестом без жодних контекстів, а решту робить ORM.Під lazy-loading-ом (у контексті ORM) я мав на увазі, що замість завантаження даних в properties створюються проксі, які відпрацьовують при звертанні до тих.
Отой
... або не підтягує, якщо не треба.
Щоб проілюструвати про що йде мова, такий юзкейс: зміна статусу таски.
У complete переводимо, якщо всі дочірні сабтаски закриті. Або з force=true закриваємо всю ієрархію. Питання — скільки даних завантажити з БД для цього?
Спробуй розверни усі декоратори які навішані на класс таски, побачиш наскільки перегружений логікою цей обєкт. Чим складніший класс — тим важче його змінювати в майбутьному.
Шоб позбутись тої проблеми коли завантажеється лишня інфа де її не треба. Просто з моєї точки зору то дві різні бізнес сутності які затребувані в різних бізнес юзкейсах.
В твоїй реалізаціїї, клас = таблиці. і це якраз той coupling про який я казав, система більш гнучка коли це не так.
Догнав. ІМХО це страшне зло, але то вже персональні вподобання.
Про розділення логіки. ХЗ як краще, але зазвичай якшо це суто властивостей класу сутності — то в клвсі сутності. Якшо це про маніпуляції над сутнісю чи між декількома — то в інтеракторі.
Service — це інтерактор, тут описуються юзкейси маніпуляцій над таском, які від нас просить продактовнер.
Так само в обох варіантах ... чи я шось не розумію.
Class TaskService{ changeStatus(taskID:number, status:TaskStauts, forcce?:bool){ if(status==TaskStatus.Done || status==TaskStatus.InProgress){ task = this.storage.getTaskOnly(taskID) if(!task) throw new Error('Task not found') task.status= status this.storage.saveTask(task) }else if(status == TaskStatus.Done ){ taskWithSubs = this.storage.getTaskWithSubs(taskID) if(!task) throw new Error('Task not found') isUndoneSubs = taskWithSubs.subtasks.some(subtask=>subtask.status!=TaskStatus.Done) if(isUndoneSubs && !force) throw new Error('Subtasks not done') taskWithSubs.status= status subsToUpdate = taskWithSubs.subtasks.find(subtask=>subtask.status!=TaskStatus.Done).each(subtask=>subtask.status=TaskStatus.Done) this.storgeUpdateTaskBulk([taskWithSubs, ...subsToUpdate]) }else{ throw new Error('Unknown status') } } }Просто в мому варіанті логіка в інтеракторі, і у випадку якшо при зміні статусу таски треба ще якісь діії виконувати, наприклад відправити листа асайні сабтаски, то це леко додати. Не треба тягнути у класс таски всю логіку відправки листів.
Поправочка
над таском, або над таском та іншими сутностями повязаними цим юзкейсом.
Не зрозумів. Про які декоратори йде мова?
Знову не зрозумів. Ось клас. Два конструктори-аліаси, три публічних метода — addSubTask, updateStatus, patch. Про яке перевантаження йде мова?
Коли зв’язані дані розмазані по різних сховищах — це в любому випадку буде непросто. Але буває і таке, що у додатка одне сховище — реляційна БД.
У нашій справі нема чистого зла і добра, переважно компроміси. Я не вважаю lazy-loading злом, бо в моїх кейсах це суттєво спрощує і пришвидшує розробку.
Чудово. Тобто Task — це
структурасутність, а Service — ценабір процедурінтерактор. У процедурно-орієнтовному програмуванні як такому нема нічого погано, але постає питання, як тестувати цей інтерактор.Ти ось написав метод
... там if-и, цикли — такі речі треба тестами покривати.
Як буде виглядати тест?
Я свої тести вже написав.
Мабуть недостатьо зробив акцент. У тасок можуть бути сабтаски, в тих — ще, вкладеність графу — необмежена.
... не підходить, там знову має бути
subtasks:TaskWithSubs[]Це приклад складного об’єкта, який вимагає дозавантаження даних, на одному класі, щоб не плодити багато сутностей. Ілюстрація
... цієї проблеми.
Це гарне питання, я буду передавати лямбду/колбек/consumer в
updateStatusметод.Приходить продакт овнер і каже, в нас міні чендж реквест, вкладеність тепер нескінченна.
Я вже пообіцяв, зробіть на завтра.
class Task { constructor(id:number, parent?:number, status:TaskStatus, subtasks?:Task[]){ ... } id:number parent:number status:TaskStatus subtasks:Task[] } Storage{ getALLOpenSubtasks(parentID):Task[] { // SQL пошук я зараз не придумаю але думаю що це реально, //як мінімум в циклі шукати через декілька запитів //по навантаженню на базу це буде те саме шо твій лейзі лоад у циклі private void markDone foundTasksWithNotDoneStatus:Task[] = mapper(dataFromORM) return foundTasksWithNotDoneStatus } }можем тестити на реальній локальній базі, заповненій будь якими тестовими даннимитепер троти TDD inda house, можем хоч усі юзкейси описати, але зупинемось на одному
mockStorage ={ getTask:jestMockFunction() getALLOpenSubtasks:jestMockFunction() } it('should change status of all subtasks to Done', () => { main = new Task(1, null, TaskStatus.InProgress, []) sub1 = new Task(1, 1, TaskStatus.InProgress) sub1 = new Task(2, 1, TaskStatus.New) mockStorage.getTask.mockReturnValue([main]) mockStorage.getALLOpenSubtasks.mockReturnValue([sub1, sub2]) service = new TaskService(mockStorage) service.changeStatusOfAllSubtasksToDone(1) expect(main.stauts).toEqual(Done) expect(sub1.stauts).toEqual(Done) expect(sub2.stauts).toEqual(Done) expect(mockStorage.bulkTaskSave).toHaveBeenCalledWith([main, sub1, sub2]) })тереп робимо тест зелененьким
class TaskService{ changeStatus(taskID:number, status:TaskStauts, forcce?:bool){ task = this.storage.getTaskOnly(taskID) if(!task) throw new Error('Task not found') if(status==TaskStatus.New || status==TaskStatus.InProgress){ task.status= status this.storage.saveTask(task) }else if(status == TaskStatus.Done ){ undoneSubs = this.storage.getALLOpenSubtasks(taskID) if(undoneSubs.length>0 && !force) throw new Error('Subtasks not done') tasks.status= status doneSubs = undoneSubs.each(subtask=>subtask.status=status) this.storgeUpdateTaskBulk([tasks, ...doneSubs]) }else{ throw new Error('Unknown status') } } }Ось і все, час купляти сир чи слати донат Чмуту.
Зверни увагу як мало мені довелось змінити в порівнянні з попереднім рішенням.
фікс, у тесті має бути
service = new TaskService(mockStorage)
service.changeStatus(1, Done, true)
ф забув додати, шо поки сіньйор-помідор гуглить як робити SQL-пошук по графу, джун штампує тести для TaskService і робить їх зелененькими
Або просто уважніше придивитись до README.md
Не те саме. Витягувати всю ієрархію — це вкрай погана ідея з точки зору перформанса і пам’яті. Можна спочатку надовго задуматись, а потім вилетіти з OutOfMemoryException.
Для force=false достатньо витягти тільки «безпосередні» сабтаски, весь граф не потрібен.
І при апдейті з force=true не треба завантажувати сабтаски, якщо чергова таска вже в DONE. Ось я поправив цей момент, тест поруч.
На всяк випадок, нагадаю: обговорюємо не конкретну задачу — та скільки там тих тасок? максимум 100! — а загальний підхід, коли бізнес-логіка вимагає дозавантаження даних.
Ні-ні-ні, не треба, будь ласка, займатись самообманом. Ніякого TDD не було, тест писався після реалізації, про що також свідчить оговорочка по Фрейду
Мій поінт не в тому, що треба писати спочатку — тести чи код — без різниці, а в тому що хороший тест — тестує black-box. А твої тести спочатку зазирають в код, щоб знати який мок підкласти.
Звідки тест знає що буде викликатись
getALLOpenSubtasks?А тепер дивимось на рішення — до чого я вів з самого початку — це суміш бізнес-логіки з
DAODategateway-ем.Ще тобі треба в залежності від флага force викликати
getALLOpenSubtasksчиgetJustDirectSubtasks, ну і оптимізувати перший метод, щоб він не тягнув геть усе. А потім виправляти весь тест сют, тому що моки помінялися. Гадаю, ти з цим постійно стикаєшься, тому і питаєшКрім того є ще одна проблема.
Ти вільно апдейтиш статус. Тобто Таска — це просто структура, а не об’єкт; її внутрішній стейт виставляє
сервісінтерактор, і нема ніяких гарантій, що це робиться консистетно. Всі інваріанти таски — як відбувається перехід в DONE, можемо нафантазувати ще — розмазані в методах сервіса. Слово «інкапсуляція» залишилось на співбесіді, її — нема.Не те, щоб я такого підходу не бачив, або сам, грішний, колись не притримувався. Так, колись здавалось, що логіка в сервісі, покрита моками репозиторієв — це вершина майстерності. Зараз вважаю що це не так.
як ти зміг пекласти getALLOpenSubtasks у витягнути всю іерархію?
я ж написав шо там може бути логіка ідентична циклу у markDone.
ок, тобі видніше .... менше з тим, тема дискуссії не TDD
знову ні, в storage я створив спочатку функцію яка повертає тільки таску, і додав функцію шо повертає усі не закриті сабтаски, з пошуком у глиб. я думав імена функцій кажуть самі за себе.
для якого юзкейсу потрібен getJustDirectSubtasks?
так а в чому проблема то? юзкейс зміни статуса у нашому застосунку лише в одному місці, обробці POST /task/status . я цей обробник реалізував шоб він виконував те шо просить продакт овнер. Аксептенс критерї проходе, усі варіанти подій при зміні статусу покриті в тест сюті TaskService.changeStatus. Ну чим ти ще не задоволений?
Інкапсуляція заради інкапсуляції?
дивлячись шо ти вважаєш бузнес логікою.
От продакт каже:
WHEN Task DOES status change TO Done
IF any not done subtask in hierarchy AND no force THEN throw error
IF any not done subtask in hierarchy AND force THEN main task status done and all subtasks in herarchy status done
то TaskService.changeStatus повністю описує юзкейс цєї бізнеслогіки.
з моє точки зору нічого не розмазано. якшо я хочу подививтись як веде себе Task, я відкриваю його інтерактор і вся бізнеслогіка перед очима.
Без питань, я думав, що достатньо сказати «ієрархія» і «граф», але розжую.
Є таска1 — коренева, парента нема, статус IN_PROGRESS.
Таска 2 — сабтаска таски1, статус NEW.
Таска 3 — сабтаска таски2, статус NEW.
Таска 4 — сабтаска таски3, статус DONE.
Таска 5 — сабтаска таски4, статус DONE.
У таски5 ще є сабтаски але припинемо.
Так от, якщо закривати таску1 з force=false, то достатньо витягти таску2, щоб визначити, що це неможливо. Підтаски таски2 перевіряти (і витягати) їх з БД — непотрібно. Я це мав на увазі під
getJustDirectSubtasksА якщо закривати таску1 з force=true, то після закриття тасок 2 і 3 далі йти «вглиб» непотрібно, оскільки в тасці4 не було змін, вона і так — done. Витягати таску5 — не треба.
Тепер питання як це реалізувати в
getALLOpenSubtasks.В markDone не тільки цикл, там лінива рекурсія, яка зупиться на тасці4.
А то я такого коду не бачив) Километровий метод в сервісі який
жонглюєнапряму сетить проперті різних ентіті. Якийсь з флоу покритий тестами з моками, решта — ні, бо небуло часу. При зміні логіки треба міняти десяток тестів, які той шмат наче не тестують, але моки тепер інші.Новій людині розібратись в цьому вкрай важко. Я з цим борюся за на користь OOP+ORM.
зрозумів вашу позицію, дякую що поділилсь досвідом.
Дякую за дискусію, було цікаво
оці:
@Entity
@Table(name = «tasks»)
@Getter
@Slf4j
@ManyToOne(fetch = FetchType.LAZY)
@OneToMany(mappedBy = «parent», cascade = CascadeType.ALL, orphanRemoval = true)
я буквально казав спробуй розвернути код усьої логіки.
З таким кодом можеш процювати тільки ти або якійсь мідл+. Джун без досвіду з Hibernate нічого зробити не зможе.
і як це вплине на твій існуючий тест markDoneForce?
а якшо в тебе тест сют та класс Task з сотнею тестів, в ще є тести інших сутностей де таск використовується?(це я вже перегібаю)
Це не декоратори, а анотації.
Будь-яка технологія — це трейд-оф між витратами на її опанування і користю, яку вона надає. Можно легко навести протилежні екстреми.
Запитання до автора. А додавання ORM в проект для безпритульних собак — це не жорстоке поводження з беззахисними тваринами?
Не таке вже і жостке якшо порівнуювати з примусом Jmeter-a ганяти безглузді тести віртуальних потоків.
Гарні аргументи які пояснюють рубістам чому ActiveRecord не прижився і є, загалом на практиці, збоченням, а не вершиною думки, як їм здається.
Рубі тут ні до чого — це фішка конкретно рельс.
А рубі без рельс існує? 0.о
Звісно існує, навіть в вебі (одним з прикладів є як раз компанія ТС, але деталей не буде через NDA; можливо ТС захоче розповісти більше). І навіть рельси самі по собі ще не означають бездумного використання AR — я бачив кейси, коли AR повністю ховають за репозиторієм/query objects/... (компромісне рішення, бо незроуміло навіщо обирати фреймворк щоб потім з ним боротись, але це вже ортогональне питання).
Це ж питання навіть не про рубі, чи не так? А про те, що деякі люди роблять висновки про когнітивні здібності інших тільки на підставі інструментів, що використовуються :)
Одно із найбільш яскравих вражень під час використання ORM це коли до тебе приходять DBA і кажуть що ваш застосунок робить ось такий запит, він поганий, погано впливає на продуктивність сервера і його треба переробити, мабуть ось так та ось так. І починається гра «вгадай місце у C# коді по SQL запиту» з продовженням «зроби ORM згенерувати запит іншим чином».
Факт.