Місце ORM у загальній архітектурі застосунку

💡 Усі статті, обговорення, новини про Mobile — в одному місці. Приєднуйтесь до Mobile спільноти!

Колись на співбесіді мене спитали: «А як ти вчишся?». І нічого іншого, як відповісти «На практиці», я не придумав. У цій статті хочу поділитись певним своїм практичним досвідом і сподіваюсь прочитати про схожий досвід від колег.

tldr

Ця стаття — спроба порефлексувати про місце ORM у загальній архітектурі програмного застосунку. Стандартний варіант — інжектити екземпляр об’єкта, сконструйованого ORM-класом, у бізнес-логіку та оперувати ним. Альтернатива — зробити жорстке розділення рівня сховища та об’єкта сутності через абстракцію — Datagateway. Якшо ти на реальному продакшн-проєкті з таким заморочувався — поділись досвідом у коментарях.

Два підходи до архітектури

Отже, уявімо що ми починаємо будувати пет-проєкт. Застосунок для реєстру безпритульних собак. Що в нас є? Два епіка, 75 юзерсторі, пʼять розпланованих спринтів, скрам-мастер і гора ідей, концепцій, гіпотез усіх кольорів... А фактично ми або починаємо малювати таблиці бази даних з полями, або розписувати, хто які дії може виконати в застосунку. І вже далі від цього продовжуємо розробку. І зрештою отримуємо два різні підходи:

  • схема даних диктує, як ми зможемо прописувати логіку;
  • логіка диктує, як і що ми можемо зберегти.

Звісно, життя не двокольорове, воно є градієнтом сірого. Але від фокусу (концентрації) на одному або іншому підході не втекти. Думаю, всі знають про MVС, де model йде першою, тому з неї і починають. Не знаю, чому зараз учать в університетах, але я пам’ятаю час, коли для MVС-підходу взагалі не було альтернатив. І тому не було місця і для роздумів про ORM.

  1. Створити базу.
  2. Через ORM отримати доступ до даних у зручному вигляді.
  3. Проманіпулювати даними в контролері.
  4. Показати результат у представленні.
  5. ???
  6. Profit.

То що там з альтернативами у вашому 23-му? Ну, Дядько Боб шось там казав про танець сутностей, інтректори та не дуже згадував про бази даних взагалі.

  1. Описати об’єкти.
  2. Описати, як ними маніпулювати, щоб отримати користь.
  3. Підключити джерела отримання / відправлення даних об’єктів.
  4. ???
  5. 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 — я не знаю.

Не прихований продакт-плейсмент

Якщо хочеш спробувати, як працює написаний за такою схемою застосунок, ось посилання. А якщо ти знаєш тих, кому такий інструмент може знадобитися, — поділись цим посиланням з ними. Зима вже на порозі.

👍ПодобаєтьсяСподобалось8
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Почну ще один тред. Бо в обговореннях куча теорії та маловато конкретики. А до кучі ще й вагон різноманітних термінів які чи то синоніми чи то ні.

На вашому реальному досвіді скільки було проєктів де доступ до бази був через жорску абстракцію без протікання 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 з усіми деталями за раз

От тобі непогана ввідна стаття з конкретикою www.baeldung.com/...​l-architecture-ddd-spring.

Повна шиза:

We won’t register it as a Spring bean because, from a domain perspective, this is in the inside part, and Spring configuration is on the outside. We’ll manually wire it with Spring in the infrastructure layer a bit later.

Це просто квітуча шизофазія, вирішення неіснуючих проблем.
Двукратна імплементація репозиторія боже яка діч.
І сам дизайн доменних об’єктів до речі теж гівняний.

Привіт, Дмитро

Двукратна імплементація репозиторія боже яка діч.

Не до того прискіпуєшся, дозволь я поясню.

Не перший раз чую навколо і на ДОУ зокрема дві пов’язані думки:
1) домен має бути відв’язаний від персістанса. ДДД, гексональна архітектура, оце все. Власне ТС про це саме питає.
2) якщо персістанс робиться окремо, то чи потрібен там взагалі ОРМ (знову ж таки, що таке ОРМ? Який ОРМ? Hibernate та MyBatis — це, мяко кажучи, різні речі)

Стосовно першої — я в неї тупо практично не вірю. Не буває такого ідеального домену, складнішого за хелло-ворд, якому що в SQL, що в S3, що в графовій БД зберігатися — однаково. Ти гарно висловився, я можу тільки ППКС. Але от стаття на baeldung — як раз про це. Коментарі там, нажаль, закриті, а так я би спитав: а ну перейдіть з NoSQL на SQL, подивимось на перформанс eager-loading orderItems. Чому eager-loading? Ну, а який, домен-то відокремлений.

Якщо припустити, що репозиторій окремо від домена, можна починати спеціальну олімпіаду «Hibernate тармазіт і ваабще ні нужєн».

Ці думки, строго кажучи, різні, але я їх об’єдную в один підхід, тому що в мене (і в тебе, здається, теж) — так, домен прибитий до persistance, але я від цього не страждаю, а насолоджуюсь.

Тим не менш, щоб був не просто срач, а якийсь конструктив, я не полінувався написати приклад рекурсивного lazy-loading-у і всіх їм дістаю, щоб показали мені світло істини.

ТС, молодець, не посоромився, високовчені архітектори і прирівнені до них — до таких примітивних кодерських задач не опускаються.

І тобі привіт )

(і в тебе, здається, теж) — так, домен прибитий до persistance

Звичайно прибитий )

Лейзі-вс-ігер лоадінг взагалі бездонна тема.

В окремих випадках я взагалі розв’язую один віж одного об’єкти і підвантажую їх зв’язки окреми селектами типу 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

місце ORM у загальній архітектурі

На мусорке. Это уже больше 10 лет обсуждают и ORM может работать для каких-нибудь CRUDов, но не более

На мусорке.

А шо на замість?

Data Access Layer, написанные с любовью SQL запросы, PODы

І як побороти перевантаження Data Access Layer навалою різних методів типу:
getTask
getTaskWithSubs
getTaskForAutocompit
getAggergatedTask
.....
і ТД.

мож знаєшь гарні статті про best-practices як його структурвати?

POD

шо за акронім?

І як побороти перевантаження Data Access Layer навалою різних методів

Разбивкой на подфункции которые из строки выгребают нужные данные:
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.
А потом уже в публичных методах вызываешь нужную комбинацию над результатами запроса.

А тестировать как этот процедурный стиль? Моками?

Для мене основна супер-сила ORM Hibernate не в селектах, а в апдейтах

Как раз апдейты — самое слабое место в ОРМах.
Ну когда надо проапдейтить несколько таблиц, с разным набором блокировок.

А тестировать как этот процедурный стиль? Моками?

а с чего он процедурный?
для

вызываешь нужную комбинацию

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

Тестується це все добро unit-тестом без жодних контекстів

и нафик они нужны, когда имеется сложная работа с персистентными данными в сложной схеме.

а решту робить ORM.

и делает это — плохо. без дополнительных танцев в обход ORMа.
для MVP только и хорош, и для книжек для начинающих.

Ну когда надо проапдейтить несколько таблиц, с разным набором блокировок.

Расскажите, пожалуйста, подробнее, какую проблему решаете «разным набором блокировок»? Write Skew?

Как раз апдейты — самое слабое место в ОРМах

Ок. А как и чем вы апдейты делаете?

а с чего он процедурный?
и нафик они нужны, когда имеется сложная работа с персистентными данными в сложной схеме.

Ок. А как вы бизнес-логику тестируете?

Если проект под страшным NDA, могу предложить свою задачу.
Есть таски с сабтасками и сабтасками сабтасок, иерархия не ограничена.
Статусы: new -> in progress -> done
Юзкейсы: в done можно переводить, только если сабтаски закрыты.
Но с force=true можно перевести в done весь подграф.
Проблемы юзкейсов: сложно отделить DAO от бизнес-логики.
Без всяких блокировок.

Вот моё решение, вот подход ТС-а, который я называю процедурным, вот моя критика этого подхода. Расскажите своё видиние, пожалуйста, поделимся опытом.

для MVP только и хорош, и для книжек для начинающих

С вашего позволения, процитирую прямо из профиля с минимальными изменениями.

«Топовый способ свести дискуссию о программировании к демагогическому и догматическому холивору — ввернуть аргумент о „(не)продуманной архитектуре пригодности только для MVP и начинающих“. Произносить надо с придыханием небрежным презрением, и можно смело предавать анафеме сомневающегося. Сам же за умного сойдешь.»

Расскажите, пожалуйста, подробнее, какую проблему решаете «разным набором блокировок»?

Постоянную, когда есть конкурентный доступ.

А как и чем вы апдейты делаете?

Кодом ессно. ОРМы тоже им, кодом делают :)

А как вы бизнес-логику тестируете?

Функциональными тестами, ессно.

Если проект под страшным NDA

О каком из проектов что делал речь?

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

Давно говорю

На задаче вывода «Hello world!» никак не показать необходимость в слоенной архитектуре, паттернах ООП и т.п.

Потому что для такой примитивной задачи что вы описали — можно обойтись парой php скриптов, и третий для PHPunit,
Хотя, парой хранимок — тоже можно.
На кой черт для такой примитивной задачи что вы описали вообще нужны эти ормы, дата лаеры, и прочая астронавтика?

Понимаете в чем проблема?
На мелком масштабе и задачки с литкода — невозможно показать решение проблем, которых там не существует, в силу — малого масштаба.

Расскажите своё видиние, пожалуйста, поделимся опытом.

Рассказал выше. В обсуждении способа вывода Hello world — пусть джуны делятся опытом.

пригодности только для MVP и начинающих

начинающим копать тему об ОРМах давно рекомендую с статьи
«Вьетнам компьютерной науки».
Чтобы можно было продвинуться в обсуждении еще более сложных фундаментальных вопросов в ней не упомянутых.

Все мы начинали, с Hello world’ов. И постоянно:
Снова весна.
Приходит новая глупость.
Старой на смену.
(Исса)

И каждый год, даже на ДОУ, уже второе десятилетие очарованные ОРМ делятся опытом как классно выводить им «Hello world».

Я же давно сталкиваюсь с необходимостью обходить ОРМы, лезть им в кишки, реорганизовывать код, и т.п.
И, не только я. На ДОУ таких тем уже было полно, и постоянно об этом и речь.

Да и в инете. Более общее название этого холивара между начинающими и битыми — Rich model vs Anemic model

Кодом ессно. ОРМы тоже им, кодом делают :)

Ну, это не ответ. Вы б ещё написали — update-ом.

Я предложил, например, подход

@Transactional
class Service {
    public void someMethod(Changes changes) {
        CompexEntity compexEntity = repository.findById(id).orElseThrow();
        complexEntity.updateBy(changes);
    }
}

Предложите какой-то другой, пожалуйста.

На задаче вывода «Hello world!» никак не показать необходимость в слоенной архитектуре, паттернах ООП и т.п.

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

Потому что для такой примитивной задачи что вы описали — можно обойтись парой php скриптов, и третий для PHPunit,
Хотя, парой хранимок — тоже можно.

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

Поэтому с

Понимаете в чем проблема?
На мелком масштабе и задачки с литкода — невозможно показать решение проблем, которых там не существует, в силу — малого масштаба.

... не согласен. Где-то в закладках валялась (я не Ops, поэтому не пригодилась) статья про три микросервиса, с helm-ами, K8S, horizonal scaling и прочим, два из которых передавали единицы третьему, который их суммировал. 1+1 = 2, ага.
В коментариях благодарили за готовый шаблон для развёртывания. Это я к тому, что для сложения двух чисел три микросервиса вряд ли нужны. Но показать, как оно может выглядеть в микросервисной архитектуре — можно.

Рассказал выше.

Извините, пока слышу только общие рассуждения.

начинающим копать тему об ОРМах давно рекомендую с статьи
«Вьетнам компьютерной науки».

Это интересно, спасибо, наскоро глянул, хотя, конечно, аналогия, конечно, ух!

И каждый год, даже на ДОУ, уже второе десятилетие очарованные ОРМ делятся опытом как классно выводить им «Hello world».

Покажите, пожалуйста, хоть одну статью. А то у меня ощущение, что за ОРМ топлю я один, остальные только негативно отзываются.

Я же давно сталкиваюсь с необходимостью обходить ОРМы, лезть им в кишки, реорганизовывать код, и т.п.

Я, хотя и начал с признания, что являюсь фанатом Hibernate, прекрасно понимаю сложность и невозможность 100% object-relational-mapping, и могу поиграть и в противника технологии.

Поэтому и интересны проблемы, альтернативы и подходы.

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

Ну, это не ответ. Вы б ещё написали — update-ом.

так ваш пример — им и делает.

Предложите какой-то другой, пожалуйста.

Прикалываетесь? ;)
Ваше предложение звучит примерно так:
Я вывожу «Hello world»
System.out.println("Hello World!«);

А как выводите вы?

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

А мне кажется что:
В простых системах нет сложных проблем, чтобы на них продемонстрировать методы решения сложных проблем.

Извините, пока слышу только общие рассуждения.

Да, мой метод давно во всем — от общего к частному.

Покажите, пожалуйста, хоть одну статью

Последнее обсуждение что участвовал было где-то в этом году.

А то у меня ощущение, что за ОРМ топлю я один

Не :) За ОРМ топит большинство. Потому что это «эффект учебника».
В учебниках рассматриваются прежде всего, чаще всего создание CRUD приложений. Потому что они проще. А для них использование ORMа — вполне ок.
Проектирование более сложных приложений рассматривается гораздо реже. Потому что само изложение проблем — больше. Авторов желающих и могущих описывать — меньше. у них потому что на своих проектах куча работы, некогда им много писать. Спрос на такие книги — тоже меньше, особо и не продашь.

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

Вот вам в каких случаях приходится обходить ОРМ

Самый частый, сразу, с порога — SELECT N+1

С коробки есть упомянутые eager/lazy
Но — поможет в простых — CRUD системах.

Если же вложенность коллекций объектов глубже, если поля у доменных объектов — агрегированные значения, то — схему БД и работу с ней приходится проектировать вручную, а не как в учебниках:
описали классы, описали отношения, а схему сам ОРМ сделает.

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

CompexEntity може бути яким завгодно складним об’єктом з внутрішніми колекціями і колекціями колекцій. В update(...) може відбуватись складна логіка, яка потребує (або ні) дозавантажання внутрішніх пропертей, створює або видаляє нові елементи тощо.

В том-то и проблема, что каждая догрузка пропертей — это проход в базу и тормоза.
Основная проблема ORM состоит в том, что база данных выгружает не объекты (данные + поведение), а только лишь данные.

А тестировать как этот процедурный стиль? Моками?

Что именно тестировать?

Основная проблема 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 (у каждого/для каждого) объекта.

И когда таких «пояснений» ОРМу становится больше какого-то разумного количества — и возникает вопрос — а только в миграциях в БД от него и польза.

Попробую тут всё собрать.

foreach(x of X) doV(x)

Кстати, характерная оговорка, не x.doV(), а doV(x). Ну, это так к слову, не начинать OOP vs PP vs FP )

Часто оно неявно будет описано. А есть объекты у которых есть коллекции объектов, а у тех — свои коллекции.
И есть действие — когда нужно для каждого объекта коллекции нужно выполнить действие, которое приведет к вложенным обходам коллекций.

Именно это я считаю киллер-фичей Hibernate.
Имеено это я имел в виду, когда писал complexEntity.updateBy(changes); Это не sql update, а неизвестно какая логика.
Именно об этом мой пример. Вот этот рекурсивный обход вложенных коллекций.

Далее, сама эта загрузка глубоко вложенных данных зависит от doV. Для doW она должна быть другой, например потому что будут срабатывать другие ограничения бизнес-правил.

Именно так. И Hibernate позволяет сосредоточиться на логике, предполагая неявную дозагрузку данных, если они нужны.

Вот есть объект (уточняем, энтити, конечно). Hibernate может сохранить его (и достать) в/из БД.
А если их 100 то 100 раз.
А если при сохранении одного объекта есть запись еще в две таблицы то будет 100+100+100 запросов.
5 пользователей — и база легла.

Вряд ли база ляжет от 5 * 300 = 1500 простых select/udpate... where id = ...;

И да, задачу типа «в департаменте Х поднять всем з/п на 10%» буду по возможности делать именно в стиле

repo.findAllBy(department)
    .forEach(e -> e.increaseSalaryByPercent(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);

Или что вы предлагаете?

Как я предполагаю, у вас другой энтерпрайз, настоящий.

Чим більше відбувається перекладання даних між таблицями, більш довгі flow, перерахунки чогось, потреба в агрегованих даних
Вот же удивительно, устраивался на работу Java программистом, а с каждым годом пишу все больше на PL/SQL
если поля у доменных объектов — агрегированные значения,

Конечно, если начинается логика построенная на агрегированных значениях ORM не подходит.

Расскажите, если желаете, о проекте. Что делаете, какими фрейморками, какие нагрузки?

Более того, я работал в одном, по-своему, отличном проекте, пришёл, говорю «ну, покажите мне базу, чем с ней работаете и т.д.», а базисты мне удивлённно «зачем это тебе? База закрыта, твоё дело данные сюда, в одну специально открытую stored procedure, класть». В общем, джава там была сугубо для интеграции — хттп-запросы, слушание очередей, парсинг всякого разного, а вся логика была в БД, ворочались какие-то многочасовые фиды и т.д. Базисты не лезли ко мне, а я — к ним, и всем было хорошо.

И боль, когда начали с ORM, потом немного подтюнили bulk-запросами, потом добавили SP и теперь логика размазана везде — я тоже понимаю.

Чем вас Hibernate так укусил, что вы в него везде плююете?

Самый частый, сразу, с порога — SELECT N+1

Давайте обсудим. Что не так с N+1? Какой контекст/какую задачу решаем?

Кстати, характерная оговорка, не x.doV(), а doV(x)

да, разница между «в лоб» и «по лбу» — огромная

Именно это я считаю киллер-фичей Hibernate.

ну да, об этом же и речь — ОРМ — это киллер БД :)

И Hibernate позволяет сосредоточиться на логике

а в жизни кроме идеалов логики есть ограничения физического мира.

Вряд ли база ляжет от 5 * 300 = 1500 простых

ложится. даже нетворк внутри дата центра — ложится :)

полностью осознавая минусы этого подхода, но в моей сфере плюсы перевешивают.

а в куче других сфер — вообще все что вы пишите — и с доплатой не нужно :)

а объёмы данных и нагрузка — сравнительно невелики

забавный тогда у вас энтерпрайз :D

Если хотите померяться цифрами — 10^6 пользователей, 10^2 req/s

Первая черта энтерпрайза — это система для офисных пользователей. Не обязатально, но обычно. 10^6 офисных пользователей в системе? хм... это что за ТНК такая, с таким количеством персонала, где просто столько офисных работников... и — 10^2 req/s — как-то маловато для такого количества работников.

Или что вы предлагаете?

Для чего, для вывода «Hello world»?

Как я предполагаю, у вас другой энтерпрайз, настоящий.

ну точно не тот смешной что вы описываете. Я такой и представить не могу, чтобы 10^6 пользователей, и никаких проблем с физичискими ограничениями вычислительной техники. а «чистая красота логики» :)

Конечно, если начинается логика построенная на агрегированных значениях ORM не подходит.

О как оказывается. а в энтрепрайзе значит такое — агрегированные значения редкость. всякие — остатки на начало периода. оборот за период. средняя цена, ...

а базисты мне удивлённно «зачем это тебе? База закрыта, твоё дело данные сюда, в одну специально открытую stored procedure, класть».

Да. БД как сервер приложений в энтерпрайзе — частая история.

Чем вас Hibernate так укусил,

Да просто постоянно его курочить приходилось. Влезать в кишки и обходить. И подобные.
А так — классная штука :)

Что не так с N+1

Это не ко мне. а к азбучным истинам работы с РСУБД.
У древовидных, типа MUMPS на которой тоже писал в двух разных компаниях такой проблемы нет. Там правда другие проблемы.

Какой контекст/какую задачу решаем?

как из O(n) сделать O(1)

в общем виде:
то есть чтобы количество запросов было равно количеству foreach
а не сумме количества элементов во всех коллекциях.

Вряд ли база ляжет от 5 * 300 = 1500 простых
ложится. даже нетворк внутри дата центра — ложится :)

Гм, надо попробовать тест написать, сколько мой M1, допустим, с Postgres-ом на дефолтных настройках выдерживает. Или поискать готовый.

Я когда с базистами начинал кеширование обсуждать, тема не встретила интереса, сказали, что с их точки зрения БД не загружена. Отчёты на отдельной реплике, естессно.

ну точно не тот смешной что вы описываете.

Конкретику, заметим, никакую не приводите.

а в куче других сфер — вообще все что вы пишите — и с доплатой не нужно :)

Как говорится, и вам не хворать. Сферы разные бывают, да.

Или что вы предлагаете?
Для чего, для вывода «Hello world»?

Да, для хелло ворд. Да, для задачи типа «в департаменте Х поднять всем з/п на 10%». Напишите хоть строчку псевдо-кода, а то флейм поднадоел немного.

это что за ТНК такая

Прям за моим именем написано где я работаю.

О как оказывается.

Я нигде не говорил, что ОРМ — это затычка на все случаи жизни. Но заходы «ОРМ на помойку» меня триггерят, как видите. Но вообще надо заканчивать, наверное, я уже отвёл душу.

Да просто постоянно его курочить приходилось. Влезать в кишки и обходить.

Вы уже второй раз эту фразу пишите, а конкретную проблему не приводите.

У древовидных, типа MUMPS на которой тоже писал в двух разных компаниях такой проблемы нет. Там правда другие проблемы.

Да.

как из O(n) сделать O(1)
в общем виде:
то есть чтобы количество запросов было равно количеству foreach
а не сумме количества элементов во всех коллекциях.

Ответ тогда тоже в общем виде.
Никак.

Смотря для чего foreach.
Если для последующих апдейтов, то см. пример с поднятие з/п на 10%.
Если для селектов, агрегации и прочих репортов — это противоречие между самой сутью объектов и агрегатов из реляционных БД. Тогда просто отложить ORM в сторону (ну или максимум маппинг из него использовать). Не обязательно плевать в инструмент не подходящий для задачи.

Вот эти примеры из учебников, есть parents, есть children и N+1 проблема(!), а можно-то было parents LEFT JOIN(!) children одним запросом сделать! — это ерунда на практике.

Ещё могу multiset предложить, но у него очень ограниченный диапазон применимости.

Конкретику, заметим, никакую не приводите.

в смысле вы предлагаете запостить интернет?
ее же, конкретики немеряно в инете.
какая именно нужна?

Да, для задачи типа «в департаменте Х поднять всем з/п на 10%»

Ок. Итак, есть две таблички. Департаменты и сотрудники.
Колонка salary у сотрудников. Ну и увеличиваем ее.
Задача даже для литкода смешная, ниже категории easy.

Какие у вас проблемы с такой задачкой, что вам нужен ОРМ?

Прям за моим именем написано где я работаю.

Rakuten Kobo?
У любой даже некрупной компании — куча софта.
У вас не написано каким именно ПО вы занимаетесь.
Может рабочим местом табельщиков, для ведения учета штатных офисных уборщиков.

Я лишен дара телепатии.

Вы уже второй раз эту фразу пишите, а конкретную проблему не приводите.

Я стопицот раз уже писал какую :)

Ответ тогда тоже в общем виде.
Никак.

За умение знать как — я и получаю деньги :)

Смотря для чего foreach.

Пофик для чего.
Вопрос обсуждается — сколько запросов будет сгенерировано в зависимости от «способа записи» бизнес алгоритма.

Тогда просто отложить ORM в сторону (ну или максимум маппинг из него использовать). Не обязательно плевать в инструмент не подходящий для задачи.

Плюют в него потому что декларируемые возможности — не подходят для задач для которых декларируются :)

Вот эти примеры из учебников, есть parents, есть children и N+1 проблема(!), а можно-то было parents LEFT JOIN(!) children одним запросом сделать! — это ерунда на практике.

Ну ок, тогда просто в разных вселенных живем.
Я в основном в мире финансово-экономического ПО, предназначенного для офисных пользователей. С 90ых.
Проблема постоянная. Меняются базы, ормы, были АРМы с GUI, теперь с WebUI — та же история. На разных ЯП. что на Дельфи, что на Джаве, что на Пыхе, сейчас на Ноде.
Немного был в сфере и для хоум юзверей, где тоже расчетная часть большая, плюс реалтайма немного. Та же проблема сразу в лоб. Или по лбу, да, есть нюанс между «в» и «по».

В вашей вселенной такой проблемы с РСУБД в энтерпрайзе нет.
Ок. Нет предмета дискуссии тогда.

ее же, конкретики немеряно в инете.
какая именно нужна?

Ваш личный опыт. По такому шаблону.
1. Решал такую-то задачу.
2. Взял Hibernate, сделал так, результат не устраивает.
3. Полез в кишки (что это значит, кстати? Вы патчили хибернейт?) и обходил (надо полагать, изменяли какую-то конфигурацию). Результат всё равно не устраивает, тесты падают. Надеюсь, вы в курсе, как, например, смотреть количество запросов, которое Hibernate генерит?
4. Вот ссылка на гитхаб с примером иллюстрирующем проблему, вот код, вот тесты. ЧЯДНТ?

Я лишен дара телепатии.

Я тоже лишен. А вот код посмотреть могу.

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

Может рабочим местом табельщиков, для ведения учета штатных офисных уборщиков.

Вы же пишете «я — д’Артаньян, а Hibernate — кака, кто его трогает — фу». Что это за детская подначка была?

Ок. Итак, есть две таблички. Департаменты и сотрудники.
Колонка salary у сотрудников.

Да, именно так.

Ну и увеличиваем ее.

Вы не стесняйтесь написать строчку кода, пожалуйста, даже если это хелло-ворд. Тогда будет что обсудить. Я привёл три варианта решения, у каждого есть свои плюсы-минусы.
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);
Мой опыт говорит, что 2-ой потенциально хуже, я предпочитаю 1-ый.

Что вы предлагаете? Просто увеличить? Кодом? Код напишите-то или больше словами?

Какие у вас проблемы с такой задачкой, что вам нужен ОРМ?

Я написал выше, но вы либо не читаете, либо не понимаете, либо просто троллите.
Отвечаю второй и последний раз (больше, кстати, для ТС-а, если он это сообщение увидит).
Основых — две. Инкапсуляция и аудит. Поскольку у меня нет уверенности, что мы одинаково понимаем эти термины, особенно первый, разъясню.

Это только кажется, что измение з/п — это employee.salary = employee.salary + 10%. Либо потом добавится, либо сразу самоочевидно имелось в виду, что это только для тех, кто прошёл испытательный срок, а ещё нужно применить правила округления. Если вы работаете в финансово-экономическом мире, то представляете сколько там «если, при условии, но». Как, впрочем, и везде.

Я предпочитаю всю эту логику по возможности держать в increaseSalaryByPercent() методе.

Дальше. Инкапсуляция — это прекрасно, но возникает вопрос «а как теперь отобразить изменения объекта employee в БД?». Это может быть сложный объект, с коллекциями и коллекциями коллекций, в которых что-то поменялось/добавилось/убавилось. На этот вопрос есть разные ответы, я выбираю Hibernate, чтобы не писать лишний код.

Теперь аудит. В моём мире либо сразу, либо, гораздо веселее, потом, когда уже много кода написано, бизнесу захочется смотреть изменения — а, собственно, кто и когда поменял з/п Васе Пупкину с какого значения на какое. В том числе из-за этого я предвзят к подходу update employee set salary = salary * 1.1 where department_id = x;

С 90ых.

Опыт, к сожалению, не всегда приходит с возрастом. Бывает, что возраст приходит один.

Проблема постоянная. Меняются базы, ормы, были АРМы с GUI, теперь с WebUI — та же история. На разных ЯП. что на Дельфи, что на Джаве, что на Пыхе, сейчас на Ноде.

Весь это зоопарк ничего не значит. Покажите код.
Вот вам пример как надо делать. Код, бенчмарки, выводы.

А вот код посмотреть могу.

ну так отркройте туториал по интересующему вас вопросу.
там есть и код, и пояснения.

Или спросите чатгпт. У вас же нубские вопросы, они не требуют специального ответа.

Вы не стесняйтесь написать строчку кода,

Я не стесняюсь, а не понимаю какой смысл в копипасте кода с туториала?
Вы вот копируете. Нафига? Вы считаете что только вам доступны туториалы или что?

Это только кажется, что измение з/п

Знаете ли, у меня честный сертификат есть, полученный в уч центре 1С — по «Зарплата и Кадры Управление Персоналом».
Потому что когда был в 1Сном мире — у меня специализация была такая была.

Вы же поставили задачу не про зарплату. А нубскую:
Есть табличка в БД с полем которое надо увеличить.
Никакой «зарплаты» в вашей постановке задачи нет.
Это задачка на знание оператора UPDATE в SQL, если табличка в РСУБД. или аналогичного, если другой тип БД.

Это задача — на вывод строки «Hello world»

Тут нечего обсуждать. откуда у вас для такой задачки берется

в increaseSalaryByPercent() методе.

я х.з.

бизнесу захочется смотреть изменения — а, собственно, кто и когда поменял з/п Васе Пупкину с какого значения на какое

в вашей задачке ничего о бизнес требованиях не указано.

Когда приведете хотя бы описание домена как бизнес-аналитика, тогда можно будет хотя бы к ТЗ перейти.

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

Только нуб думает что он по куче кода — бытенько что-то поймет.

Опыт, к сожалению, не всегда приходит с возрастом. Бывает, что возраст приходит один.

ну да, ну да, вы ставите задачки для трейни, и уверенно рассуждаете о чьем-то опыте :)

Покажите код.

туториалы есть в гугле.
если вам самим сложно, например по :
www.postgresqltutorial.com

Ну и можете проверить себя потом на leetcode.com/studyplan/top-sql-50

Вот вам пример как надо делать. Код, бенчмарки, выводы.

Если нубу надо доказывать о проблеме SELECT N+1 — то ничего ему не надо доказывать. Он же даже гуглить не умеет.

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

у меня честный сертификат есть, полученный в уч центре 1С

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

Есть табличка в БД с полем которое надо увеличить.

Я так не говорил. Я говорил

И да, задачу типа «в департаменте Х поднять всем з/п на 10%» буду по возможности делать именно в стиле
repo.findAllBy(department)
.forEach(e -> e.increaseSalaryByPercent(10));
Это задачка на знание оператора UPDATE в SQL

Вы понимаете, что такое объекты? В контексте первой буквы из ORM? Инкапсуляция? Прямой доступ к полю? Сеттер? ООП? Понимаете, что строчка
update employee set salary = salary * 1.1 where department_id = x;
... это 1) та самая Leaky Abstraction, про которую вы ссылку приводили? 2) нарушение инкапсуляции?

Понимаете, что Hibernate и leetcode.com/studyplan/top-sql-50... как бы это сказать... ортогональны немного? Хотя знание SQL безусловно полезно.

Напишите, пожалуйста, какую-то ответную колкость, но так, чтобы было видно, что вы понимаете. Не «вопрос нубский, вот ссылка на википедию».

бизнесу захочется смотреть изменения — а, собственно, кто и когда поменял з/п Васе Пупкину с какого значения на какое
в вашей задачке ничего о бизнес требованиях не указано.

Ну вот, указал задним числом. Нужен аудит. Как его сделать?

Когда приведете хотя бы описание домена как бизнес-аналитика, тогда можно будет хотя бы к ТЗ перейти.

Вы реально в таком мире живёте, что пока аналитики всё не разжуют и не опишут, и ТЗ не составлено, кодить не начинаем?
Могу только усомниться и позавидовать.

Если нубу надо доказывать о проблеме SELECT N+1

У меня странное ощущение.
С одной стороны, вы вроде бы кинули неглупую статью про Вьетнам, цитата про Rich vs Anemic Model была уместна, в кишки Hibernate, говорите, лазили.
Я подумал, вы в теме, поделитесь опытом и нюансами.

С другой — вы же не свои приключения описываете, а ссылки кидаете, предлагаете гуглить, туториалы читать и т.д. Код не показываете.

Может вы просто где-то услышали «Hibernate, ORM, N+1, куча селектов, киллер БД, ко-ко-ко», а какие трейд-оффы за этим стоят и как в каких случаях решаются — не понимаете, потому что на практике не сталкивались?

Это точно не пранк? Я с ChatGPT разговариваю? Ну, повёлся, конечно, признаю.

Но теперь мне начинает казаться, что шутки про бывших 1С-ников не на пустом месте появились.

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

А шутки про джавистов — да, тоже ходят. Особенно классные в среде функциональщиков. Одна из них даже в общий мемасик превратилась — «ООП головного мозга».

Я же упомянул — чтобы вы в своей телепатии как-то попустились. А то ишь ты, мне о домене зарплата решили рассказать :D

Ну вот, указал задним числом. Нужен аудит. Как его сделать?

огласите весь список. интересно посмотреть как вы его всунете в форумный формат :)

А то что вы называете аудитом — делается кучей способов. И правильный — зависит от общей архитектуры системы.

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

Вы реально в таком мире живёте

вообще-то я бывал в роли не только аналитика.
И видел и как — давайте код писать, ничерта не понимая к чему и зачем. И как без подписанного ТЗ — ни строчки.
Всякое видел.

Вы понимаете, что такое объекты?
Это точно не пранк? Я с ChatGPT разговариваю?

«Если кто-то кого считает кретином — один из них точно кретин.
Возможно и оба»

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

Поделитесь, пожалуйста, идеями.
Ну вот, например, ТС сделал свой DataGateway, а к нему бизнес пришёл с таким вопросом.

Только, пожалуйста, без ожидания детального ТС на 100500 страниц. Всё на ваше усмотрение. Табличка со столбцами «кто, что, когда, старое_значение, новое_значение». Или сразу «нубский вопрос, сами гуглите», встаёте и уходите?

Поделитесь, пожалуйста, идеями.

диапазон решений от
пишем все действия в лог файлик, и когда надо читаем
до
поднимаем какой-нить Drools который вообще провалидирует допустимость действия.

Идей — тьмы. А как надо то? покажите если не ТЗ, то хотя бы бизнес хотелки :)

Или найдите телепата, который догадается, сколько у вас фонового контекста к вопросу.

Только, пожалуйста, без ожидания детального ТС на 100500 страниц.

за Hello worldaми — идите в гугл. Там тьмы примеров, от небольших статей, до книг в сотни страниц с описанием как проектируется такое, для вашего (я х.з. какого) домена. Потому что в разных доменах — немного, а то и много разные бестпрактики.

Или сразу «нубский вопрос, сами гуглите», встаёте и уходите?

Совершенно верно. Только нуб верует что существует какая-то Одна Идея, да еще такая что в 5 строчек ее можно описать.

Мало мальски опытный уже понимает что код все пишется и пишется, потому что контекст — меняет все.

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

Причина догматизма у людей, обычно от скудости опыта. Черта юношества потому — максимализм и идеализм.

диапазон решений от
пишем все действия в лог файлик, и когда надо читаем
до
поднимаем какой-нить Drools

Ура! Таки началась техническая дискуссия, спасибо.

А как надо то? покажите если не ТЗ, то хотя бы бизнес хотелки :)

Табличка со столбцами «кто, что, когда, старое_значение, новое_значение».
Записывать сюда всё, что приложение меняет в х.з. каком домене (сорри, слишком обще сформулировал) в других таблицах (лучше так сказать).
Варианты — 1) требование такого аудита известно сразу 2) появилось, когда уже написано пол-приложения.

Причина догматизма у людей, обычно от скудости опыта.

Поэтому и хочется понять нелюбовь и нападки — без всякого контекста — на Hibernate

Табличка со столбцами «кто, что, когда, старое_значение, новое_значение».

Ок, если так надо, если у вас так записано, а чаще не записано а витает в воздухе «дев гайд» — так и делайте.

Я ж ничего не знаю о проекте где такое надо.
Такое может быть и лишним, если изменения осуществляются сущностями аля Документ.

Варианты — 1) требование такого аудита известно сразу 2) появилось, когда уже написано пол-приложения.

1 — понятия не имею «как надо». Неизвестно мне — ни-че-го.
2 — еще хуже чем 1. Потому что к 1 добавляется — а как УЖЕ написано остальное. Потому что весьма желательно, чтобы реализация была в том же стиле. Даже если он такой себе. «Безобразно — но единообразно» лучше разнообразия подходов в реализации.

1 — понятия не имею «как надо». Неизвестно мне — ни-че-го.

Я тоже не знаю «как надо». Но, навскидку, могу предложить несколько вариантов.
1) если там «ООП или ДДД головного мозга», то, может, летают какие-то ивенты про изменения. Ну, и записывать их в аудит табличку. Но это не звучит как надёжный подход
2) триггеры на базе. Бронебойное решение, единственно, не соображу как туда прокинуть «кто» (делал изменение)
3) раз это Java (вы полиглот, я на других языках не пишу), то там скорее всего Spring Boot c Spring Data JPA. И как бы неправильно его не использовали, если только без sql update-ов, то любые фантазии аудита решаются на раз.

2 — еще хуже чем 1. Потому что к 1 добавляется — а как УЖЕ написано остальное.

Конечно, хуже. Потому что если Hibernate (или другой state ORM), то всё хорошо, см. пункт выше, а если нет — миллион SQL DML команд в коде — кроме триггеров ничего не могу придумать, чтобы развернуть update employee set salary = salary * 1.1 where department_id = x; в отдельные аудит-записи по каждому человеку.

Идей — тьмы.

Предложите, мне интересно.

Но, навскидку, могу предложить несколько вариантов.

ну, я самое главное не услышал — а зачем он нужен, аудит этот. какая его бизнес ценность.
без этого оценить «правильность» вариантов — тоже не берусь.
Они все правильные.
Как в дзен-притче:
Рынок
— А какой кусок мяса у вас лучший?
— У меня каждый кусок — лучший!

может, летают какие-то ивенты про изменения

может. и тогда такой и тотальный аудит может получиться полным враньем. Потому что пользователь делал одно, а какая-то подсистема внесла изменения согласно бизнес логике о том, о чем этот пользователь вообще не в курсе. Просто по его должности, роли — это изменение вне зоны его ответственности. И тогда — такой «простой аудит» — в мусорку.

триггеры на базе

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

если только без sql update-ов, то любые фантазии аудита решаются на раз.

и с sql update решаются. зависит от того как организовано размещение и использование SQL кода.

update employee set salary = salary * 1.1 where department_id = x

Если нудить, исходя из моих познаний домена зарплата — такое неверно в любом исполнении.
Минимум что обычно требуется для зарплатных алгоритмов — история изменений зарплаты.
Но, зависит от конкретных потребностей и бизнес требований.
Для примитивной системы расчета ЗП, уровня курсовой работы первых курсов сгодится вот так изменять зарплату.
да, и у сотрудника в его таблице не зарплата, а ставка, оклад, и прочая. если опять же — нудить.
Зарплата — это результат ее расчета :)

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

Ну, вот так бизнесу захотелось.
Делали CRUD-приложение, со логикой изменения всякого разного, а потом бизнес такой «стоп, нужен учёт и контроль. Это значение, вроде бы, правильное, но надо понять, откуда оно здесь взялось, что было до этого, кто и когда».

Просто по его должности, роли — это изменение вне зоны его ответственности. И тогда — такой «простой аудит» — в мусорку.

И да, и нет. Триггернул изменение все-таки этот пользователь, считаем, что он. Но бизнес может быть изобретателен в аудите, типа: поменялось значение в свойстве подствойства — но запишите как изменение парента.

Правда — думаю придется иметь тождественность пользователей системы и пользователей БД.

Теоретически — да, это отвечает на вопрос «кто», практически, боюсь, малореально до невозможности.

и с sql update решаются.

Как?

зависит от того как организовано размещение и использование SQL кода.

Приведите, пример, пожалуйста.

Если нудить, исходя из моих познаний домена зарплата

Я не возражаю, это ж просто пример. s/зарплата/оклад/g ко всему, что было выше.

Ну, вот так бизнесу захотелось.

я не понял что именно ему захотелось.
Если бы бизнес умел рефлексировать и формулировать свои желания — такая штука как «Сбор и управление требованиями» была бы бессмысленной

стоп, нужен учёт и контроль

Ессно. Он всегда так говорит.
Эти слова ни о чем. затерты до потери смысла. Я не собираюсь гадать что именно имеет ввиду конкретный бизнес.
Я — спрашиваю бизнес об этом.

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

ну ок, значит так.
«Кораблю которому все равно куда плыть — любой ветер попутный»

Как?

Зависит от существующего кода.

Приведите, пример, пожалуйста.

Какой именно из ста пицот примеров с гитхаба?
Если отсутствуют критерии даже на уровне хотелок бизнеса, то какие у вас критерии к техническим требованиям отбора «примера»?

это ж просто пример

Ну да.
И я вам о том что список таких примеров — не влезет даже в статью для ДОУ. без кода. с кодом — книжечку можно написать, со всеми этими «просто примерами»

Как?
Зависит от существующего кода.

Приведите, пример, пожалуйста.
Какой именно из ста пицот примеров с гитхаба?

Свой. Не надо гитхаб. Я рассказываю свои решения, вы — свои.
Или вы про свой репозиторий на гитхабе? Тогда, наоборот, показывайте скорее.

и с sql update решаются. зависит от того как организовано размещение и использование SQL кода.

Типа: я sql update-ы организовываю и размещаю так. Поэтому если нужен такой странный аудит, сделаю то-то.

Свой

С какого из своих проектов?

Я рассказываю свои решения

Мои решения проистекают из требований и кучи намеками указанных условий.

Я рассказываю свои решения,

Не увидел ни одного, которого не видел в туториалах :)
Чем они — «ваши»?

Типа: я sql update-ы организовываю и размещаю так.

Древнее правило, еще с процедурных языков:
стягивайте SQL код в один, или несколько модулей.
Оборачивайте его в процедуры.
И дальше, если надо, рефакторите эти процедуры, и работайте с ними.

Только на очень простых проектах я видел практику когда SQL код лежит где попало.

Так что вам рассказывать, зачем в языках программирования процедуры? Как их применять, и прочее?

С какого из своих проектов?

Из любого, на ваш выбор.

Чем они — «ваши»?

Я не претендую на новаторство, только стараюсь правильно использовать инструменты.

стягивайте SQL код в один, или несколько модулей.
Оборачивайте его в процедуры.

Ок, есть модуль, есть процедура, в ней.

update employee set salary = salary * 1.1 where department_id = x;

Если не так, или не правильно понял, поправьте всё что угодно.

Как из этого sql кода получить тот аудит, о котором у нас вроде бы начал получаться разговор? Т.е. чтобы при вызове процедуры также летели insert-ы в табличку аудита «кто, что, когда, старое_значение, новое_значение»?

Из любого, на ваш выбор.

не вижу смысла напрягаться делать выбор.
работа программиста в том немало состоит — постоянно делаешь выбор. вы мне работы решили отсыпать? :)

Ок, есть модуль, есть процедура, в ней.

зайдите в эту процедуру и добавьте INSERT в предложенную вами табличку.

Как из этого sql кода получить тот аудит,

выше сами написали и другие варианты.

Т.е. чтобы при вызове процедуры также летели insert-ы

Если нужны такие insertы а их нет, то их где-то нужно написать. или что-то откунфигурировать.
Само по себе — то чего не было — не появляется. нужно что-то сделать чтобы появилось.

Это не так, вот в меру своих скромных сил пытаюсь объяснить.

Вот по ссылке классический пример — геттеры и сеттеры (а также добавление и удаление из коллекций) не делают из куска данных объект.

Бизнес-логику, вестимо.

Тогда моками. Ну или базу подключать.

А как по-другому?

База данных (или storage layer в общем) возвращают ответ на вопрос. Поэтому нужно делать иерархию PODов, которые будет возвращать layer.

Проблемы юзкейсов: сложно отделить DAO от бизнес-логики.

Проблема в том, что у CRUDов почти всегда нет бизнес логики, вообще. Круд — это когда интерфейс (REST, GUI, CLI, etc) стыкуется почти напрямую с DAO (плюс какие-то базовые проверки валидации).

Есть таски с сабтасками и сабтасками сабтасок, иерархия не ограничена.
Статусы: new -> in progress -> done
Юзкейсы: в done можно переводить, только если сабтаски закрыты.
Но с force=true можно перевести в done весь подграф.

Есть несколько методов представления дерева в SQL, например методом отрезков. Некоторые СУБД позволяют рекурсивные запросы: learnsql.com/...​recursive-tree-traversal.
Таким образом метод который будет переводить таски пачкой в done будет уникальным для каждого стореджа.

не делают из куска данных объект.

Не понимаю. Вот есть объект (уточняем, энтити, конечно). Hibernate может сохранить его (и достать) в/из БД. Почему вы утверждаете, что

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

?

База данных (или storage layer в общем) возвращают ответ на вопрос. Поэтому нужно делать иерархию PODов, которые будет возвращать layer.

Это, извините, не понял. Не затруднит псевдокодом проиллюстрировать, пожалуйста?

Проблема в том, что у CRUDов почти всегда нет бизнес логики, вообще.

Наверное, я не так понимаю определение. Для меня CRUD — это, когда есть БД, с данными которой происходит это самое CRUD. Короче, записываю сюда весь кровавый энтерпрайз.

Есть несколько методов представления дерева в SQL

Вопрос был не про собственно граф тасок — это просто пример, чтобы не плодить многие сущности — а про дозагрузку данных.

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

Т.е. бизнес-логика утекла в сторедж? Или ей норм там быть?

Т.е. бизнес-логика утекла в сторедж? Или ей норм там быть?

Как шутил один в меру известный программист, неплохой блог у него:
Вот же удивительно, устраивался на работу Java программистом, а с каждым годом пишу все больше на PL/SQL

Oracle/Postgre как сервер приложений — вполне часто в сложных и нагруженных системах.

Короче, записываю сюда весь кровавый энтерпрайз.

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

А CRUD — это когда мы работаем с БД практически как с электронной таблицей.

На одном из проектов чтобы не морочиться с админкой — я так и сделал, ввод кучи данных был в гугл таблицах. По несложным правилам описывались там же отношения.
И по кнопочке все это затягивалось. с валидациями на обновление,создание и т.п.. Сэкономили на разработке UI знатно :)

Это и была — CRUD часть базы. Самая маленькая по количеству таблиц и занимаемых данных. И самая простая в плане бизнес логики.

Вот есть объект (уточняем, энтити, конечно). Hibernate может сохранить его (и достать) в/из БД.

А если их 100 то 100 раз.
А если при сохранении одного объекта есть запись еще в две таблицы то будет 100+100+100 запросов.
5 пользователей — и база легла.

Не понимаю. Вот есть объект (уточняем, энтити, конечно). Hibernate может сохранить его (и достать) в/из БД.

Если у вас есть 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 — это, когда есть БД, с данными которой происходит это самое CRUD.

Нет, CRUD — это впервую очередь софт, который заточен на редактирование данные в БД в специализированном интерфейсе. Например: форум, новостной сайт, соцсеть и т.д. и т.п. Грубо говоря, если отбросить валидацию данных и прав доступа, то клиенты могли напрямую гонять запросы в базу и потеряли бы только в графической части.

Т.е. бизнес-логика утекла в сторедж? Или ей норм там быть?

Это не бизнес-логика. Это сторедж логика. Бизнес-логика была бы в обсчете и создании новых данных на базе выгруженных. Например, из базы выстаскиваются данные для настройки сети и пользователей. Вот «настройка» — это и есть бизнес-логика.


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;
};
Если у вас есть Java «object» который состоит из геттеров-сеттеров, которые можно довольно легко выкинуть (просто вывернув в паблик внутренние члены), то это не «объект» в смысле ООП, а просто структура (запись, struct, record).

Не очень понимаю, зачем вы мне это пишите, я полностью согласен и категорически против использования геттеров-сеттеров в коде.

Более того, я считаю чудовищной проблемой в джава-мире и не только, что создателям Java Beans конвенции было, видимо, очевидно, для чего они будут использоваться, а огромное число разработчиков видят в них «инкапсуляцию», к сожалению.
У меня стоят @Getter-ы на классами, да. Это для MapStruct-а. Не для бизнес-логики.

У такого «объекта» нет инкапсуляции, т.к. через геттеры-сеттеры все кругом ковыряются в его кишках, и никаких других «собственных» действий этот объект делать не может.

Если хотите, можете с ТС-ом поговорить на эту тему.

struct TaskHeader

Извините, я не понял идею. Сначала вы вроде бы за ООП (и я тоже), потом говорите, что с БД читаете структуру.

virtual Task getTaskById(int id) = 0;

Это я так предполагаю, что-то вроде интерфейса.

И как теперь это всё соединить и прийти к имплементации юзкейсов?

Юзкейсы: в done можно переводить, только если сабтаски закрыты.
Но с force=true можно перевести в done весь подграф.

?

Нет, CRUD — это впервую очередь софт... то клиенты могли напрямую гонять запросы в базу и потеряли бы только в графической части.

Я не очень люблю спорить про терминологию (предпочитаю смотреть на код), но, видимо, придётся, чтобы говорить на одном языке. Могу принять любое ваше определение.

Вот когда с одной стороны REST API, с другой — БД, а посреди джава-код со всяким разным, это что? CRUD, bloody enterprise, web-application, backend? Я согласен на любое название.

Это не бизнес-логика. Это сторедж логика. Бизнес-логика была бы в обсчете и создании новых данных на базе выгруженных.

А вот этот момент давайте ещё раз пройдём, пожалуйста.
Требование, что таску нельзя переводить таску нельзя переводить в DONE, пока сабтаски не в DONE — это что? Бизнес-логика, какая-то другая логика, констрейнт, инвариант? Как это правильно называть? Приложение, реализующее это требование — это ещё CRUD или уже нет?

В принципе, если дать пользователю доступ в БД, он и сам может, конечно, проверять это условие, перед редактированием статуса. Но тогда и я мог бы в Privat24 честно обновлять баланс счёта после транзакций, но почему-то нет такой возможности.

Ну и чтобы два раза не вставать про терминологию. Вот это, я так понимаю, вам недостачно класс, наверное, потому что вы в шапке видите @Getter. Покажите, пожалуйста, настоящий класс (энтити), и я сразу говорю, что Hibernate может сохранять/загружать его экземпляр в/из базу. Это я про ваше

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

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

Извините, я не понял идею. Сначала вы вроде бы за ООП (и я тоже), потом говорите, что с БД читаете структуру.

Я же говорю — data access layer возвращает структуры, а не объекты, именно потому что у этих структур нет поведения и инкапсуляции. ООП возникает когда на основе этих структур создаются объекты, у которых как раз есть поведение.

Требование, что таску нельзя переводить таску нельзя переводить в DONE, пока сабтаски не в DONE — это что? Бизнес-логика, какая-то другая логика, констрейнт, инвариант? Как это правильно называть? Приложение, реализующее это требование — это ещё CRUD или уже нет?

В терминах MVC, это — контроллер. И да, это всё ещё CRUD.

В принципе, если дать пользователю доступ в БД, он и сам может, конечно, проверять это условие, перед редактированием статуса. Но тогда и я мог бы в Privat24 честно обновлять баланс счёта после транзакций, но почему-то нет такой возможности.

Авторизацию и аутентификацию юзеров сложно назвать бизнес-логикой.

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

Вы из БД в принципе ничего другого получить не можете. БД отвечает на запросы, оно не создает объекты. Создавать объекты должен кто-то другой.

Наверное, последняя отчаянная попытка донести мысль.

Вы из БД в принципе ничего другого получить не можете. БД отвечает на запросы, оно не создает объекты.

Ну, технически, конечно, да. И из файлов и JSON-ов тоже объекты не получить, поскольку это байты.

Создавать объекты должен кто-то другой.

Ну, вот Hibernate это и делает. Читает данные из БД, создаёт экземляр класса и засовывает данные в него. Для этого ему, кстати, и нужны пустой конструктор с сеттерами. Да, это прямое нарушение инкапсуляции, поэтому никто другой их трогать не должен. Поэтому, чтобы не смущать слабых духом коллег, лучше их не добавлять. Hibernate-у нормально без сеттеров. И даже без пустого конструктора.

Поэтому, считаю, здесь читаются объекты из БД.

Требование, что таску нельзя переводить таску нельзя переводить в DONE, пока сабтаски не в DONE...
В терминах MVC, это — контроллер. И да, это всё ещё CRUD.

Ну, вот вы считаете, что это контроллер, ТС считает, что это интерактор (вы, кстати, открываете ссылки которые я вставляю?) и его абсолютно не смущает прямой доступ к кишкам таски, а я считаю, что это бизнес-логика (пофиг на название), главное что это метод класса Task. Я инкапсуляцию понимаю именно так, неважно кто и откуда будет этот метод вызывать, бизнес-консистентность не нарушится.

Что вы предлагаете делать после чтения структуры

virtual Task getTaskById(int id) = 0;

... я так и не понял.

ТС считает, что это интерактор (вы, кстати, открываете ссылки которые я вставляю?) и его абсолютно не смущает прямой доступ к кишкам таски, а я считаю, что это бизнес-логика (пофиг на название), главное что это метод класса Task.

Ну це класичний holy war «anemic vs rich domain model», Sergey Lysak вже згадував.
Є прихильники як одного підходу, так і іншого, багато матеріалу на цю тему в мережі. На dou також були теми.
Ось навіть картинки якісь Гугл видає — dev-to-uploads.s3.amazonaws.com/...​/9k46k3h6tli86azv7ry7.png

З власного досвіду, якщо проект не дуже великий, нехай абстрактно до 50 людино/років, є код рев’ю, тех.лід не міняється кожні пів року, то байдуже, anemic (з логікою в сервісах) чи rich model. Якщо якийсь здоровий ентерпрайз де важко за всім угледіти і дати по руках, то rich мабуть краще.

holy war «anemic vs rich domain model»

как пример, таких текстов немеряно

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 — светлая вера в Единого и Истинного Бога — когда умиляет, когда — «о, очередной великий»...

как пример, таких текстов немеряно

так я знаю, читано-перечитано в своё время и из практики для себя выводы сделал.

Если ваше приложение простое, то anemic.

от це іде в розріз з думкою більшості.

адже загальний підхід: якшо в тебе простий круд то використовуй Rich Domain Model

чи я не вірно зрозумів шо є шо?

от це іде в розріз з думкою більшості.

Так, про це й писав — «ефект підручника».
А цей GlebZ написав свою особисту думку, з практики.

якшо в тебе простий круд то використовуй Rich Domain Model

На практиці для CRUDа я рекомендую ActiveRecord, а не Hibernate-Doctrine. Додає зручності, але не такою ціною як в тру ормах.
і у ActiveRecord зазвичай легко залізти на рівень нижче, як треба.
і легко як втулити бізнес логіку у моделі AR, так і витягти з них.

Прикол в тому шо з моєї практики написання простого телеграм боту Anemic теж виявилась єффективнішою.

реализация функциональности за меньшие деньги

я реалізовував тільки те шо було потрібно у моменті, легко вносив суттеві зміни бо елементи системи прості та слабко залежать один від одного.

Прикол в тому шо з моєї практики написання простого телеграм боту Anemic теж виявилась єффективнішою.

...а ще є
10. Оптимізуйте розмір іміджів, щоб вони завантажувалися швидше
...видалення з іміджу зайвих даних та пакетів;
20 способів оптимізації витрат на AWS, які легко допоможуть заощадити 80+ % бюджету
dou.ua/forums/topic/46648

І от, для лямбди обробки таблиці з тасками — затягуємо в неї «Hibernate». щоб православний код був.

Хорошая цитата, почти со всем согласен, но это

2. Инкапсуляция. ...те изменения, которые касаются состояния, также отслеживаются компиляцией со статической типизацией в Anemic

... не понял. Это как? Как статическая типизация связана с состоянием объекта? Что через в сеттер числа строку не подложить? Такая себе защита состояния. Или как это понять?

Может честнее писать «Инкапсуляция — её нет на совести разработчиков в интеракторе (в терминах ТС)»?

В Anemic приходится отслеживать, чтобы в ворохе сервисов не делали свои велосипеды.

Вот как практически предлагается это делать? На код-ревью? В проекте сидит гуру, знающий всю кодовую базу?

И, каждый год, вчерашний джун выросший до мидла

... сначала бросает «ОRM на помойку» чтобы пофлеймить, хотя вроде бы

делал и по подходу X, и по подходу Y

а в другом месте — it depends on.

Ну це класичний holy war «anemic vs rich domain model»

Так, звісно. На мою думку, перця додає той факт, що чи то немає чіткого визначення цих понять, чи все одно кожен чує щось своє.
От rich це обов’язково active record entity.save()? Якщо repo.save(entity) то це ще rich чи вже anemic?
Чи різниця між rich чи anemic в тому хто state виставляє?
Чи rich це коли при зміні цього state сама «модель» може листа, наприкрад, відправити?

Я зразу чесно зізнаюсь, що термінологією не володію, і тому не впевнений в якості комунікації.

З власного досвіду, якщо проект не дуже великий, нехай абстрактно до 50 людино/років, є код рев’ю, тех.лід не міняється кожні пів року,

Хіба позаздрити можу. Для мене це поруч з країною рожевих поні. Все життя в кривавому ентерпрайзі (а що таке ентерпрайз? яке визначення?). Тому я тяжію до rich та ORM.

Ну і безвідмовний спосіб розпочати флейм це накинути «фреймворк Х — на смітник». Приклад1, приклад2, приклад3 (а, ні, тут відносно нормальна дискусія).

Я не очень люблю спорить про терминологию (предпочитаю смотреть на код)

Звучит примерно как — не люблю спорить об идеях изложенных в книгах. Предпочитаю подсчитывать количество букв, слов и страниц — потому что это — точная, объективная информация! а идеи — мутные любые все...

Вот когда с одной стороны REST API, с другой — БД, а посреди джава-код со всяким разным, это что? CRUD, bloody enterprise, web-application, backend?

А что такое REST API, а что такое не REST API? ;)
что означают эти буквы REST, и как глядя в код увидеть что он реализует именно REST API, если «не люблю спорить про терминологию»?
REST ведь это — идея, та самая «терминология». Не имея, не зная этой идеи, ее в коде невозможно увидеть :)

с другой — БД, а посреди джава-код со всяким разным, это что? CRUD, bloody enterprise, web-application, backend?

это что? соленое, тяжелое, красное, дорогое, длинное?
То есть вот такое вы поставили через запятую «или»

Звучит примерно как

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

это что? соленое, тяжелое, красное, дорогое, длинное?

Гусары, молчать!!

P.S. Чуть разгребусь и отвечу на ваши другие сообщения, где есть что обсудить по технической части.

Чуть разгребусь и отвечу на ваши другие сообщения, где есть что обсудить по технической части.

Давайте, вдруг напишите что-то новое с апологетики ОРМов что не читал еще за последние лет 10-15 :)

Ну почему сразу на мусорке :) Внутри Data Access Layer, для многих случаев вполне себе может жизнь упростить. Там же его можно и комбинировать с написанными с любовью SQL запросами )) Главное не светить всей этой радостью наружу интерфейсов репозиториев ни в явном ни в неявном виде. Может разве что за исключением рид моделей, и то я бы поспорил, слишком неявно когда один наделает цепочек свойств с той же ленивой загрузкой, а другой гуляет по ним без задней мысли что бомбардирует базу десятками запросов.

Дисклеймер. Я палкий прихильник Hibernate, і коли чую ORM, насамперед маю на увазі цю імплементацію. Я в курсі, що є інші, з не таким багатим функціоналом.

Колись на співбесіді мене спитали: «А як ти вчишся?». І нічого іншого, як відповісти «На практиці», я не придумав.

Аналогічно.

поділись досвідом у коментарях.

Стадіон так стадіон.

ORM — object relational mapper. Приблуда, яку придумали ліниві розробники, які не хотіли вчити SQL

Перепрошую, не mapper, а mapping. Це я не прискіпуюсь до слів, а хочу сказати, що ORM — це більш широка абстракцію/технологія.

А якщо я веду розробку від бізнес-логіки? То мене не цікавить, як саме імплементовано рівень сховища

Це все гарно і добре, я закликаю до того самого.

Відокремлення джерела отримання об’єкта означає, що цих джерел може існувати нескінченна кількість і вони можуть мати будь-які реалізації.

А от стосовно цього у мене певні сумніви.
Взагалі, аргументи статті дуже схожі на ДДД, який, з одного боку, мені симпатичний, з іншого — ні, бо не знайшов відповіді на практичні питання.
Якщо ліньки читати треди в іншому топіку, повторюсь тут.

Як тільки згадується SQL, питання, що робити з lazy-loading-ом. Як будуть виглядати оці всі getUser, getDog при умові, що там будуть більш-менш складні структури, а не навчальні три поля з ID? Будемо витягати з бази все одразу (страждає перформанс) або при потребі (ускладнюється код, змішується власне бізнес-логіка з DAO)?

Я з NoSQL не працював, схоже, там такої проблеми нема, або вона не така гостра.

Щоб краще проілюструвати проблему, навіть наваяв маленький проект з ієрархією тасок. З нетерпінням чекаю його кращої реалізації без ОРМ, але з DataGateway (хоча б на рівні ідеї).

Щоб краще проілюструвати проблему, навіть наваяв маленький проект з ієрархією тасок.

С втруктурі вашого проєкту все завязано на визначенні Таски. Більше 120 рядків коду. А репозиторій — 2 рядка. От і виходить проєкт з захмарно високим cupling, це прям монолітний моноліт.

Взагалі, аргументи статті дуже схожі на ДДД

Бо підход проектування через бізнес логіку це він і є. Саме в ньому використання розділення сховища і сутностей більш природнє, але не обовязково тільки там.
З ДДД проблема в тому що це не річ яку приймає окремо розробник, це не те як називають класи чи групують файли у проєкті. Це методологія ведення розробки програмного продукту у яку залучені усі хто дотичні до розробки програмного продукту.

Але якшо чесно мене менше цікавить ДДД. А більше цікавить окреме інжененерне рішення — дозволяти ORM протікати в бізнес-логіку чи ні. І я наївно сподівався шо хтось з читачв ДОУ під цею статтею напише шось на кшалт: «О, а ми в себе теж використовували datagatewy, тіки в нас то repository. Норм штука всім раджу, був тіки косяк з .... але ми пофіксили так ....».
Ех :(

що робити з lazy-loading-ом

Те саме шо робили 5-10-15 років тому: використовувати або курсор або офсети.
Увімкни лог на базі і в живу побачиш як @ManyToOne(fetch = FetchType.LAZY) данні підтягує.

З нетерпінням чекаю його кращої реалізації без ОРМ, але з DataGateway (хоча б на рівні ідеї).

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 надасть нам обєкти для наших маніпуляцій. Спочатку бізнес логіка, потім імплементація джерел данних.

Я ж розмітив код тегом.
Чому ДОУ зїв ентери?

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(.....) }
})


Таке враження, що у нас сильно відрізняються світи, це буде або цікава або безглузда дискусія.

С втруктурі вашого проєкту все завязано на визначенні Таски. Більше 120 рядків коду. А репозиторій — 2 рядка.

Ем, а що тут поганого-то? Я так розумію ООП — є клас, його проперті та методи. Що не так?

це прям монолітний моноліт.

(витріщає очі) Чому моноліт?!

Я вже боюсь питати, як ти розумієш coupling, бо невідомо куда зайдем.

Бо підход проектування через бізнес логіку це він і є. Саме в ньому використання розділення сховища і сутностей більш природнє, але не обовязково тільки там.
З ДДД проблема в тому що це не річ яку приймає окремо розробник, це не те як називають класи чи групують файли у проєкті. Це методологія ведення розробки програмного продукту у яку залучені усі хто дотичні до розробки програмного продукту.

З цим категорично згоден.

І я наївно сподівався шо хтось з читачв ДОУ під цею статтею напише шось на кшалт: "О, а ми в себе теж використовували datagatewy, тіки в нас то repository

Мені так само цікаво, як з Datagateway(DAO?) вирішується питання, коли в залежності від бізнес-логіки треба дозавантажувати (або ні) додаткові дані з SQL DB.

тут в твому коді циклічна залежність яку на проді жоднен адекватний архітектор не дозволить, тому замість обєкту таски просто ID parent:number

(знову витріщає очі) По-перше, це не циклічная залежність, а ієрархічна. А хай би і циклічна залежність — у доменній моделі може бути що завгодно, якщо так є по бізнесу. Ніяких обмежен. У чому проблема?

І чому архітектор рев’юіть код? ;-)

class TaskWithSubs extends Task

Для чого два класи на одну бізнес-сутність?

Class Service

Що робить сервіс? Це просто проксі до storage?

З такми підходом файл Task.js тепер короткий а storage.js здоровий.

Тобто Task.js — це тільки структура даних? А бізнес-логіка де? В сервісі? Чи в сторажі?

Закон збереження енергії в дії.

Ага. А тестуємо що? Storage? З моками? Чи з in-memory DB?

Спочатку бізнес логіка, потім імплементація джерел данних.

А що робити, коли в залежності від бізнес-логіки треба (або не треба) завантажувати дані?
В якому шарі це робити?

Як тільки згадується SQL, питання, що робити з lazy-loading-ом. Як будуть виглядати оці всі getUser, getDog при умові, що там будуть більш-менш складні структури, а не навчальні три поля з ID? Будемо витягати з бази все одразу (страждає перформанс) або при потребі (ускладнюється код, змішується власне бізнес-логіка з DAO)?

Не зовсім вдало сформулював, спробую ще раз. Для мене основна супер-сила ORM Hibernate не в селектах, а в апдейтах, тобто

@Transactional
class Service {
    public void someMethod(Changes changes) {
        CompexEntity compexEntity = repository.findById(id).orElseThrow();
        complexEntity.updateBy(changes);
    }
}

CompexEntity може бути яким завгодно складним об’єктом з внутрішніми колекціями і колекціями колекцій. В update(...) може відбуватись складна логіка, яка потребує (або ні) дозавантажання внутрішніх пропертей. Тестується це все добро unit-тестом без жодних контекстів, а решту робить ORM.

що робити з lazy-loading-ом
Те саме шо робили 5-10-15 років тому: використовувати або курсор або офсети.

Під lazy-loading-ом (у контексті ORM) я мав на увазі, що замість завантаження даних в properties створюються проксі, які відпрацьовують при звертанні до тих.

Отой

в живу побачиш як @ManyToOne(fetch = FetchType.LAZY) данні підтягує.

... або не підтягує, якщо не треба.

Щоб проілюструвати про що йде мова, такий юзкейс: зміна статусу таски.
У complete переводимо, якщо всі дочірні сабтаски закриті. Або з force=true закриваємо всю ієрархію. Питання — скільки даних завантажити з БД для цього?

Ем, а що тут поганого-то?

Спробуй розверни усі декоратори які навішані на класс таски, побачиш наскільки перегружений логікою цей обєкт. Чим складніший класс — тим важче його змінювати в майбутьному.

Для чого два класи на одну бізнес-сутність?

Шоб позбутись тої проблеми коли завантажеється лишня інфа де її не треба. Просто з моєї точки зору то дві різні бізнес сутності які затребувані в різних бізнес юзкейсах.
В твоїй реалізаціїї, клас = таблиці. і це якраз той coupling про який я казав, система більш гнучка коли це не так.

Під lazy-loading-ом (у контексті ORM) я мав на увазі, що замість завантаження даних в properties створюються проксі, які відпрацьовують при звертанні до тих.

Догнав. ІМХО це страшне зло, але то вже персональні вподобання.

Про розділення логіки. ХЗ як краще, але зазвичай якшо це суто властивостей класу сутності — то в клвсі сутності. Якшо це про маніпуляції над сутнісю чи між декількома — то в інтеракторі.
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')
        }

    }
}

Просто в мому варіанті логіка в інтеракторі, і у випадку якшо при зміні статусу таски треба ще якісь діії виконувати, наприклад відправити листа асайні сабтаски, то це леко додати. Не треба тягнути у класс таски всю логіку відправки листів.

Поправочка

Service — це інтерактор, тут описуються юзкейси маніпуляцій над таском,

над таском, або над таском та іншими сутностями повязаними цим юзкейсом.

Спробуй розверни усі декоратори які навішані на класс таски

Не зрозумів. Про які декоратори йде мова?

наскільки перегружений логікою цей обєкт

Знову не зрозумів. Ось клас. Два конструктори-аліаси, три публічних метода — addSubTask, updateStatus, patch. Про яке перевантаження йде мова?

В твоїй реалізаціїї, клас = таблиці.

Коли зв’язані дані розмазані по різних сховищах — це в любому випадку буде непросто. Але буває і таке, що у додатка одне сховище — реляційна БД.

Під lazy-loading-ом (у контексті ORM) я мав на увазі, що замість завантаження даних в properties створюються проксі, які відпрацьовують при звертанні до тих.
Догнав. ІМХО це страшне зло, але то вже персональні вподобання.

У нашій справі нема чистого зла і добра, переважно компроміси. Я не вважаю lazy-loading злом, бо в моїх кейсах це суттєво спрощує і пришвидшує розробку.

Service — це інтерактор, тут описуються юзкейси маніпуляцій над таском, або над таском та іншими сутностями повязаними цим юзкейсом, які від нас просить продактовнер.

Чудово. Тобто Task — це структура сутність, а Service — це набір процедур інтерактор. У процедурно-орієнтовному програмуванні як такому нема нічого погано, але постає питання, як тестувати цей інтерактор.

Ти ось написав метод

changeStatus(taskID:number, status:TaskStauts, forcce?:bool)

... там if-и, цикли — такі речі треба тестами покривати.

Як буде виглядати тест?

З моками? Чи з in-memory DB?

Я свої тести вже написав.

Так само в обох варіантах ... чи я шось не розумію.

Мабуть недостатьо зробив акцент. У тасок можуть бути сабтаски, в тих — ще, вкладеність графу — необмежена.

class TaskWithSubs extends Task{
subtasks:Task[]
}

... не підходить, там знову має бути 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

getALLOpenSubtasks(parentID):Task[] {
// SQL пошук я зараз не придумаю але думаю що це реально,
//як мінімум в циклі шукати через декілька запитів
//по навантаженню на базу це буде те саме шо твій лейзі лоад у циклі private void markDone

Не те саме. Витягувати всю ієрархію — це вкрай погана ідея з точки зору перформанса і пам’яті. Можна спочатку надовго задуматись, а потім вилетіти з OutOfMemoryException.
Для force=false достатньо витягти тільки «безпосередні» сабтаски, весь граф не потрібен.
І при апдейті з force=true не треба завантажувати сабтаски, якщо чергова таска вже в DONE. Ось я поправив цей момент, тест поруч.

На всяк випадок, нагадаю: обговорюємо не конкретну задачу — та скільки там тих тасок? максимум 100! — а загальний підхід, коли бізнес-логіка вимагає дозавантаження даних.

тепер троти TDD inda house

Ні-ні-ні, не треба, будь ласка, займатись самообманом. Ніякого TDD не було, тест писався після реалізації, про що також свідчить оговорочка по Фрейду

джун штампує тести для TaskService і робить їх зелененькими

Мій поінт не в тому, що треба писати спочатку — тести чи код — без різниці, а в тому що хороший тест — тестує black-box. А твої тести спочатку зазирають в код, щоб знати який мок підкласти.

mockStorage.getTask.mockReturnValue([main])
mockStorage.getALLOpenSubtasks.mockReturnValue([sub1, sub2])

Звідки тест знає що буде викликатись getALLOpenSubtasks?

Зверни увагу як мало мені довелось змінити в порівнянні з попереднім рішенням.

А тепер дивимось на рішення — до чого я вів з самого початку — це суміш бізнес-логіки з DAODategateway-ем.
Ще тобі треба в залежності від флага force викликати getALLOpenSubtasks чи getJustDirectSubtasks, ну і оптимізувати перший метод, щоб він не тягнув геть усе. А потім виправляти весь тест сют, тому що моки помінялися. Гадаю, ти з цим постійно стикаєшься, тому і питаєш

а якшо в тебе тест сют та класс Task з сотнею тестів, в ще є тести інших сутностей де таск використовується?(це я вже перегібаю)

Крім того є ще одна проблема.

task.status= status

Ти вільно апдейтиш статус. Тобто Таска — це просто структура, а не об’єкт; її внутрішній стейт виставляє сервіс інтерактор, і нема ніяких гарантій, що це робиться консистетно. Всі інваріанти таски — як відбувається перехід в DONE, можемо нафантазувати ще — розмазані в методах сервіса. Слово «інкапсуляція» залишилось на співбесіді, її — нема.

Не те, щоб я такого підходу не бачив, або сам, грішний, колись не притримувався. Так, колись здавалось, що логіка в сервісі, покрита моками репозиторієв — це вершина майстерності. Зараз вважаю що це не так.

Витягувати всю ієрархію

як ти зміг пекласти getALLOpenSubtasks у витягнути всю іерархію?
я ж написав шо там може бути логіка ідентична циклу у markDone.

Ніякого TDD не було

ок, тобі видніше .... менше з тим, тема дискуссії не TDD

Ще тобі треба в залежності від флага force викликати getALLOpenSubtasks чи getJustDirectSubtasks, ну і оптимізувати перший метод, щоб він не тягнув геть усе.

знову ні, в 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, я відкриваю його інтерактор і вся бізнеслогіка перед очима.

як ти зміг пекласти getALLOpenSubtasks у витягнути всю іерархію?

Без питань, я думав, що достатньо сказати «ієрархія» і «граф», але розжую.
Є таска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.

В markDone не тільки цикл, там лінива рекурсія, яка зупиться на тасці4.

з моє точки зору нічого не розмазано. якшо я хочу подививтись як веде себе Task, я відкриваю його інтерактор і вся бізнеслогіка перед очима.

А то я такого коду не бачив) Километровий метод в сервісі який жонглює напряму сетить проперті різних ентіті. Якийсь з флоу покритий тестами з моками, решта — ні, бо небуло часу. При зміні логіки треба міняти десяток тестів, які той шмат наче не тестують, але моки тепер інші.
Новій людині розібратись в цьому вкрай важко. Я з цим борюся за на користь OOP+ORM.

А то я такого коду не бачив) ...

зрозумів вашу позицію, дякую що поділилсь досвідом.

Дякую за дискусію, було цікаво

Не зрозумів. Про які декоратори йде мова?

оці:
@Entity
@Table(name = «tasks»)
@Getter
@Slf4j
@ManyToOne(fetch = FetchType.LAZY)
@OneToMany(mappedBy = «parent», cascade = CascadeType.ALL, orphanRemoval = true)

я буквально казав спробуй розвернути код усьої логіки.
З таким кодом можеш процювати тільки ти або якійсь мідл+. Джун без досвіду з Hibernate нічого зробити не зможе.

Це гарне питання, я буду передавати лямбду/колбек/consumer в updateStatus метод.

і як це вплине на твій існуючий тест markDoneForce?
а якшо в тебе тест сют та класс Task з сотнею тестів, в ще є тести інших сутностей де таск використовується?(це я вже перегібаю)

я буквально казав спробуй розвернути код усьої логіки.

Це не декоратори, а анотації.

З таким кодом можеш процювати тільки ти або якійсь мідл+. Джун без досвіду з Hibernate нічого зробити не зможе.

Будь-яка технологія — це трейд-оф між витратами на її опанування і користю, яку вона надає. Можно легко навести протилежні екстреми.

Запитання до автора. А додавання ORM в проект для безпритульних собак — це не жорстоке поводження з беззахисними тваринами?

Не таке вже і жостке якшо порівнуювати з примусом Jmeter-a ганяти безглузді тести віртуальних потоків.

джерел може існувати нескінченна кількість і вони можуть мати будь-які реалізації. Також це означає, що об’єкт не несе в собі багаж структур, повʼязаних із цим сховищем. Навіть якщо ти майстерно використовуєш dependecy injection — мокати окремий клас легше, ніж цілу ORM-бібліотеку з її інстансем

Гарні аргументи які пояснюють рубістам чому ActiveRecord не прижився і є, загалом на практиці, збоченням, а не вершиною думки, як їм здається.

Рубі тут ні до чого — це фішка конкретно рельс.

А рубі без рельс існує? 0.о

Звісно існує, навіть в вебі (одним з прикладів є як раз компанія ТС, але деталей не буде через NDA; можливо ТС захоче розповісти більше). І навіть рельси самі по собі ще не означають бездумного використання AR — я бачив кейси, коли AR повністю ховають за репозиторієм/query objects/... (компромісне рішення, бо незроуміло навіщо обирати фреймворк щоб потім з ним боротись, але це вже ортогональне питання).

Це ж питання навіть не про рубі, чи не так? А про те, що деякі люди роблять висновки про когнітивні здібності інших тільки на підставі інструментів, що використовуються :)

Одно із найбільш яскравих вражень під час використання ORM це коли до тебе приходять DBA і кажуть що ваш застосунок робить ось такий запит, він поганий, погано впливає на продуктивність сервера і його треба переробити, мабуть ось так та ось так. І починається гра «вгадай місце у C# коді по SQL запиту» з продовженням «зроби ORM згенерувати запит іншим чином».

Остаточної відповіді нема.

Факт.

Підписатись на коментарі