[{"data":1,"prerenderedAt":15209},["ShallowReactive",2],{"navigation_docs":3,"-java-pr2-specification-pattern":2949,"-java-pr2-specification-pattern-surround":15204},[4,1640,1765,2219,2352,2559,2641,2691,2748,2782,2908,2945],{"title":5,"icon":6,"path":7,"stem":8,"children":9},"C#","i-devicon-csharp","/csharp","01.csharp",[10,13,60,90,120,202,219,253,379,404,457,650,1346,1636],{"title":11,"path":7,"stem":12},"C# Roadmap","01.csharp/index",{"title":14,"icon":15,"path":16,"stem":17,"children":18,"page":59},"Fundamentals","i-lucide-book-open","/csharp/fundamentals","01.csharp/01.fundamentals",[19,23,27,31,35,39,43,47,51,55],{"title":20,"path":21,"stem":22},"Вступ до екосистеми .NET","/csharp/fundamentals/introduction-to-ecosystem","01.csharp/01.fundamentals/01.introduction-to-ecosystem",{"title":24,"path":25,"stem":26},"Структура програми на C#","/csharp/fundamentals/program-structure","01.csharp/01.fundamentals/02.program-structure",{"title":28,"path":29,"stem":30},"Змінні та Типи Даних","/csharp/fundamentals/variables-data-types","01.csharp/01.fundamentals/03.variables-data-types",{"title":32,"path":33,"stem":34},"Масиви","/csharp/fundamentals/arrays","01.csharp/01.fundamentals/04.arrays",{"title":36,"path":37,"stem":38},"Strings & Text Handling","/csharp/fundamentals/strings-text-handling","01.csharp/01.fundamentals/05.strings-text-handling",{"title":40,"path":41,"stem":42},"Дати і Час","/csharp/fundamentals/dates-time-handling","01.csharp/01.fundamentals/06.dates-time-handling",{"title":44,"path":45,"stem":46},"Потік Керування","/csharp/fundamentals/control-flow","01.csharp/01.fundamentals/07.control-flow",{"title":48,"path":49,"stem":50},"Методи","/csharp/fundamentals/methods","01.csharp/01.fundamentals/08.methods",{"title":52,"path":53,"stem":54},"Основи Відлагодження","/csharp/fundamentals/debugging-basics","01.csharp/01.fundamentals/09.debugging-basics",{"title":56,"path":57,"stem":58},"Інтерактивна Консоль (Classic)","/csharp/fundamentals/interactive-console","01.csharp/01.fundamentals/10.interactive-console",false,{"title":61,"icon":62,"path":63,"stem":64,"children":65,"page":59},"OOP","i-lucide-box","/csharp/oop","01.csharp/02.oop",[66,70,74,78,82,86],{"title":67,"path":68,"stem":69},"Package Management (Управління Пакетами)","/csharp/oop/package-management","01.csharp/02.oop/01.package-management",{"title":71,"path":72,"stem":73},"Класи та Об'єкти","/csharp/oop/classes-objects","01.csharp/02.oop/02.classes-objects",{"title":75,"path":76,"stem":77},"Властивості та Поля","/csharp/oop/properties-fields","01.csharp/02.oop/03.properties-fields",{"title":79,"path":80,"stem":81},"Стовпи ООП","/csharp/oop/oop-pillars","01.csharp/02.oop/04.oop-pillars",{"title":83,"path":84,"stem":85},"Advanced Types","/csharp/oop/advanced-types","01.csharp/02.oop/05.advanced-types",{"title":87,"path":88,"stem":89},"Namespaces (Простори Імен)","/csharp/oop/namespaces","01.csharp/02.oop/06.namespaces",{"title":91,"icon":92,"path":93,"stem":94,"children":95,"page":59},"Advanced Core","i-lucide-zap","/csharp/advanced-core","01.csharp/03.advanced-core",[96,100,104,108,112,116],{"title":97,"path":98,"stem":99},"Generics (Узагальнення)","/csharp/advanced-core/generics","01.csharp/03.advanced-core/01.generics",{"title":101,"path":102,"stem":103},"Делегати, Події та Лямбда-вирази","/csharp/advanced-core/delegates-events-lambdas","01.csharp/03.advanced-core/02.delegates-events-lambdas",{"title":105,"path":106,"stem":107},"Interfaces Deep Dive (Інтерфейси: Поглиблений Розгляд)","/csharp/advanced-core/interfaces-deep-dive","01.csharp/03.advanced-core/03.interfaces-deep-dive",{"title":109,"path":110,"stem":111},"Обробка Винятків","/csharp/advanced-core/exception-handling","01.csharp/03.advanced-core/04.exception-handling",{"title":113,"path":114,"stem":115},"Pattern Matching","/csharp/advanced-core/pattern-matching","01.csharp/03.advanced-core/05.pattern-matching",{"title":117,"path":118,"stem":119},"Додаткові Можливості C#","/csharp/advanced-core/additional-features","01.csharp/03.advanced-core/06.additional-features",{"title":121,"icon":122,"path":123,"stem":124,"children":125,"page":59},"Architecture Best Practices","i-lucide-building-2","/csharp/architecture-best-practices","01.csharp/04.architecture-best-practices",[126,130,149,153,157,161,165,169],{"title":127,"path":128,"stem":129},"Software Design Principles (Частина 1)","/csharp/architecture-best-practices/software-design-principles","01.csharp/04.architecture-best-practices/01.software-design-principles",{"title":131,"icon":132,"path":133,"stem":134,"children":135,"page":59},"Design Patterns","i-lucide-folder","/csharp/architecture-best-practices/design-patterns","01.csharp/04.architecture-best-practices/02.design-patterns",[136],{"title":137,"icon":132,"path":138,"stem":139,"children":140,"page":59},"Creational","/csharp/architecture-best-practices/design-patterns/creational","01.csharp/04.architecture-best-practices/02.design-patterns/creational",[141,145],{"title":142,"path":143,"stem":144},"Singleton (Одинак)","/csharp/architecture-best-practices/design-patterns/creational/singleton","01.csharp/04.architecture-best-practices/02.design-patterns/creational/01.singleton",{"title":146,"path":147,"stem":148},"Builder (Будівельник)","/csharp/architecture-best-practices/design-patterns/creational/builder","01.csharp/04.architecture-best-practices/02.design-patterns/creational/02.builder",{"title":150,"path":151,"stem":152},"Building Professional CLIs","/csharp/architecture-best-practices/building-professional-clis","01.csharp/04.architecture-best-practices/03.building-professional-clis",{"title":154,"path":155,"stem":156},"Validation & Flow Control","/csharp/architecture-best-practices/validation-flow-control","01.csharp/04.architecture-best-practices/04.validation-flow-control",{"title":158,"path":159,"stem":160},"The Modern .NET Host (Microsoft.Extensions)","/csharp/architecture-best-practices/modern-dotnet-host","01.csharp/04.architecture-best-practices/05.modern-dotnet-host",{"title":162,"path":163,"stem":164},"Data Mapper: Repository та DAO патерни (Частина 1)","/csharp/architecture-best-practices/data-mapper-part1","01.csharp/04.architecture-best-practices/06.data-mapper-part1",{"title":166,"path":167,"stem":168},"Data Mapper: Repository та DAO патерни (Частина 2)","/csharp/architecture-best-practices/data-mapper-part2","01.csharp/04.architecture-best-practices/07.data-mapper-part2",{"title":170,"icon":132,"path":171,"stem":172,"children":173,"page":59},"Di Ioc","/csharp/architecture-best-practices/di-ioc","01.csharp/04.architecture-best-practices/08.di-ioc",[174,178,182,186,190,194,198],{"title":175,"path":176,"stem":177},"Проблема залежностей та Інверсія Контролю","/csharp/architecture-best-practices/di-ioc/the-dependency-problem","01.csharp/04.architecture-best-practices/08.di-ioc/01.the-dependency-problem",{"title":179,"path":180,"stem":181},"Будуємо власний Service Container","/csharp/architecture-best-practices/di-ioc/build-your-own-container","01.csharp/04.architecture-best-practices/08.di-ioc/02.build-your-own-container",{"title":183,"path":184,"stem":185},"Service Locator: Паттерн та Анти-паттерн","/csharp/architecture-best-practices/di-ioc/service-locator-pattern","01.csharp/04.architecture-best-practices/08.di-ioc/03.service-locator-pattern",{"title":187,"path":188,"stem":189},"Паттерни Dependency Injection","/csharp/architecture-best-practices/di-ioc/dependency-injection-patterns","01.csharp/04.architecture-best-practices/08.di-ioc/04.dependency-injection-patterns",{"title":191,"path":192,"stem":193},"Microsoft DI: IServiceCollection та IServiceProvider","/csharp/architecture-best-practices/di-ioc/microsoft-di-deep-dive","01.csharp/04.architecture-best-practices/08.di-ioc/05.microsoft-di-deep-dive",{"title":195,"path":196,"stem":197},"Service Lifetimes та Scopes","/csharp/architecture-best-practices/di-ioc/service-lifetimes-and-scopes","01.csharp/04.architecture-best-practices/08.di-ioc/06.service-lifetimes-and-scopes",{"title":199,"path":200,"stem":201},"DI Анти-паттерни та Найкращі Практики","/csharp/architecture-best-practices/di-ioc/di-anti-patterns-and-best-practices","01.csharp/04.architecture-best-practices/08.di-ioc/07.di-anti-patterns-and-best-practices",{"title":203,"icon":132,"path":204,"stem":205,"children":206,"page":59},"Standard Library","/csharp/standard-library","01.csharp/05.standard-library",[207,211,215],{"title":208,"path":209,"stem":210},"Collections (Колекції)","/csharp/standard-library/collections","01.csharp/05.standard-library/01.collections",{"title":212,"path":213,"stem":214},"High Performance Types (Високопродуктивні Типи)","/csharp/standard-library/high-performance-types","01.csharp/05.standard-library/02.high-performance-types",{"title":216,"path":217,"stem":218},"LINQ (Language Integrated Query)","/csharp/standard-library/linq","01.csharp/05.standard-library/03.linq",{"title":220,"icon":221,"path":222,"stem":223,"children":224,"page":59},"System Internals Concurrency","i-lucide-server","/csharp/system-internals-concurrency","01.csharp/06.system-internals-concurrency",[225,229,233,237,241,245,249],{"title":226,"path":227,"stem":228},"Memory Management","/csharp/system-internals-concurrency/memory-management","01.csharp/06.system-internals-concurrency/01.memory-management",{"title":230,"path":231,"stem":232},"Reflection API: System.Type та Метадані","/csharp/system-internals-concurrency/reflection-fundamentals","01.csharp/06.system-internals-concurrency/02.reflection-fundamentals",{"title":234,"path":235,"stem":236},"Attributes та Dynamic Language Runtime","/csharp/system-internals-concurrency/attributes-dynamic","01.csharp/06.system-internals-concurrency/03.attributes-dynamic",{"title":238,"path":239,"stem":240},"Expression Trees: Швидка Альтернатива Рефлексії","/csharp/system-internals-concurrency/expression-trees-compiled","01.csharp/06.system-internals-concurrency/04.expression-trees-compiled",{"title":242,"path":243,"stem":244},"Source Generators: Compile-Time Code Generation","/csharp/system-internals-concurrency/source-generators","01.csharp/06.system-internals-concurrency/05.source-generators",{"title":246,"path":247,"stem":248},"Multithreading Fundamentals","/csharp/system-internals-concurrency/multithreading-fundamentals","01.csharp/06.system-internals-concurrency/06.multithreading-fundamentals",{"title":250,"path":251,"stem":252},"Synchronization Primitives","/csharp/system-internals-concurrency/synchronization-primitives","01.csharp/06.system-internals-concurrency/07.synchronization-primitives",{"title":254,"icon":255,"path":256,"stem":257,"children":258,"page":59},"System Programming Windows","i-lucide-cpu","/csharp/system-programming-windows","01.csharp/07.system-programming-windows",[259,263,267,271,275,279,283,287,291,295,299,303,307,311,315,319,323,327,331,335,339,343,347,351,355,359,363,367,371,375],{"title":260,"path":261,"stem":262},"Як Працює Операційна Система","/csharp/system-programming-windows/how-os-works","01.csharp/07.system-programming-windows/01.how-os-works",{"title":264,"path":265,"stem":266},"Процеси в .NET — API та Запуск","/csharp/system-programming-windows/processes-in-dotnet","01.csharp/07.system-programming-windows/02.processes-in-dotnet",{"title":268,"path":269,"stem":270},"Процеси в .NET — IPC та Міжпроцесна Комунікація","/csharp/system-programming-windows/02a.processes-ipc","01.csharp/07.system-programming-windows/02a.processes-ipc",{"title":272,"path":273,"stem":274},"Application Domains та Збірки — AppDomain і AssemblyLoadContext","/csharp/system-programming-windows/appdomains-assemblies","01.csharp/07.system-programming-windows/03.appdomains-assemblies",{"title":276,"path":277,"stem":278},"Application Domains та Збірки — Plug-in Система з Hot-Reload","/csharp/system-programming-windows/03a.appdomains-plugin-system","01.csharp/07.system-programming-windows/03a.appdomains-plugin-system",{"title":280,"path":281,"stem":282},"Потоки — Основи та API Thread","/csharp/system-programming-windows/thread-fundamentals","01.csharp/07.system-programming-windows/04.thread-fundamentals",{"title":284,"path":285,"stem":286},"Потоки — Lifecycle, Пріоритети та Безпечне Завершення","/csharp/system-programming-windows/04a.thread-lifecycle-priorities","01.csharp/07.system-programming-windows/04a.thread-lifecycle-priorities",{"title":288,"path":289,"stem":290},"Проблеми Спільного Стану — Race Condition та Data Race","/csharp/system-programming-windows/shared-state-problems","01.csharp/07.system-programming-windows/05.shared-state-problems",{"title":292,"path":293,"stem":294},"Проблеми Спільного Стану — Memory Model та volatile","/csharp/system-programming-windows/05a.shared-state-memory-model","01.csharp/07.system-programming-windows/05a.shared-state-memory-model",{"title":296,"path":297,"stem":298},"Синхронізація — Monitor, lock та еволюція примітивів","/csharp/system-programming-windows/synchronization-fundamentals","01.csharp/07.system-programming-windows/06.synchronization-fundamentals",{"title":300,"path":301,"stem":302},"Синхронізація — Наскрізний Приклад та Deadlock Detection","/csharp/system-programming-windows/06a.synchronization-walkthrough","01.csharp/07.system-programming-windows/06a.synchronization-walkthrough",{"title":304,"path":305,"stem":306},"Синхронізація — Mutex, Semaphore та Event-Based Primitives","/csharp/system-programming-windows/synchronization-advanced","01.csharp/07.system-programming-windows/07.synchronization-advanced",{"title":308,"path":309,"stem":310},"Синхронізація — Interlocked, Volatile та Lock-Free Структури","/csharp/system-programming-windows/07a.synchronization-advanced-walkthrough","01.csharp/07.system-programming-windows/07a.synchronization-advanced-walkthrough",{"title":312,"path":313,"stem":314},"Interlocked, CAS та Lock-Free Структури","/csharp/system-programming-windows/interlocked-cas-lockfree","01.csharp/07.system-programming-windows/08.interlocked-cas-lockfree",{"title":316,"path":317,"stem":318},"Volatile, Memory Model та Spinning","/csharp/system-programming-windows/08a.volatile-memory-model","01.csharp/07.system-programming-windows/08a.volatile-memory-model",{"title":320,"path":321,"stem":322},"ThreadPool — Пул Потоків для Ефективного Виконання","/csharp/system-programming-windows/thread-pool","01.csharp/07.system-programming-windows/09.thread-pool",{"title":324,"path":325,"stem":326},"ThreadPool — Просунуті Сценарії та Внутрішня Будова","/csharp/system-programming-windows/09a.thread-pool-advanced","01.csharp/07.system-programming-windows/09a.thread-pool-advanced",{"title":328,"path":329,"stem":330},"Concurrent та Immutable Collections","/csharp/system-programming-windows/concurrent-collections","01.csharp/07.system-programming-windows/10.concurrent-collections",{"title":332,"path":333,"stem":334},"TPL, Task та Композиція — Від Thread до Task","/csharp/system-programming-windows/tpl-parallel-plinq","01.csharp/07.system-programming-windows/11.tpl-parallel-plinq",{"title":336,"path":337,"stem":338},"Parallel Class та PLINQ — Data Parallelism","/csharp/system-programming-windows/11a.tpl-parallel-plinq-advanced","01.csharp/07.system-programming-windows/11a.tpl-parallel-plinq-advanced",{"title":340,"path":341,"stem":342},"Async/Await — Фундамент Асинхронного Програмування","/csharp/system-programming-windows/async-fundamentals","01.csharp/07.system-programming-windows/12.async-fundamentals",{"title":344,"path":345,"stem":346},"SynchronizationContext та ConfigureAwait — Контекст Виконання","/csharp/system-programming-windows/async-context-configureawait","01.csharp/07.system-programming-windows/13.async-context-configureawait",{"title":348,"path":349,"stem":350},"Async — Просунуті Паттерни","/csharp/system-programming-windows/async-advanced","01.csharp/07.system-programming-windows/14.async-advanced",{"title":352,"path":353,"stem":354},"System.Threading.Channels — Async Producer-Consumer","/csharp/system-programming-windows/channels","01.csharp/07.system-programming-windows/15.channels",{"title":356,"path":357,"stem":358},"Асинхронна Синхронізація","/csharp/system-programming-windows/async-synchronization","01.csharp/07.system-programming-windows/16.async-synchronization",{"title":360,"path":361,"stem":362},"Unsafe Code та Вказівники","/csharp/system-programming-windows/unsafe-code","01.csharp/07.system-programming-windows/17.unsafe-code",{"title":364,"path":365,"stem":366},"P/Invoke та Windows API — Міст між .NET та Native Code","/csharp/system-programming-windows/pinvoke-winapi","01.csharp/07.system-programming-windows/18.pinvoke-winapi",{"title":368,"path":369,"stem":370},"Реєстр Windows — Центральна База Конфігурації Системи","/csharp/system-programming-windows/windows-registry","01.csharp/07.system-programming-windows/19.windows-registry",{"title":372,"path":373,"stem":374},"Windows Hooks, Hotkeys та Services — Глибока Інтеграція з ОС","/csharp/system-programming-windows/windows-hooks-services","01.csharp/07.system-programming-windows/20.windows-hooks-services",{"title":376,"path":377,"stem":378},"Системне Програмування C# (Windows) — 07.system-programming-windows","/csharp/system-programming-windows/implementation_plan","01.csharp/07.system-programming-windows/implementation_plan",{"title":380,"icon":132,"path":381,"stem":382,"children":383,"page":59},"Io","/csharp/io","01.csharp/08.io",[384,388,392,396,400],{"title":385,"path":386,"stem":387},"8.1.1. Основи роботи з файловою системою","/csharp/io/file-system-basics","01.csharp/08.io/01.file-system-basics",{"title":389,"path":390,"stem":391},"8.1.2. Потоки (Streams) та Серіалізація Даних","/csharp/io/streams-serialization","01.csharp/08.io/02.streams-serialization",{"title":393,"path":394,"stem":395},"8.2.1. JSON Serialization з System.Text.Json","/csharp/io/json-serialization","01.csharp/08.io/03.json-serialization",{"title":397,"path":398,"stem":399},"8.2.2. XML Serialization та LINQ to XML","/csharp/io/xml-serialization","01.csharp/08.io/04.xml-serialization",{"title":401,"path":402,"stem":403},"8.2.3. Binary Serialization: MessagePack та Protocol Buffers","/csharp/io/binary-serialization","01.csharp/08.io/05.binary-serialization",{"title":405,"icon":132,"path":406,"stem":407,"children":408,"page":59},"Ado Net","/csharp/ado-net","01.csharp/09.ado-net",[409,413,417,421,425,429,433,437,441,445,449,453],{"title":410,"path":411,"stem":412},"9.1. Введення в ADO.NET","/csharp/ado-net/introduction-to-adonet","01.csharp/09.ado-net/01.introduction-to-adonet",{"title":414,"path":415,"stem":416},"9.2. Клас DbConnection — з'єднання з базою даних","/csharp/ado-net/connection","01.csharp/09.ado-net/02.connection",{"title":418,"path":419,"stem":420},"9.3. Клас DbCommand — виконання SQL-запитів","/csharp/ado-net/command-and-queries","01.csharp/09.ado-net/03.command-and-queries",{"title":422,"path":423,"stem":424},"9.4. Клас DbDataReader — ефективне читання даних","/csharp/ado-net/datareader","01.csharp/09.ado-net/04.datareader",{"title":426,"path":427,"stem":428},"9.5. Параметризовані запити та захист від SQL Injection","/csharp/ado-net/parameters-and-sql-injection","01.csharp/09.ado-net/05.parameters-and-sql-injection",{"title":430,"path":431,"stem":432},"9.6. Транзакції в ADO.NET","/csharp/ado-net/transactions","01.csharp/09.ado-net/06.transactions",{"title":434,"path":435,"stem":436},"9.7. DbProviderFactory — провайдер-незалежний код","/csharp/ado-net/provider-factory","01.csharp/09.ado-net/07.provider-factory",{"title":438,"path":439,"stem":440},"9.8. Асинхронний доступ до даних","/csharp/ado-net/async-data-access","01.csharp/09.ado-net/08.async-data-access",{"title":442,"path":443,"stem":444},"9.9. Від'єднаний режим: DataSet, DataTable, DataRow","/csharp/ado-net/disconnected-mode-dataset","01.csharp/09.ado-net/09.disconnected-mode-dataset",{"title":446,"path":447,"stem":448},"9.10. DataAdapter — міст між DataSet та базою даних","/csharp/ado-net/data-adapter","01.csharp/09.ado-net/10.data-adapter",{"title":450,"path":451,"stem":452},"9.11. Data Mapper та Repository: Архітектура доступу до даних","/csharp/ado-net/data-mapper-repository","01.csharp/09.ado-net/11.data-mapper-repository",{"title":454,"path":455,"stem":456},"9.12. Identity Map, Unit of Work та Specification Pattern","/csharp/ado-net/advanced-patterns","01.csharp/09.ado-net/12.advanced-patterns",{"title":458,"icon":255,"path":459,"stem":460,"children":461,"page":59},"Ef Core","/csharp/ef-core","01.csharp/10.ef-core",[462,466,470,474,478,482,486,490,494,498,502,506,510,514,518,522,526,532,538,542,546,550,554,558,562,566,570,574,578,582,586,590,594,598,602,606,610,614,618,622,626,630,634,638,642,646],{"title":463,"path":464,"stem":465},"Що таке ORM? Від SQL до об'єктів","/csharp/ef-core/what-is-orm","01.csharp/10.ef-core/01.what-is-orm",{"title":467,"path":468,"stem":469},"Перший проєкт — від нуля до CRUD","/csharp/ef-core/first-project","01.csharp/10.ef-core/02.first-project",{"title":471,"path":472,"stem":473},"DbContext — Серце EF Core","/csharp/ef-core/dbcontext-deep-dive","01.csharp/10.ef-core/03.dbcontext-deep-dive",{"title":475,"path":476,"stem":477},"Провайдери баз даних — Архітектура та Вибір СУБД","/csharp/ef-core/database-providers","01.csharp/10.ef-core/04.database-providers",{"title":479,"path":480,"stem":481},"Конвенції EF Core — Магія без конфігурації","/csharp/ef-core/conventions","01.csharp/10.ef-core/05.conventions",{"title":483,"path":484,"stem":485},"Fluent API та Data Annotations — Явна конфігурація моделі","/csharp/ef-core/fluent-api-vs-annotations","01.csharp/10.ef-core/06.fluent-api-vs-annotations",{"title":487,"path":488,"stem":489},"Зв'язки — One-to-One та One-to-Many","/csharp/ef-core/relationships-basics","01.csharp/10.ef-core/07.relationships-basics",{"title":491,"path":492,"stem":493},"Зв'язки Advanced — Many-to-Many та Складні Сценарії","/csharp/ef-core/relationships-advanced","01.csharp/10.ef-core/08.relationships-advanced",{"title":495,"path":496,"stem":497},"Властивості — Типи, Конвертери, Компаратори (Частина 1)","/csharp/ef-core/property-configuration-part1","01.csharp/10.ef-core/09.property-configuration-part1",{"title":499,"path":500,"stem":501},"Властивості — Value Comparers, Generators, Shadow Properties (Частина 2)","/csharp/ef-core/property-configuration-part2","01.csharp/10.ef-core/09.property-configuration-part2",{"title":503,"path":504,"stem":505},"Складні типи — Owned Types та Complex Types (Частина 1)","/csharp/ef-core/complex-types-owned-part1","01.csharp/10.ef-core/10.complex-types-owned-part1",{"title":507,"path":508,"stem":509},"Складні типи — Complex Types, Keyless Entities, Порівняння (Частина 2)","/csharp/ef-core/complex-types-owned-part2","01.csharp/10.ef-core/10.complex-types-owned-part2",{"title":511,"path":512,"stem":513},"JSON Columns — Складні дані у JSON (Частина 1)","/csharp/ef-core/json-columns-part1","01.csharp/10.ef-core/11.json-columns-part1",{"title":515,"path":516,"stem":517},"JSON Columns — Value Comparers, Індекси, Провайдери (Частина 2)","/csharp/ef-core/json-columns-part2","01.csharp/10.ef-core/11.json-columns-part2",{"title":519,"path":520,"stem":521},"Успадкування — Абстрактні класи та TPH (Частина 1)","/csharp/ef-core/inheritance-part1","01.csharp/10.ef-core/12.inheritance-part1",{"title":523,"path":524,"stem":525},"Успадкування — TPT, TPC та Порівняння Стратегій (Частина 2)","/csharp/ef-core/inheritance-part2","01.csharp/10.ef-core/12.inheritance-part2",{"title":527,"path":528,"stem":529,"children":530},"Індекси, Обмеження та Схема (Частина 1)","/csharp/ef-core/indexes-constraints-part1","01.csharp/10.ef-core/13.indexes-constraints-part1",[531],{"title":527,"path":528,"stem":529},{"title":533,"path":534,"stem":535,"children":536},"Індекси, Обмеження та Схема (Частина 2)","/csharp/ef-core/indexes-constraints-part2","01.csharp/10.ef-core/13.indexes-constraints-part2",[537],{"title":533,"path":534,"stem":535},{"title":539,"path":540,"stem":541},"Seed Data — Початкові Дані (Частина 1)","/csharp/ef-core/seeding-part1","01.csharp/10.ef-core/14.seeding-part1",{"title":543,"path":544,"stem":545},"Seed Data — SQL-скрипти, Bogus та Стратегії (Частина 2)","/csharp/ef-core/seeding-part2","01.csharp/10.ef-core/14.seeding-part2",{"title":547,"path":548,"stem":549},"Global Query Filters — Глобальні Фільтри (Частина 1)","/csharp/ef-core/global-query-filters-part1","01.csharp/10.ef-core/15.global-query-filters-part1",{"title":551,"path":552,"stem":553},"Global Query Filters — Підводні камені та Інтеграція (Частина 2)","/csharp/ef-core/global-query-filters-part2","01.csharp/10.ef-core/15.global-query-filters-part2",{"title":555,"path":556,"stem":557},"LINQ-запити в EF Core (Частина 1)","/csharp/ef-core/linq-queries-part1","01.csharp/10.ef-core/16.linq-queries-part1",{"title":559,"path":560,"stem":561},"LINQ-запити в EF Core (Частина 2)","/csharp/ef-core/linq-queries-part2","01.csharp/10.ef-core/16.linq-queries-part2",{"title":563,"path":564,"stem":565},"Завантаження Пов'язаних Даних (Частина 1)","/csharp/ef-core/loading-related-data-part1","01.csharp/10.ef-core/17.loading-related-data-part1",{"title":567,"path":568,"stem":569},"Завантаження Пов'язаних Даних (Частина 2)","/csharp/ef-core/loading-related-data-part2","01.csharp/10.ef-core/17.loading-related-data-part2",{"title":571,"path":572,"stem":573},"Raw SQL, Views та Stored Procedures (Частина 1)","/csharp/ef-core/raw-sql-part1","01.csharp/10.ef-core/18.raw-sql-part1",{"title":575,"path":576,"stem":577},"Raw SQL — Stored Procedures, DbFunction та Bulk Operations (Частина 2)","/csharp/ef-core/raw-sql-part2","01.csharp/10.ef-core/18.raw-sql-part2",{"title":579,"path":580,"stem":581},"Продвинуті Запити — Compiled Queries, Bulk та Оптимізація (Частина 1)","/csharp/ef-core/advanced-queries-part1","01.csharp/10.ef-core/19.advanced-queries-part1",{"title":583,"path":584,"stem":585},"Продвинуті Запити — Query Tags, Bulk та Interceptors (Частина 2)","/csharp/ef-core/advanced-queries-part2","01.csharp/10.ef-core/19.advanced-queries-part2",{"title":587,"path":588,"stem":589},"Change Tracker — Відстеження Змін (Частина 1)","/csharp/ef-core/change-tracking-part1","01.csharp/10.ef-core/20.change-tracking-part1",{"title":591,"path":592,"stem":593},"Change Tracker — Графи Об'єктів та Disconnected (Частина 2)","/csharp/ef-core/change-tracking-part2","01.csharp/10.ef-core/20.change-tracking-part2",{"title":595,"path":596,"stem":597},"Збереження Даних та Транзакції (Частина 1)","/csharp/ef-core/saving-data-part1","01.csharp/10.ef-core/21.saving-data-part1",{"title":599,"path":600,"stem":601},"Збереження Даних — Concurrency та Outbox (Частина 2)","/csharp/ef-core/saving-data-part2","01.csharp/10.ef-core/21.saving-data-part2",{"title":603,"path":604,"stem":605},"Конкурентність та Блокування (Частина 1)","/csharp/ef-core/concurrency-part1","01.csharp/10.ef-core/22.concurrency-part1",{"title":607,"path":608,"stem":609},"Конкурентність — Дедлоки та Queue Processing (Частина 2)","/csharp/ef-core/concurrency-part2","01.csharp/10.ef-core/22.concurrency-part2",{"title":611,"path":612,"stem":613},"Міграції в EF Core — Основи (Частина 1)","/csharp/ef-core/migrations-basics-part1","01.csharp/10.ef-core/23.migrations-basics-part1",{"title":615,"path":616,"stem":617},"Міграції в EF Core — Основи (Частина 2)","/csharp/ef-core/migrations-basics-part2","01.csharp/10.ef-core/23.migrations-basics-part2",{"title":619,"path":620,"stem":621},"Міграції — Просунуті Сценарії (Частина 1)","/csharp/ef-core/migrations-advanced-part1","01.csharp/10.ef-core/24.migrations-advanced-part1",{"title":623,"path":624,"stem":625},"Міграції — Просунуті Сценарії (Частина 2)","/csharp/ef-core/migrations-advanced-part2","01.csharp/10.ef-core/24.migrations-advanced-part2",{"title":627,"path":628,"stem":629},"Управління Схемою та Database-First (Частина 1)","/csharp/ef-core/schema-management-part1","01.csharp/10.ef-core/25.schema-management-part1",{"title":631,"path":632,"stem":633},"Управління Схемою та Database-First (Частина 2)","/csharp/ef-core/schema-management-part2","01.csharp/10.ef-core/25.schema-management-part2",{"title":635,"path":636,"stem":637},"Продуктивність EF Core — Основи (Частина 1)","/csharp/ef-core/performance-fundamentals-part1","01.csharp/10.ef-core/26.performance-fundamentals-part1",{"title":639,"path":640,"stem":641},"Interceptors в EF Core (Частина 1)","/csharp/ef-core/interceptors-part1","01.csharp/10.ef-core/29.interceptors-part1",{"title":643,"path":644,"stem":645},"Interceptors в EF Core — Connection, Transaction та Materialization (Частина 2)","/csharp/ef-core/interceptors-part2","01.csharp/10.ef-core/29.interceptors-part2",{"title":647,"path":648,"stem":649},"План вивчення Entity Framework Core — Повний курс","/csharp/ef-core/implementation_plan","01.csharp/10.ef-core/implementation_plan",{"title":651,"icon":652,"path":653,"stem":654,"children":655,"page":59},"ASP.NET","i-devicon-dotnetcore","/csharp/aspnet","01.csharp/11.aspnet",[656,730,791,869,927,941,967,1057,1111,1182,1212,1289],{"title":657,"icon":658,"path":659,"stem":660,"children":661,"page":59},"Minimal API","i-lucide-network","/csharp/aspnet/minimal-api","01.csharp/11.aspnet/01.minimal-api",[662,666,670,674,678,682,686,690,694,698,702,706,710,714,718,722,726],{"title":663,"path":664,"stem":665},"Вступ до ASP.NET та еволюція фреймворку","/csharp/aspnet/minimal-api/introduction","01.csharp/11.aspnet/01.minimal-api/01.introduction",{"title":667,"path":668,"stem":669},"Перший додаток на ASP.NET Core","/csharp/aspnet/minimal-api/first-application","01.csharp/11.aspnet/01.minimal-api/02.first-application",{"title":671,"path":672,"stem":673},"WebApplication, Builder та Dependency Injection","/csharp/aspnet/minimal-api/webapplication-builder","01.csharp/11.aspnet/01.minimal-api/03.webapplication-builder",{"title":675,"path":676,"stem":677},"Конвеєр запитів та Middleware","/csharp/aspnet/minimal-api/request-pipeline-middleware","01.csharp/11.aspnet/01.minimal-api/04.request-pipeline-middleware",{"title":679,"path":680,"stem":681},"Маршрутизація в ASP.NET Core: Основи","/csharp/aspnet/minimal-api/routing-basics","01.csharp/11.aspnet/01.minimal-api/05.routing-basics",{"title":683,"path":684,"stem":685},"Маршрутизація в ASP.NET Core: Розширені можливості","/csharp/aspnet/minimal-api/routing-advanced","01.csharp/11.aspnet/01.minimal-api/06.routing-advanced",{"title":687,"path":688,"stem":689},"Статичні файли в ASP.NET Core","/csharp/aspnet/minimal-api/static-files","01.csharp/11.aspnet/01.minimal-api/07.static-files",{"title":691,"path":692,"stem":693},"Статичні Активи: MapStaticAssets (ASP.NET Core 9.0)","/csharp/aspnet/minimal-api/static-assets","01.csharp/11.aspnet/01.minimal-api/08.static-assets",{"title":695,"path":696,"stem":697},"Конфігурація в ASP.NET Core: Основи","/csharp/aspnet/minimal-api/configuration-fundamentals","01.csharp/11.aspnet/01.minimal-api/09.configuration-fundamentals",{"title":699,"path":700,"stem":701},"Конфігурація: Паттерн Options","/csharp/aspnet/minimal-api/configuration-options","01.csharp/11.aspnet/01.minimal-api/10.configuration-options",{"title":703,"path":704,"stem":705},"Логування в ASP.NET Core: Основи","/csharp/aspnet/minimal-api/logging-basics","01.csharp/11.aspnet/01.minimal-api/11.logging-basics",{"title":707,"path":708,"stem":709},"Логування: Serilog та Middleware","/csharp/aspnet/minimal-api/logging-advanced","01.csharp/11.aspnet/01.minimal-api/12.logging-advanced",{"title":711,"path":712,"stem":713},"Управління станом: HttpContext.Items та Cookies","/csharp/aspnet/minimal-api/state-management","01.csharp/11.aspnet/01.minimal-api/13.state-management",{"title":715,"path":716,"stem":717},"Стан сесії: Sessions","/csharp/aspnet/minimal-api/session-state","01.csharp/11.aspnet/01.minimal-api/14.session-state",{"title":719,"path":720,"stem":721},"Структура проєкту: від хаосу до архітектури","/csharp/aspnet/minimal-api/project-structure","01.csharp/11.aspnet/01.minimal-api/15.project-structure",{"title":723,"path":724,"stem":725},"Scalar у Minimal API: повний проєкт і Fluent OpenAPI","/csharp/aspnet/minimal-api/scalar-openapi-fluent","01.csharp/11.aspnet/01.minimal-api/16.scalar-openapi-fluent",{"title":727,"path":728,"stem":729},"Swagger / Swashbuckle у Minimal API: окремий класичний шлях","/csharp/aspnet/minimal-api/swagger-swashbuckle","01.csharp/11.aspnet/01.minimal-api/17.swagger-swashbuckle",{"title":731,"icon":658,"path":732,"stem":733,"children":734,"page":59},"API","/csharp/aspnet/api","01.csharp/11.aspnet/02.api",[735,739,743,747,751,755,759,763,767,771,775,779,783,787],{"title":736,"path":737,"stem":738},"Що таке API. Клієнт-серверна архітектура","/csharp/aspnet/api/what-is-api","01.csharp/11.aspnet/02.api/01.what-is-api",{"title":740,"path":741,"stem":742},"Формати даних: JSON, XML, TOML та бінарні формати","/csharp/aspnet/api/data-formats","01.csharp/11.aspnet/02.api/02.data-formats",{"title":744,"path":745,"stem":746},"Парадигми API та концепція REST","/csharp/aspnet/api/api-paradigms-rest","01.csharp/11.aspnet/02.api/03.api-paradigms-rest",{"title":748,"path":749,"stem":750},"HTTP-методи, статус-коди та заголовки","/csharp/aspnet/api/http-methods-status-codes","01.csharp/11.aspnet/02.api/04.http-methods-status-codes",{"title":752,"path":753,"stem":754},"Організація HTTP API за принципами REST","/csharp/aspnet/api/rest-organizing","01.csharp/11.aspnet/02.api/05.rest-organizing",{"title":756,"path":757,"stem":758},"Номенклатура URL та CRUD-операції","/csharp/aspnet/api/url-nomenclature-crud","01.csharp/11.aspnet/02.api/06.url-nomenclature-crud",{"title":760,"path":761,"stem":762},"Правила дизайну: іменування та стандарти","/csharp/aspnet/api/api-design-naming","01.csharp/11.aspnet/02.api/07.api-design-naming",{"title":764,"path":765,"stem":766},"Валідація, ліміти та обробка помилок","/csharp/aspnet/api/api-design-validation","01.csharp/11.aspnet/02.api/08.api-design-validation",{"title":768,"path":769,"stem":770},"Обробка помилок у Minimal API","/csharp/aspnet/api/error-handling-http","01.csharp/11.aspnet/02.api/09.error-handling-http",{"title":772,"path":773,"stem":774},"Ідемпотентність та синхронізація стану","/csharp/aspnet/api/idempotency-sync","01.csharp/11.aspnet/02.api/10.idempotency-sync",{"title":776,"path":777,"stem":778},"Пагінація та організація списків","/csharp/aspnet/api/pagination-lists","01.csharp/11.aspnet/02.api/11.pagination-lists",{"title":780,"path":781,"stem":782},"Безпека API, кешування та інтернаціоналізація","/csharp/aspnet/api/security-auth","01.csharp/11.aspnet/02.api/12.security-auth",{"title":784,"path":785,"stem":786},"Процес проєктування API та документування","/csharp/aspnet/api/api-design-process","01.csharp/11.aspnet/02.api/13.api-design-process",{"title":788,"path":789,"stem":790},"OpenAPI: контракт, специфікація та документація API","/csharp/aspnet/api/openapi","01.csharp/11.aspnet/02.api/14.openapi",{"title":792,"icon":793,"path":794,"stem":795,"children":796,"page":59},"Auth","i-lucide-shield-check","/csharp/aspnet/auth","01.csharp/11.aspnet/03.auth",[797,801,805,809,813,817,821,825,829,833,837,841,845,849,853,857,861,865],{"title":798,"path":799,"stem":800},"Основи аутентифікації та авторизації","/csharp/aspnet/auth/auth-fundamentals","01.csharp/11.aspnet/03.auth/01.auth-fundamentals",{"title":802,"path":803,"stem":804},"JWT-аутентифікація","/csharp/aspnet/auth/jwt-authentication","01.csharp/11.aspnet/03.auth/02.jwt-authentication",{"title":806,"path":807,"stem":808},"Авторизація: ролі, політики та resource-based доступ","/csharp/aspnet/auth/authorization-policies","01.csharp/11.aspnet/03.auth/03.authorization-policies",{"title":810,"path":811,"stem":812},"Cookie-аутентифікація та ASP.NET Core Identity","/csharp/aspnet/auth/cookie-auth-identity","01.csharp/11.aspnet/03.auth/04.cookie-auth-identity",{"title":814,"path":815,"stem":816},"JWT + Refresh Tokens (HttpOnly Cookie)","/csharp/aspnet/auth/04b.identity-auth-jwt","01.csharp/11.aspnet/03.auth/04b.identity-auth-jwt",{"title":818,"path":819,"stem":820},"Identity: Підтвердження Email та Скидання Пароля","/csharp/aspnet/auth/identity-email-confirmation","01.csharp/11.aspnet/03.auth/05.identity-email-confirmation",{"title":822,"path":823,"stem":824},"Identity: Двофакторна Аутентифікація (2FA)","/csharp/aspnet/auth/identity-two-factor","01.csharp/11.aspnet/03.auth/06.identity-two-factor",{"title":826,"path":827,"stem":828},"Identity: Внутрішня Архітектура та Кастомізація","/csharp/aspnet/auth/identity-internals","01.csharp/11.aspnet/03.auth/07.identity-internals",{"title":830,"path":831,"stem":832},"OAuth 2.0 та зовнішні провайдери","/csharp/aspnet/auth/oauth-external-providers","01.csharp/11.aspnet/03.auth/08.oauth-external-providers",{"title":834,"path":835,"stem":836},"Безпека на практиці: CORS, HTTPS та захист від атак","/csharp/aspnet/auth/security-hardening","01.csharp/11.aspnet/03.auth/09.security-hardening",{"title":838,"path":839,"stem":840},"Теорія OAuth 2.0: Поняття, Аналогії та Флоу","/csharp/aspnet/auth/oauth-theory","01.csharp/11.aspnet/03.auth/10.oauth-theory",{"title":842,"path":843,"stem":844},"OIDC, OAuth 2.0 та Keycloak в ASP.NET Core","/csharp/aspnet/auth/oidc-keycloak","01.csharp/11.aspnet/03.auth/10.oidc-keycloak",{"title":846,"path":847,"stem":848},"API Keys аутентифікація в ASP.NET Core","/csharp/aspnet/auth/api-keys","01.csharp/11.aspnet/03.auth/11.api-keys",{"title":850,"path":851,"stem":852},"Rate Limiting та Throttling в ASP.NET Core","/csharp/aspnet/auth/rate-limiting","01.csharp/11.aspnet/03.auth/12.rate-limiting",{"title":854,"path":855,"stem":856},"Refresh Token Rotation в ASP.NET Core","/csharp/aspnet/auth/refresh-token-rotation","01.csharp/11.aspnet/03.auth/13.refresh-token-rotation",{"title":858,"path":859,"stem":860},"Certificate Authentication та mTLS в ASP.NET Core","/csharp/aspnet/auth/certificate-auth","01.csharp/11.aspnet/03.auth/14.certificate-auth",{"title":862,"path":863,"stem":864},"RBAC, ABAC та ReBAC в ASP.NET Core","/csharp/aspnet/auth/rbac-abac-rebac","01.csharp/11.aspnet/03.auth/15.rbac-abac-rebac",{"title":866,"path":867,"stem":868},"Multi-tenancy та ізоляція даних в ASP.NET Core","/csharp/aspnet/auth/multi-tenancy","01.csharp/11.aspnet/03.auth/16.multi-tenancy",{"title":870,"icon":871,"path":872,"stem":873,"children":874,"page":59},"Нотифікації","i-lucide-bell","/csharp/aspnet/notifications","01.csharp/11.aspnet/04.notifications",[875,879,883,887,891,895,899,903,907,911,915,919,923],{"title":876,"path":877,"stem":878},"In-App нотифікації через базу даних","/csharp/aspnet/notifications/in-app-database-notifications","01.csharp/11.aspnet/04.notifications/01.in-app-database-notifications",{"title":880,"path":881,"stem":882},"Polling: Регулярний запит оновлень","/csharp/aspnet/notifications/polling","01.csharp/11.aspnet/04.notifications/02.polling",{"title":884,"path":885,"stem":886},"Server-Sent Events: Однострімовий push від сервера","/csharp/aspnet/notifications/server-sent-events","01.csharp/11.aspnet/04.notifications/03.server-sent-events",{"title":888,"path":889,"stem":890},"WebSockets: Двостороннє з'єднання в реальному часі","/csharp/aspnet/notifications/websockets","01.csharp/11.aspnet/04.notifications/04.websockets",{"title":892,"path":893,"stem":894},"SignalR: Абстракція над транспортами реального часу","/csharp/aspnet/notifications/signalr","01.csharp/11.aspnet/04.notifications/05.signalr",{"title":896,"path":897,"stem":898},"Background Services: Фонові задачі в ASP.NET Core","/csharp/aspnet/notifications/background-services","01.csharp/11.aspnet/04.notifications/06.background-services",{"title":900,"path":901,"stem":902},"Web Push нотифікації","/csharp/aspnet/notifications/web-push","01.csharp/11.aspnet/04.notifications/07.web-push",{"title":904,"path":905,"stem":906},"Email нотифікації","/csharp/aspnet/notifications/email-notifications","01.csharp/11.aspnet/04.notifications/08.email-notifications",{"title":908,"path":909,"stem":910},"Порівняння підходів: Як вибрати правильну технологію нотифікацій","/csharp/aspnet/notifications/choosing-the-right-approach","01.csharp/11.aspnet/04.notifications/09.choosing-the-right-approach",{"title":912,"path":913,"stem":914},"Hangfire: Надійне планування фонових задач","/csharp/aspnet/notifications/hangfire","01.csharp/11.aspnet/04.notifications/10.hangfire",{"title":916,"path":917,"stem":918},"Практика: Конвертація зображень у WebP через Hangfire","/csharp/aspnet/notifications/hangfire-image-webp","01.csharp/11.aspnet/04.notifications/11.hangfire-image-webp",{"title":920,"path":921,"stem":922},"Практика: Підготовка відео до HLS-стрімінгу через Hangfire","/csharp/aspnet/notifications/hangfire-video-hls","01.csharp/11.aspnet/04.notifications/12.hangfire-video-hls",{"title":924,"path":925,"stem":926},"Telegram-нотифікації: від одного повідомлення до масових розсилок і мульти-канального підходу","/csharp/aspnet/notifications/telegram-notifications","01.csharp/11.aspnet/04.notifications/13.telegram-notifications",{"title":928,"icon":929,"path":930,"stem":931,"children":932,"page":59},"Інтернаціоналізація","i-lucide-languages","/csharp/aspnet/i18n","01.csharp/11.aspnet/05.i18n",[933,937],{"title":934,"path":935,"stem":936},"Інтернаціоналізація (i18n) у Minimal API: від A до Я","/csharp/aspnet/i18n/internationalization","01.csharp/11.aspnet/05.i18n/01.internationalization",{"title":938,"path":939,"stem":940},"Humanizer: людиномовні рядки у .NET","/csharp/aspnet/i18n/humanizer","01.csharp/11.aspnet/05.i18n/02.humanizer",{"title":942,"icon":943,"path":944,"stem":945,"children":946,"page":59},"Кешування","i-lucide-layers","/csharp/aspnet/caching","01.csharp/11.aspnet/06.caching",[947,951,955,959,963],{"title":948,"path":949,"stem":950},"Огляд кешування: чотири рівні і коли що обирати","/csharp/aspnet/caching/caching","01.csharp/11.aspnet/06.caching/01.caching",{"title":952,"path":953,"stem":954},"IMemoryCache: кеш в оперативній пам'яті","/csharp/aspnet/caching/memory-cache","01.csharp/11.aspnet/06.caching/02.memory-cache",{"title":956,"path":957,"stem":958},"IDistributedCache і Redis: розподілений кеш","/csharp/aspnet/caching/distributed-cache","01.csharp/11.aspnet/06.caching/03.distributed-cache",{"title":960,"path":961,"stem":962},"Response Cache: HTTP-кешування через Cache-Control","/csharp/aspnet/caching/response-cache","01.csharp/11.aspnet/06.caching/04.response-cache",{"title":964,"path":965,"stem":966},"Output Cache: серверний кеш HTTP-відповідей (.NET 7+)","/csharp/aspnet/caching/output-cache","01.csharp/11.aspnet/06.caching/05.output-cache",{"title":968,"icon":969,"path":970,"stem":971,"children":972,"page":59},"Тестування","i-lucide-test-tube","/csharp/aspnet/testing","01.csharp/11.aspnet/07.testing",[973,977,981,985,989,993,997,1001,1005,1009,1013,1017,1021,1025,1029,1033,1037,1041,1045,1049,1053],{"title":974,"path":975,"stem":976},"Що таке тестування? Від інтуїції до науки","/csharp/aspnet/testing/what-is-testing","01.csharp/11.aspnet/07.testing/01.what-is-testing",{"title":978,"path":979,"stem":980},"Піраміда тестування — Стратегія, а не Догма","/csharp/aspnet/testing/testing-pyramid","01.csharp/11.aspnet/07.testing/02.testing-pyramid",{"title":982,"path":983,"stem":984},"Дві Школи Тестування — Лондон проти Детройту","/csharp/aspnet/testing/testing-schools","01.csharp/11.aspnet/07.testing/03.testing-schools",{"title":986,"path":987,"stem":988},"TDD та BDD — Тести як Дизайн-інструмент","/csharp/aspnet/testing/tdd-and-bdd","01.csharp/11.aspnet/07.testing/04.tdd-and-bdd",{"title":990,"path":991,"stem":992},"Що саме тестувати — Техніки аналізу та Циклomatична складність","/csharp/aspnet/testing/what-to-test","01.csharp/11.aspnet/07.testing/05.what-to-test",{"title":994,"path":995,"stem":996},"Тестові Фреймворки — Навіщо вони і що всередині","/csharp/aspnet/testing/test-frameworks","01.csharp/11.aspnet/07.testing/06.test-frameworks",{"title":998,"path":999,"stem":1000},"xUnit — Факти, Теорії та Lifecycle тестів","/csharp/aspnet/testing/xunit-basics","01.csharp/11.aspnet/07.testing/07.xunit-basics",{"title":1002,"path":1003,"stem":1004},"xUnit Advanced — Fixtures, Кастомізація та Розширення","/csharp/aspnet/testing/xunit-advanced","01.csharp/11.aspnet/07.testing/08.xunit-advanced",{"title":1006,"path":1007,"stem":1008},"Moq — Глибоке занурення в мокування","/csharp/aspnet/testing/mocking-with-moq","01.csharp/11.aspnet/07.testing/09.mocking-with-moq",{"title":1010,"path":1011,"stem":1012},"Тестування Баз Даних — EF Core, SQLite та Testcontainers","/csharp/aspnet/testing/database-testing","01.csharp/11.aspnet/07.testing/10.database-testing",{"title":1014,"path":1015,"stem":1016},"Integration Testing — Частина 1 [Теорія та WebApplicationFactory]","/csharp/aspnet/testing/integration-testing","01.csharp/11.aspnet/07.testing/11.integration-testing",{"title":1018,"path":1019,"stem":1020},"Інтеграційне тестування — Практика","/csharp/aspnet/testing/11a.integration-testing-practice","01.csharp/11.aspnet/07.testing/11a.integration-testing-practice",{"title":1022,"path":1023,"stem":1024},"Integration Testing — Частина 2 [Просунуті Сценарії та Testcontainers]","/csharp/aspnet/testing/integration-testing-advanced","01.csharp/11.aspnet/07.testing/12.integration-testing-advanced",{"title":1026,"path":1027,"stem":1028},"Професійний Postman: Колекції, Змінні та GitHub Інтеграція","/csharp/aspnet/testing/postman-professional","01.csharp/11.aspnet/07.testing/13.postman-professional",{"title":1030,"path":1031,"stem":1032},"HttpClient у Тестах Частина 1: Архітектура та MockHttpMessageHandler","/csharp/aspnet/testing/httpclient-testing","01.csharp/11.aspnet/07.testing/14.httpclient-testing",{"title":1034,"path":1035,"stem":1036},"HttpClient у Тестах Частина 2: WireMock.Net та Resilience","/csharp/aspnet/testing/wiremock-net","01.csharp/11.aspnet/07.testing/15.wiremock-net",{"title":1038,"path":1039,"stem":1040},"Патерни та Анти-патерни Тестування: Test Smells","/csharp/aspnet/testing/testing-patterns","01.csharp/11.aspnet/07.testing/16.testing-patterns",{"title":1042,"path":1043,"stem":1044},"Просунуті інструменти: Time, Snapshots та Властивості","/csharp/aspnet/testing/advanced-testing-tools","01.csharp/11.aspnet/07.testing/17.advanced-testing-tools",{"title":1046,"path":1047,"stem":1048},"Тестування Архітектури з NetArchTest","/csharp/aspnet/testing/architecture-testing","01.csharp/11.aspnet/07.testing/18.architecture-testing",{"title":1050,"path":1051,"stem":1052},"Тестування Продуктивності: BenchmarkDotNet, NBomber та k6","/csharp/aspnet/testing/performance-testing","01.csharp/11.aspnet/07.testing/19.performance-testing",{"title":1054,"path":1055,"stem":1056},"Залишок плану для курсу \"Тестування ASP.NET Minimal API\"","/csharp/aspnet/testing/remaining_plan","01.csharp/11.aspnet/07.testing/remaining_plan",{"title":1058,"icon":1059,"path":1060,"stem":1061,"children":1062,"page":59},"Платежі","i-lucide-credit-card","/csharp/aspnet/payments","01.csharp/11.aspnet/08.payments",[1063,1067,1071,1075,1079,1083,1087,1091,1095,1099,1103,1107],{"title":1064,"path":1065,"stem":1066},"Основи платіжної інфраструктури","/csharp/aspnet/payments/payment-fundamentals","01.csharp/11.aspnet/08.payments/01.payment-fundamentals",{"title":1068,"path":1069,"stem":1070},"Методи оплати в Україні","/csharp/aspnet/payments/payment-methods-ukraine","01.csharp/11.aspnet/08.payments/02.payment-methods-ukraine",{"title":1072,"path":1073,"stem":1074},"PCI DSS та безпека платежів","/csharp/aspnet/payments/pci-dss-security","01.csharp/11.aspnet/08.payments/03.pci-dss-security",{"title":1076,"path":1077,"stem":1078},"Архітектура платіжної підсистеми","/csharp/aspnet/payments/payment-architecture","01.csharp/11.aspnet/08.payments/04.payment-architecture",{"title":1080,"path":1081,"stem":1082},"Інтеграція LiqPay (ПриватБанк)","/csharp/aspnet/payments/liqpay-integration","01.csharp/11.aspnet/08.payments/05.liqpay-integration",{"title":1084,"path":1085,"stem":1086},"Інтеграція Monobank Acquiring API","/csharp/aspnet/payments/monobank-acquiring","01.csharp/11.aspnet/08.payments/06.monobank-acquiring",{"title":1088,"path":1089,"stem":1090},"Інтеграція Stripe","/csharp/aspnet/payments/stripe-integration","01.csharp/11.aspnet/08.payments/07.stripe-integration",{"title":1092,"path":1093,"stem":1094},"Webhooks — глибоке занурення","/csharp/aspnet/payments/webhooks-deep-dive","01.csharp/11.aspnet/08.payments/08.webhooks-deep-dive",{"title":1096,"path":1097,"stem":1098},"Підписки та рекурентні платежі","/csharp/aspnet/payments/subscriptions-recurring","01.csharp/11.aspnet/08.payments/09.subscriptions-recurring",{"title":1100,"path":1101,"stem":1102},"Повернення коштів та диспути","/csharp/aspnet/payments/refunds-disputes","01.csharp/11.aspnet/08.payments/10.refunds-disputes",{"title":1104,"path":1105,"stem":1106},"Тестування платіжних інтеграцій","/csharp/aspnet/payments/testing-payments","01.csharp/11.aspnet/08.payments/11.testing-payments",{"title":1108,"path":1109,"stem":1110},"Чекліст виходу в Production","/csharp/aspnet/payments/production-checklist","01.csharp/11.aspnet/08.payments/12.production-checklist",{"title":1112,"icon":1113,"items":1114,"path":1127,"stem":1128,"children":1129,"page":59},"Популярні бібліотеки","lucide:box",[1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126],"01.fluent-validation","02.mapster","03.erroror-result-pattern","04.serilog","05.mediatr","06.polly","07.health-checks","08.feature-management","09.fluent-email","10.quest-pdf","11.bogus","12.humanizer-guard","/csharp/aspnet/libraries","01.csharp/11.aspnet/09.libraries",[1130,1134,1138,1142,1146,1150,1154,1158,1162,1166,1170,1174,1178],{"title":1131,"path":1132,"stem":1133},"Валідація з FluentValidation в ASP.NET Core","/csharp/aspnet/libraries/fluent-validation","01.csharp/11.aspnet/09.libraries/01.fluent-validation",{"title":1135,"path":1136,"stem":1137},"Маппінг об","/csharp/aspnet/libraries/mapster","01.csharp/11.aspnet/09.libraries/02.mapster",{"title":1139,"path":1140,"stem":1141},"Обробка помилок з ErrorOr та Result Pattern в ASP.NET Core","/csharp/aspnet/libraries/erroror-result-pattern","01.csharp/11.aspnet/09.libraries/03.erroror-result-pattern",{"title":1143,"path":1144,"stem":1145},"Структуроване логування з Serilog в ASP.NET Core","/csharp/aspnet/libraries/serilog","01.csharp/11.aspnet/09.libraries/04.serilog",{"title":1147,"path":1148,"stem":1149},"CQRS та Mediator з MediatR в ASP.NET Core","/csharp/aspnet/libraries/mediatr","01.csharp/11.aspnet/09.libraries/05.mediatr",{"title":1151,"path":1152,"stem":1153},"Відмовостійкість з Polly в ASP.NET Core","/csharp/aspnet/libraries/polly","01.csharp/11.aspnet/09.libraries/06.polly",{"title":1155,"path":1156,"stem":1157},"Health Checks в ASP.NET Core","/csharp/aspnet/libraries/health-checks","01.csharp/11.aspnet/09.libraries/07.health-checks",{"title":1159,"path":1160,"stem":1161},"Feature Management та Feature Flags в ASP.NET Core","/csharp/aspnet/libraries/feature-management","01.csharp/11.aspnet/09.libraries/08.feature-management",{"title":1163,"path":1164,"stem":1165},"Відправка Email з FluentEmail в ASP.NET Core","/csharp/aspnet/libraries/fluent-email","01.csharp/11.aspnet/09.libraries/09.fluent-email",{"title":1167,"path":1168,"stem":1169},"Генерація PDF з QuestPDF в ASP.NET Core","/csharp/aspnet/libraries/quest-pdf","01.csharp/11.aspnet/09.libraries/10.quest-pdf",{"title":1171,"path":1172,"stem":1173},"Генерація тестових даних з Bogus в ASP.NET Core","/csharp/aspnet/libraries/bogus","01.csharp/11.aspnet/09.libraries/11.bogus",{"title":1175,"path":1176,"stem":1177},"Humanizer та Guard Clauses в ASP.NET Core","/csharp/aspnet/libraries/humanizer-guard","01.csharp/11.aspnet/09.libraries/12.humanizer-guard",{"title":1179,"path":1180,"stem":1181},"План модуля 10.libraries — Популярні бібліотеки ASP.NET","/csharp/aspnet/libraries/plan","01.csharp/11.aspnet/09.libraries/plan",{"title":1183,"icon":1184,"path":1185,"stem":1186,"children":1187,"page":59},"Razor Pages","i-lucide-layout-template","/csharp/aspnet/razor-pages","01.csharp/11.aspnet/10.razor-pages",[1188,1192,1196,1200,1204,1208],{"title":1189,"path":1190,"stem":1191},"Від Minimal API до Razor Pages: концептуальний перехід","/csharp/aspnet/razor-pages/from-minimal-api","01.csharp/11.aspnet/10.razor-pages/01.from-minimal-api",{"title":1193,"path":1194,"stem":1195},"PageModel: логіка сторінки Razor Pages","/csharp/aspnet/razor-pages/page-model","01.csharp/11.aspnet/10.razor-pages/02.page-model",{"title":1197,"path":1198,"stem":1199},"Razor синтаксис: шаблонізатор у .cshtml","/csharp/aspnet/razor-pages/razor-syntax","01.csharp/11.aspnet/10.razor-pages/03.razor-syntax",{"title":1201,"path":1202,"stem":1203},"Tag Helpers: типізований HTML","/csharp/aspnet/razor-pages/tag-helpers","01.csharp/11.aspnet/10.razor-pages/04.tag-helpers",{"title":1205,"path":1206,"stem":1207},"Форми і валідація: повний цикл обробки даних","/csharp/aspnet/razor-pages/forms-validation","01.csharp/11.aspnet/10.razor-pages/05.forms-validation",{"title":1209,"path":1210,"stem":1211},"Практичний проєкт: TaskManager на Razor Pages","/csharp/aspnet/razor-pages/project-task-manager","01.csharp/11.aspnet/10.razor-pages/06.project-task-manager",{"title":1213,"path":1214,"stem":1215,"children":1216,"page":59},"ASP.NET Core MVC","/csharp/aspnet/mvc","01.csharp/11.aspnet/11.mvc",[1217,1221,1225,1229,1233,1237,1241,1245,1249,1253,1257,1261,1265,1269,1273,1277,1281,1285],{"title":1218,"path":1219,"stem":1220},"Патерн MVC: архітектура, що змінила веб","/csharp/aspnet/mvc/mvc-pattern","01.csharp/11.aspnet/11.mvc/01.mvc-pattern",{"title":1222,"path":1223,"stem":1224},"Від Razor Pages до MVC: концептуальний перехід","/csharp/aspnet/mvc/from-razor-pages","01.csharp/11.aspnet/11.mvc/02.from-razor-pages",{"title":1226,"path":1227,"stem":1228},"Controllers та Actions: серце MVC","/csharp/aspnet/mvc/controllers-actions","01.csharp/11.aspnet/11.mvc/03.controllers-actions",{"title":1230,"path":1231,"stem":1232},"Маршрутизація в MVC: Convention vs Attribute Routing","/csharp/aspnet/mvc/routing-mvc","01.csharp/11.aspnet/11.mvc/04.routing-mvc",{"title":1234,"path":1235,"stem":1236},"Model Binding: від HTTP до C#","/csharp/aspnet/mvc/model-binding","01.csharp/11.aspnet/11.mvc/05.model-binding",{"title":1238,"path":1239,"stem":1240},"Views, ViewData, ViewBag, TempData і ViewModel","/csharp/aspnet/mvc/views-viewdata-tempdata","01.csharp/11.aspnet/11.mvc/06.views-viewdata-tempdata",{"title":1242,"path":1243,"stem":1244},"Filters: аспектно-орієнтоване програмування в MVC","/csharp/aspnet/mvc/filters","01.csharp/11.aspnet/11.mvc/07.filters",{"title":1246,"path":1247,"stem":1248},"Areas: структурування великих застосунків","/csharp/aspnet/mvc/areas","01.csharp/11.aspnet/11.mvc/08.areas",{"title":1250,"path":1251,"stem":1252},"View Components: повторювані незалежні блоки UI","/csharp/aspnet/mvc/view-components","01.csharp/11.aspnet/11.mvc/09.view-components",{"title":1254,"path":1255,"stem":1256},"Display та Editor Templates","/csharp/aspnet/mvc/display-editor-templates","01.csharp/11.aspnet/11.mvc/10.display-editor-templates",{"title":1258,"path":1259,"stem":1260},"Валідація: IValidatableObject та FluentValidation","/csharp/aspnet/mvc/validation-advanced","01.csharp/11.aspnet/11.mvc/11.validation-advanced",{"title":1262,"path":1263,"stem":1264},"HTMX: інтерактивність через HTML-атрибути","/csharp/aspnet/mvc/htmx","01.csharp/11.aspnet/11.mvc/12.htmx",{"title":1266,"path":1267,"stem":1268},"HTMX у ASP.NET Core MVC: серверна інтеграція","/csharp/aspnet/mvc/ajax-htmx-mvc","01.csharp/11.aspnet/11.mvc/13.ajax-htmx-mvc",{"title":1270,"path":1271,"stem":1272},"Практичний проєкт: Каталог товарів з HTMX","/csharp/aspnet/mvc/htmx-project","01.csharp/11.aspnet/11.mvc/14.htmx-project",{"title":1274,"path":1275,"stem":1276},"Завантаження та обробка файлів","/csharp/aspnet/mvc/file-upload","01.csharp/11.aspnet/11.mvc/15.file-upload",{"title":1278,"path":1279,"stem":1280},"Глобалізація та Локалізація MVC","/csharp/aspnet/mvc/globalization-localization","01.csharp/11.aspnet/11.mvc/16.globalization-localization",{"title":1282,"path":1283,"stem":1284},"Підсумковий проєкт: Блог-платформа","/csharp/aspnet/mvc/mvc-project","01.csharp/11.aspnet/11.mvc/17.mvc-project",{"title":1286,"path":1287,"stem":1288},"План курсу: ASP.NET Core MVC","/csharp/aspnet/mvc/plan","01.csharp/11.aspnet/11.mvc/plan",{"title":1290,"path":1291,"stem":1292,"children":1293,"page":59},"Web Api","/csharp/aspnet/web-api","01.csharp/11.aspnet/12.web-api",[1294,1298,1302,1306,1310,1314,1318,1322,1326,1330,1334,1338,1342],{"title":1295,"path":1296,"stem":1297},"Від Minimal API до Controller-based API","/csharp/aspnet/web-api/from-minimal-api-to-controllers","01.csharp/11.aspnet/12.web-api/01.from-minimal-api-to-controllers",{"title":1299,"path":1300,"stem":1301},"ControllerBase, ActionResult\u003CT> та Response Types","/csharp/aspnet/web-api/controller-base-actionresult","01.csharp/11.aspnet/12.web-api/02.controller-base-actionresult",{"title":1303,"path":1304,"stem":1305},"Content Negotiation - JSON, XML та власні форматери","/csharp/aspnet/web-api/content-negotiation","01.csharp/11.aspnet/12.web-api/03.content-negotiation",{"title":1307,"path":1308,"stem":1309},"Версіонування API","/csharp/aspnet/web-api/api-versioning","01.csharp/11.aspnet/12.web-api/04.api-versioning",{"title":1311,"path":1312,"stem":1313},"ProblemDetails та структурована обробка помилок","/csharp/aspnet/web-api/problemdetails-error-handling","01.csharp/11.aspnet/12.web-api/05.problemdetails-error-handling",{"title":1315,"path":1316,"stem":1317},"Фільтри у Web API контексті","/csharp/aspnet/web-api/filters-for-api","01.csharp/11.aspnet/12.web-api/06.filters-for-api",{"title":1319,"path":1320,"stem":1321},"Пагінація, фільтрація та сортування","/csharp/aspnet/web-api/pagination-filtering-sorting","01.csharp/11.aspnet/12.web-api/07.pagination-filtering-sorting",{"title":1323,"path":1324,"stem":1325},"HATEOAS та Resource Expansion","/csharp/aspnet/web-api/hateoas-resource-expansion","01.csharp/11.aspnet/12.web-api/08.hateoas-resource-expansion",{"title":1327,"path":1328,"stem":1329},"Гібридна архітектура - Minimal API + Controllers","/csharp/aspnet/web-api/minimal-api-vs-controllers-hybrid","01.csharp/11.aspnet/12.web-api/09.minimal-api-vs-controllers-hybrid",{"title":1331,"path":1332,"stem":1333},"Документація API - Swashbuckle, NSwag та генерація клієнтів","/csharp/aspnet/web-api/api-documentation-generation","01.csharp/11.aspnet/12.web-api/10.api-documentation-generation",{"title":1335,"path":1336,"stem":1337},"Health Checks та моніторинг API","/csharp/aspnet/web-api/health-checks-monitoring","01.csharp/11.aspnet/12.web-api/11.health-checks-monitoring",{"title":1339,"path":1340,"stem":1341},"Підсумковий проєкт - Production-Ready REST API","/csharp/aspnet/web-api/web-api-project","01.csharp/11.aspnet/12.web-api/12.web-api-project",{"title":1343,"path":1344,"stem":1345},"План курсу: ASP.NET Core Web API (Controllers)","/csharp/aspnet/web-api/plan","01.csharp/11.aspnet/12.web-api/plan",{"title":1347,"icon":1348,"path":1349,"stem":1350,"children":1351,"page":59},"Desktop UI","i-lucide-app-window","/csharp/desktop-ui","01.csharp/12.desktop-ui",[1352,1356,1360,1364,1368,1372,1376,1380,1384,1388,1392,1396,1400,1404,1408,1412,1416,1420,1424,1428,1432,1436,1440,1444,1448,1452,1456,1460,1464,1468,1472,1476,1480,1484,1488,1492,1496,1500,1504,1508,1512,1516,1520,1524,1528,1532,1536,1540,1544,1548,1552,1556,1560,1564,1568,1572,1576,1580,1584,1588,1592,1596,1600,1604,1608,1612,1616,1620,1624,1628,1632],{"title":1353,"path":1354,"stem":1355},"Що таке десктопна розробка?","/csharp/desktop-ui/what-is-desktop-dev","01.csharp/12.desktop-ui/01.what-is-desktop-dev",{"title":1357,"path":1358,"stem":1359},"Архітектура WPF — як влаштований графічний інтерфейс","/csharp/desktop-ui/wpf-architecture","01.csharp/12.desktop-ui/02.wpf-architecture",{"title":1361,"path":1362,"stem":1363},"Перший WPF-проєкт — від нуля до вікна","/csharp/desktop-ui/first-wpf-app","01.csharp/12.desktop-ui/03.first-wpf-app",{"title":1365,"path":1366,"stem":1367},"Перший Avalonia-проєкт: WPF для всіх платформ","/csharp/desktop-ui/03a.first-avalonia-app","01.csharp/12.desktop-ui/03a.first-avalonia-app",{"title":1369,"path":1370,"stem":1371},"XAML: декларативний інтерфейс","/csharp/desktop-ui/xaml-basics","01.csharp/12.desktop-ui/04.xaml-basics",{"title":1373,"path":1374,"stem":1375},"Fluent UI у WPF — сучасний дизайн Windows 11","/csharp/desktop-ui/04a.wpf-fluent-ui","01.csharp/12.desktop-ui/04a.wpf-fluent-ui",{"title":1377,"path":1378,"stem":1379},"WPF UI — сучасна бібліотека Fluent контролів","/csharp/desktop-ui/04b.wpf-ui-library","01.csharp/12.desktop-ui/04b.wpf-ui-library",{"title":1381,"path":1382,"stem":1383},"HandyControl — велика бібліотека UI контролів для WPF","/csharp/desktop-ui/04c.handycontrol-library","01.csharp/12.desktop-ui/04c.handycontrol-library",{"title":1385,"path":1386,"stem":1387},"Простори імен та ресурси XAML","/csharp/desktop-ui/xaml-namespaces-resources","01.csharp/12.desktop-ui/05.xaml-namespaces-resources",{"title":1389,"path":1390,"stem":1391},"XAML в Avalonia: ключові відмінності від WPF","/csharp/desktop-ui/05a.avalonia-xaml-differences","01.csharp/12.desktop-ui/05a.avalonia-xaml-differences",{"title":1393,"path":1394,"stem":1395},"Розширення розмітки XAML (Markup Extensions)","/csharp/desktop-ui/xaml-markup-extensions","01.csharp/12.desktop-ui/06.xaml-markup-extensions",{"title":1397,"path":1398,"stem":1399},"Панелі Layout: StackPanel, WrapPanel, DockPanel","/csharp/desktop-ui/layout-panels-part1","01.csharp/12.desktop-ui/07.layout-panels-part1",{"title":1401,"path":1402,"stem":1403},"Grid, Canvas, UniformGrid","/csharp/desktop-ui/layout-panels-part2","01.csharp/12.desktop-ui/07.layout-panels-part2",{"title":1405,"path":1406,"stem":1407},"Просунуті техніки Layout","/csharp/desktop-ui/layout-advanced","01.csharp/12.desktop-ui/08.layout-advanced",{"title":1409,"path":1410,"stem":1411},"Адаптивний Layout та найкращі практики","/csharp/desktop-ui/layout-responsive","01.csharp/12.desktop-ui/09.layout-responsive",{"title":1413,"path":1414,"stem":1415},"Layout в Avalonia: відмінності та нові можливості","/csharp/desktop-ui/09a.layout-avalonia","01.csharp/12.desktop-ui/09a.layout-avalonia",{"title":1417,"path":1418,"stem":1419},"Button, Image, ProgressBar та інші базові контроли","/csharp/desktop-ui/basic-controls","01.csharp/12.desktop-ui/10.basic-controls",{"title":1421,"path":1422,"stem":1423},"Контроли в Avalonia: відмінності від WPF","/csharp/desktop-ui/10a.controls-avalonia","01.csharp/12.desktop-ui/10a.controls-avalonia",{"title":1425,"path":1426,"stem":1427},"Текстові контроли — TextBlock, TextBox, RichTextBox","/csharp/desktop-ui/text-controls","01.csharp/12.desktop-ui/11.text-controls",{"title":1429,"path":1430,"stem":1431},"Контроли вибору — CheckBox, RadioButton, ComboBox, ListBox, DatePicker","/csharp/desktop-ui/selection-controls","01.csharp/12.desktop-ui/12.selection-controls",{"title":1433,"path":1434,"stem":1435},"Content Model — GroupBox, Expander, TabControl, StatusBar","/csharp/desktop-ui/content-controls","01.csharp/12.desktop-ui/13.content-controls",{"title":1437,"path":1438,"stem":1439},"UI/UX принципи десктопних застосунків","/csharp/desktop-ui/13a.ui-ux-principles","01.csharp/12.desktop-ui/13a.ui-ux-principles",{"title":1441,"path":1442,"stem":1443},"Dependency Properties — Концепція та Value Resolution","/csharp/desktop-ui/dependency-properties-part1","01.csharp/12.desktop-ui/14.dependency-properties-part1",{"title":1445,"path":1446,"stem":1447},"Avalonia Property System — StyledProperty та DirectProperty","/csharp/desktop-ui/14a.avalonia-property-system","01.csharp/12.desktop-ui/14a.avalonia-property-system",{"title":1449,"path":1450,"stem":1451},"Attached Properties — Властивості без меж","/csharp/desktop-ui/attached-properties","01.csharp/12.desktop-ui/15.attached-properties",{"title":1453,"path":1454,"stem":1455},"Routed Events — Маршрутизація подій у WPF","/csharp/desktop-ui/routed-events","01.csharp/12.desktop-ui/16.routed-events",{"title":1457,"path":1458,"stem":1459},"Data Binding — Від Code-Behind до Декларативності","/csharp/desktop-ui/data-binding-basics-part1","01.csharp/12.desktop-ui/17.data-binding-basics-part1",{"title":1461,"path":1462,"stem":1463},"INotifyPropertyChanged — Живе оновлення UI","/csharp/desktop-ui/data-binding-basics-part2","01.csharp/12.desktop-ui/17.data-binding-basics-part2",{"title":1465,"path":1466,"stem":1467},"Compiled Bindings в Avalonia — Безпека на етапі компіляції","/csharp/desktop-ui/17a.avalonia-compiled-bindings","01.csharp/12.desktop-ui/17a.avalonia-compiled-bindings",{"title":1469,"path":1470,"stem":1471},"Просунутий Data Binding — ElementName, RelativeSource, MultiBinding","/csharp/desktop-ui/data-binding-advanced","01.csharp/12.desktop-ui/18.data-binding-advanced",{"title":1473,"path":1474,"stem":1475},"Value Converters — Перетворення типів даних у Data Binding","/csharp/desktop-ui/value-converters","01.csharp/12.desktop-ui/19.value-converters",{"title":1477,"path":1478,"stem":1479},"Data Templates — Візуалізація об'єктів у WPF","/csharp/desktop-ui/data-templates","01.csharp/12.desktop-ui/20.data-templates",{"title":1481,"path":1482,"stem":1483},"Collections Binding Part 1 — ObservableCollection та ItemsControl","/csharp/desktop-ui/collections-binding-part1","01.csharp/12.desktop-ui/21.collections-binding-part1",{"title":1485,"path":1486,"stem":1487},"Collections Binding Part 2 — ICollectionView, Filtering, Sorting та Virtualization","/csharp/desktop-ui/collections-binding-part2","01.csharp/12.desktop-ui/21.collections-binding-part2",{"title":1489,"path":1490,"stem":1491},"MVVM Pattern — Від Spaghetti Code до архітектури","/csharp/desktop-ui/mvvm-pattern","01.csharp/12.desktop-ui/22.mvvm-pattern",{"title":1493,"path":1494,"stem":1495},"ViewModel Implementation — Від BaseViewModel до валідації","/csharp/desktop-ui/viewmodel-implementation","01.csharp/12.desktop-ui/23.viewmodel-implementation",{"title":1497,"path":1498,"stem":1499},"Commands — Від event handlers до декларативних команд","/csharp/desktop-ui/commands","01.csharp/12.desktop-ui/24.commands",{"title":1501,"path":1502,"stem":1503},"MVVM Toolkit — MVVM без boilerplate через Source Generators","/csharp/desktop-ui/mvvm-toolkit","01.csharp/12.desktop-ui/25.mvvm-toolkit",{"title":1505,"path":1506,"stem":1507},"Messenger Pattern — Комунікація між ViewModel без прямих посилань","/csharp/desktop-ui/messenger-pattern","01.csharp/12.desktop-ui/26.messenger-pattern",{"title":1509,"path":1510,"stem":1511},"Стилі WPF — CSS для десктопу","/csharp/desktop-ui/styles-basics","01.csharp/12.desktop-ui/27.styles-basics",{"title":1513,"path":1514,"stem":1515},"CSS-like стилі Avalonia","/csharp/desktop-ui/27a.avalonia-css-styling","01.csharp/12.desktop-ui/27a.avalonia-css-styling",{"title":1517,"path":1518,"stem":1519},"Control Templates — Частина 1. Концепція та TemplateBinding","/csharp/desktop-ui/control-templates-part1","01.csharp/12.desktop-ui/28.control-templates-part1",{"title":1521,"path":1522,"stem":1523},"Control Templates — Частина 2. Named Parts та ContentPresenter","/csharp/desktop-ui/control-templates-part2","01.csharp/12.desktop-ui/28.control-templates-part2",{"title":1525,"path":1526,"stem":1527},"Control Themes в Avalonia — нова ера стилізації","/csharp/desktop-ui/28a.avalonia-control-themes","01.csharp/12.desktop-ui/28a.avalonia-control-themes",{"title":1529,"path":1530,"stem":1531},"Triggers та Visual State Manager у WPF","/csharp/desktop-ui/triggers-visual-states","01.csharp/12.desktop-ui/29.triggers-visual-states",{"title":1533,"path":1534,"stem":1535},"Pseudo-classes в Avalonia — замість WPF Triggers","/csharp/desktop-ui/29a.avalonia-pseudo-classes","01.csharp/12.desktop-ui/29a.avalonia-pseudo-classes",{"title":1537,"path":1538,"stem":1539},"Теми та ресурсні словники у WPF","/csharp/desktop-ui/resources-themes","01.csharp/12.desktop-ui/30.resources-themes",{"title":1541,"path":1542,"stem":1543},"Avalonia Themes — Fluent Design та система тематизації","/csharp/desktop-ui/30a.avalonia-themes-fluent","01.csharp/12.desktop-ui/30a.avalonia-themes-fluent",{"title":1545,"path":1546,"stem":1547},"Контроли колекцій — глибоке занурення","/csharp/desktop-ui/collection-controls","01.csharp/12.desktop-ui/31.collection-controls",{"title":1549,"path":1550,"stem":1551},"DataGrid — колонки та базове відображення","/csharp/desktop-ui/datagrid-part1","01.csharp/12.desktop-ui/32.datagrid-part1",{"title":1553,"path":1554,"stem":1555},"DataGrid — сортування, фільтрація, редагування","/csharp/desktop-ui/datagrid-part2","01.csharp/12.desktop-ui/32.datagrid-part2",{"title":1557,"path":1558,"stem":1559},"TreeView та GridView","/csharp/desktop-ui/treeview-listview","01.csharp/12.desktop-ui/33.treeview-listview",{"title":1561,"path":1562,"stem":1563},"Меню, Toolbar, ContextMenu, StatusBar","/csharp/desktop-ui/menus-toolbars","01.csharp/12.desktop-ui/34.menus-toolbars",{"title":1565,"path":1566,"stem":1567},"Навігація та керування вікнами. Частина 1: вікна та сторінки","/csharp/desktop-ui/navigation-windows-part1","01.csharp/12.desktop-ui/35.navigation-windows-part1",{"title":1569,"path":1570,"stem":1571},"Навігація та керування вікнами. Частина 2: MVVM-навігація","/csharp/desktop-ui/navigation-windows-part2","01.csharp/12.desktop-ui/35.navigation-windows-part2",{"title":1573,"path":1574,"stem":1575},"Avalonia — Навігація та діалоги","/csharp/desktop-ui/35a.avalonia-navigation-dialogs","01.csharp/12.desktop-ui/35a.avalonia-navigation-dialogs",{"title":1577,"path":1578,"stem":1579},"Діалоги та File Pickers у WPF","/csharp/desktop-ui/dialogs-file-pickers","01.csharp/12.desktop-ui/36.dialogs-file-pickers",{"title":1581,"path":1582,"stem":1583},"UserControl: компонентний підхід у WPF","/csharp/desktop-ui/user-controls","01.csharp/12.desktop-ui/37.user-controls",{"title":1585,"path":1586,"stem":1587},"Custom Controls: Lookless Controls у WPF","/csharp/desktop-ui/custom-controls","01.csharp/12.desktop-ui/38.custom-controls",{"title":1589,"path":1590,"stem":1591},"Avalonia TemplatedControl — Lookless Controls","/csharp/desktop-ui/38a.avalonia-templated-controls","01.csharp/12.desktop-ui/38a.avalonia-templated-controls",{"title":1593,"path":1594,"stem":1595},"Анімації у WPF: Storyboard та Easing Functions","/csharp/desktop-ui/animations-transitions","01.csharp/12.desktop-ui/39.animations-transitions",{"title":1597,"path":1598,"stem":1599},"Анімації в Avalonia","/csharp/desktop-ui/39a.avalonia-animations","01.csharp/12.desktop-ui/39a.avalonia-animations",{"title":1601,"path":1602,"stem":1603},"2D Графіка та Мультимедіа у WPF","/csharp/desktop-ui/media-graphics","01.csharp/12.desktop-ui/40.media-graphics",{"title":1605,"path":1606,"stem":1607},"Dependency Injection у WPF та Avalonia","/csharp/desktop-ui/di-integration","01.csharp/12.desktop-ui/41.di-integration",{"title":1609,"path":1610,"stem":1611},"SQLite та EF Core у десктопних додатках","/csharp/desktop-ui/data-persistence-part1","01.csharp/12.desktop-ui/42.data-persistence-part1",{"title":1613,"path":1614,"stem":1615},"Repository Pattern та Unit of Work","/csharp/desktop-ui/data-persistence-part2","01.csharp/12.desktop-ui/43.data-persistence-part2",{"title":1617,"path":1618,"stem":1619},"Тестування ViewModels","/csharp/desktop-ui/viewmodel-testing","01.csharp/12.desktop-ui/44.viewmodel-testing",{"title":1621,"path":1622,"stem":1623},"Avalonia Headless Testing — тестування UI без вікон","/csharp/desktop-ui/44a.avalonia-headless-testing","01.csharp/12.desktop-ui/44a.avalonia-headless-testing",{"title":1625,"path":1626,"stem":1627},"Кросплатформна розробка з Avalonia","/csharp/desktop-ui/avalonia-cross-platform","01.csharp/12.desktop-ui/45.avalonia-cross-platform",{"title":1629,"path":1630,"stem":1631},"Пакування та розгортання Avalonia додатків","/csharp/desktop-ui/avalonia-packaging-deployment","01.csharp/12.desktop-ui/46.avalonia-packaging-deployment",{"title":1633,"path":1634,"stem":1635},"Розгортання WPF застосунків","/csharp/desktop-ui/wpf-packaging-deployment","01.csharp/12.desktop-ui/47.wpf-packaging-deployment",{"title":1637,"path":1638,"stem":1639},"C# & .NET: The Ultimate Roadmap","/csharp/roadmap","01.csharp/roadmap",{"title":1641,"icon":1642,"path":1643,"stem":1644,"children":1645,"page":59},"C++","i-devicon-cplusplus","/cpp","02.cpp",[1646,1650,1654,1658,1662,1666,1670,1674,1678,1681,1685,1689,1693,1697,1701,1705,1709,1713,1717,1721,1725,1729,1733,1737,1741,1745,1749,1753,1757,1761],{"title":1647,"path":1648,"stem":1649},"Вступ у програмування та алгоритми","/cpp/intro-algorithms","02.cpp/01.intro-algorithms",{"title":1651,"path":1652,"stem":1653},"Code Style: угоди про оформлення коду","/cpp/code-style","02.cpp/02.code-style",{"title":1655,"path":1656,"stem":1657},"Середовище розробки та перший проєкт","/cpp/ide-setup","02.cpp/03.ide-setup",{"title":1659,"path":1660,"stem":1661},"Вивід даних на екран","/cpp/data-output","02.cpp/04.data-output",{"title":1663,"path":1664,"stem":1665},"Типи даних, змінні та константи","/cpp/data-types-variables","02.cpp/05.data-types-variables",{"title":1667,"path":1668,"stem":1669},"Ввід даних з клавіатури","/cpp/data-input","02.cpp/06.data-input",{"title":1671,"path":1672,"stem":1673},"Оператори, перетворення типів та логічні операції","/cpp/operators-type-conversion","02.cpp/07.operators-type-conversion",{"title":1675,"path":1676,"stem":1677},"Цикли","/cpp/loops","02.cpp/08.loops",{"title":32,"path":1679,"stem":1680},"/cpp/arrays","02.cpp/09.arrays",{"title":1682,"path":1683,"stem":1684},"Алгоритми сортування та аналіз складності","/cpp/sorting","02.cpp/10.sorting",{"title":1686,"path":1687,"stem":1688},"Алгоритми пошуку","/cpp/searching","02.cpp/11.searching",{"title":1690,"path":1691,"stem":1692},"Функції: основи","/cpp/functions-basics","02.cpp/12.functions-basics",{"title":1694,"path":1695,"stem":1696},"Функції: прототипи, область видимості та додаткові можливості","/cpp/functions-scope","02.cpp/13.functions-scope",{"title":1698,"path":1699,"stem":1700},"Функції: перевантаження та шаблони","/cpp/functions-overloading-templates","02.cpp/14.functions-overloading-templates",{"title":1702,"path":1703,"stem":1704},"Вказівники: основи","/cpp/pointers-basics","02.cpp/15.pointers-basics",{"title":1706,"path":1707,"stem":1708},"Посилання (References)","/cpp/references","02.cpp/16.references",{"title":1710,"path":1711,"stem":1712},"Вказівники, const і масиви","/cpp/pointers-const-arrays","02.cpp/17.pointers-const-arrays",{"title":1714,"path":1715,"stem":1716},"Адресна арифметика","/cpp/pointer-arithmetic","02.cpp/18.pointer-arithmetic",{"title":1718,"path":1719,"stem":1720},"Динамічна пам'ять","/cpp/dynamic-memory","02.cpp/19.dynamic-memory",{"title":1722,"path":1723,"stem":1724},"Вказівники типу void","/cpp/void-pointers","02.cpp/20.void-pointers",{"title":1726,"path":1727,"stem":1728},"Вказівники на вказівники","/cpp/pointers-to-pointers","02.cpp/21.pointers-to-pointers",{"title":1730,"path":1731,"stem":1732},"Оператор доступу до членів через вказівник (->)","/cpp/member-access-operator","02.cpp/22.member-access-operator",{"title":1734,"path":1735,"stem":1736},"Цикл for-each (Range-based for)","/cpp/foreach-loop","02.cpp/23.foreach-loop",{"title":1738,"path":1739,"stem":1740},"Вказівники на функції","/cpp/function-pointers","02.cpp/24.function-pointers",{"title":1742,"path":1743,"stem":1744},"Лямбда-вирази","/cpp/lambdas","02.cpp/25.lambdas",{"title":1746,"path":1747,"stem":1748},"Лямбда-захоплення","/cpp/lambda-captures","02.cpp/26.lambda-captures",{"title":1750,"path":1751,"stem":1752},"Еліпсис","/cpp/ellipsis","02.cpp/27.ellipsis",{"title":1754,"path":1755,"stem":1756},"Аргументи командного рядка","/cpp/command-line-arguments","02.cpp/28.command-line-arguments",{"title":1758,"path":1759,"stem":1760},"Перерахування (enum)","/cpp/enum","02.cpp/29.enum",{"title":1762,"path":1763,"stem":1764},"План навчання: Курс C++ — Продовження (Статті 29–60+)","/cpp/curriculum-plan","02.cpp/curriculum-plan",{"title":1766,"icon":1767,"path":1768,"stem":1769,"children":1770,"page":59},"JavaScript","i-devicon-javascript","/javascript","03.javascript",[1771,1797,1851,1873,2177,2215],{"title":1772,"icon":1773,"path":1774,"stem":1775,"children":1776,"page":59},"Events","i-lucide-mouse-pointer-click","/javascript/events","03.javascript/01.events",[1777,1781,1785,1789,1793],{"title":1778,"path":1779,"stem":1780},"Вступ до подій браузера","/javascript/events/intro","03.javascript/01.events/01.intro",{"title":1782,"path":1783,"stem":1784},"Бульбашковий механізм (Bubbling) та занурення (Capturing)","/javascript/events/bubbling-capturing","03.javascript/01.events/02.bubbling-capturing",{"title":1786,"path":1787,"stem":1788},"Делегування подій (Event Delegation)","/javascript/events/delegate-events","03.javascript/01.events/03.delegate-events",{"title":1790,"path":1791,"stem":1792},"Типові дії браузера та preventDefault()","/javascript/events/prevent-default","03.javascript/01.events/04.prevent-default",{"title":1794,"path":1795,"stem":1796},"Запуск користувацьких подій (Custom Events)","/javascript/events/custom-events","03.javascript/01.events/05.custom-events",{"title":1798,"icon":1799,"path":1800,"stem":1801,"children":1802,"page":59},"Network","i-lucide-globe","/javascript/network","03.javascript/02.network",[1803,1807,1811,1815,1819,1823,1827,1831,1835,1839,1843,1847],{"title":1804,"path":1805,"stem":1806},"Fetch API - Сучасний підхід до HTTP-запитів","/javascript/network/01-fetch-api","03.javascript/02.network/01-fetch-api",{"title":1808,"path":1809,"stem":1810},"FormData - Робота з формами та файлами","/javascript/network/02-formdata","03.javascript/02.network/02-formdata",{"title":1812,"path":1813,"stem":1814},"Відстеження прогресу завантаження","/javascript/network/03-download-progress","03.javascript/02.network/03-download-progress",{"title":1816,"path":1817,"stem":1818},"Переривання fetch-запитів","/javascript/network/04-abort-requests","03.javascript/02.network/04-abort-requests",{"title":1820,"path":1821,"stem":1822},"CORS - Запити між різними джерелами","/javascript/network/05-cors","03.javascript/02.network/05-cors",{"title":1824,"path":1825,"stem":1826},"Fetch API - Повний довідник опцій","/javascript/network/06-fetch-options","03.javascript/02.network/06-fetch-options",{"title":1828,"path":1829,"stem":1830},"URL Objects - Робота з посиланнями","/javascript/network/07-url-objects","03.javascript/02.network/07-url-objects",{"title":1832,"path":1833,"stem":1834},"XMLHttpRequest - AJAX та низькорівневі запити","/javascript/network/08-xmlhttprequest","03.javascript/02.network/08-xmlhttprequest",{"title":1836,"path":1837,"stem":1838},"Відновлюване завантаження файлів","/javascript/network/09-resumable-upload","03.javascript/02.network/09-resumable-upload",{"title":1840,"path":1841,"stem":1842},"Cookies, document.cookie та світ після \"Cookiepocalypse\"","/javascript/network/10-cookies","03.javascript/02.network/10-cookies",{"title":1844,"path":1845,"stem":1846},"js-cookie: Керування Cookies без Болю","/javascript/network/11-js-cookie","03.javascript/02.network/11-js-cookie",{"title":1848,"path":1849,"stem":1850},"Axios: Потужний HTTP-клієнт для JavaScript","/javascript/network/12-axios","03.javascript/02.network/12-axios",{"title":1852,"icon":1853,"path":1854,"stem":1855,"children":1856,"page":59},"Bom","i-lucide-monitor","/javascript/bom","03.javascript/03.bom",[1857,1861,1865,1869],{"title":1858,"path":1859,"stem":1860},"LocalStorage, SessionStorage та patterns збереження даних","/javascript/bom/01-localstorage","03.javascript/03.bom/01-localstorage",{"title":1862,"path":1863,"stem":1864},"Location Object - Керування адресою сторінки","/javascript/bom/02-location-object","03.javascript/03.bom/02-location-object",{"title":1866,"path":1867,"stem":1868},"History API - Керування історією браузера","/javascript/bom/03-history-api","03.javascript/03.bom/03-history-api",{"title":1870,"path":1871,"stem":1872},"Navigator Object - Ідентифікація та Можливості Пристрою","/javascript/bom/04-navigator-object","03.javascript/03.bom/04-navigator-object",{"title":1874,"icon":1875,"path":1876,"stem":1877,"children":1878},"React","i-devicon-react","/javascript/react","03.javascript/04.react/index",[1879,1880,1884,1888,1892,1896,1959,1994,2146],{"title":1874,"path":1876,"stem":1877},{"title":1881,"path":1882,"stem":1883},"Робота з Формами в React","/javascript/react/react-forms","03.javascript/04.react/01.react-forms",{"title":1885,"path":1886,"stem":1887},"React Hook Form: Професійна Робота з Формами","/javascript/react/react-hook-form","03.javascript/04.react/02.react-hook-form",{"title":1889,"path":1890,"stem":1891},"React Hook Form: Глибоке Розуміння Архітектури та Оптимізації","/javascript/react/react-hook-form-new","03.javascript/04.react/02.react-hook-form-new",{"title":1893,"path":1894,"stem":1895},"Axios та React: Професійна Архітектура Запитів","/javascript/react/data-fetching-axios","03.javascript/04.react/03.data-fetching-axios",{"title":1897,"icon":132,"path":1898,"stem":1899,"children":1900},"Tanstack Query","/javascript/react/tanstack-query","03.javascript/04.react/04.tanstack-query/index",[1901,1903,1907,1911,1915,1919,1923,1927,1931,1935,1939,1943,1947,1951,1955],{"title":1902,"path":1898,"stem":1899},"TanStack Query: Майстерність Керування Станом Сервера",{"title":1904,"path":1905,"stem":1906},"Парадигма Server State: Чому useEffect недостатньо","/javascript/react/tanstack-query/server-state-paradigm","03.javascript/04.react/04.tanstack-query/01.server-state-paradigm",{"title":1908,"path":1909,"stem":1910},"Встановлення та Налаштування: Фундамент","/javascript/react/tanstack-query/installation-and-devtools","03.javascript/04.react/04.tanstack-query/02.installation-and-devtools",{"title":1912,"path":1913,"stem":1914},"Основи Запитів та Магія Ключів","/javascript/react/tanstack-query/query-basics-and-keys","03.javascript/04.react/04.tanstack-query/03.query-basics-and-keys",{"title":1916,"path":1917,"stem":1918},"Синхронізація Даних: Життєвий Цикл Запиту","/javascript/react/tanstack-query/data-synchronization","03.javascript/04.react/04.tanstack-query/04.data-synchronization",{"title":1920,"path":1921,"stem":1922},"Мутації та Інвалідація: Зміна Даних","/javascript/react/tanstack-query/mutations-and-invalidation","03.javascript/04.react/04.tanstack-query/05.mutations-and-invalidation",{"title":1924,"path":1925,"stem":1926},"Оптимістичні Оновлення: Швидше за Світло","/javascript/react/tanstack-query/optimistic-updates","03.javascript/04.react/04.tanstack-query/06.optimistic-updates",{"title":1928,"path":1929,"stem":1930},"Пагінація та Infinite Scroll","/javascript/react/tanstack-query/pagination-and-load-more","03.javascript/04.react/04.tanstack-query/07.pagination-and-load-more",{"title":1932,"path":1933,"stem":1934},"Просунуті Патерни та Оптимізація","/javascript/react/tanstack-query/advanced-patterns","03.javascript/04.react/04.tanstack-query/08.advanced-patterns",{"title":1936,"path":1937,"stem":1938},"Архітектура та Best Practices","/javascript/react/tanstack-query/architecture-and-best-practices","03.javascript/04.react/04.tanstack-query/09.architecture-and-best-practices",{"title":1940,"path":1941,"stem":1942},"Server-Side Rendering (SSR) та Гідратація","/javascript/react/tanstack-query/server-side-rendering","03.javascript/04.react/04.tanstack-query/10.server-side-rendering",{"title":1944,"path":1945,"stem":1946},"Стратегії Тестування","/javascript/react/tanstack-query/testing-strategies","03.javascript/04.react/04.tanstack-query/11.testing-strategies",{"title":1948,"path":1949,"stem":1950},"Аутентифікація та Обробка Помилок","/javascript/react/tanstack-query/authentication-and-errors","03.javascript/04.react/04.tanstack-query/12.authentication-and-errors",{"title":1952,"path":1953,"stem":1954},"React Suspense та Майбутнє","/javascript/react/tanstack-query/react-suspense","03.javascript/04.react/04.tanstack-query/13.react-suspense",{"title":1956,"path":1957,"stem":1958},"Глибоке Занурення в Продуктивність","/javascript/react/tanstack-query/performance-deep-dive","03.javascript/04.react/04.tanstack-query/14.performance-deep-dive",{"title":1960,"icon":1875,"path":1961,"stem":1962,"children":1963},"React Router","/javascript/react/react-router","03.javascript/04.react/05.react-router/index",[1964,1966,1970,1974,1978,1982,1986,1990],{"title":1965,"path":1961,"stem":1962},"React Router: Навігаційна система сучасного вебу",{"title":1967,"path":1968,"stem":1969},"Налаштування та Базовий Роутинг","/javascript/react/react-router/setup-and-basic-routing","03.javascript/04.react/05.react-router/01.setup-and-basic-routing",{"title":1971,"path":1972,"stem":1973},"Динамічна Навігація","/javascript/react/react-router/navigation-and-links","03.javascript/04.react/05.react-router/02.navigation-and-links",{"title":1975,"path":1976,"stem":1977},"Вкладені Маршрути та Макети","/javascript/react/react-router/nested-routes-and-layouts","03.javascript/04.react/05.react-router/03.nested-routes-and-layouts",{"title":1979,"path":1980,"stem":1981},"Динамічні Маршрути та Параметри","/javascript/react/react-router/dynamic-routing","03.javascript/04.react/05.react-router/04.dynamic-routing",{"title":1983,"path":1984,"stem":1985},"Data APIs: Loaders та Actions","/javascript/react/react-router/data-loading","03.javascript/04.react/05.react-router/05.data-loading",{"title":1987,"path":1988,"stem":1989},"Просунуті Патерни","/javascript/react/react-router/advanced-patterns","03.javascript/04.react/05.react-router/06.advanced-patterns",{"title":1991,"path":1992,"stem":1993},"Legacy Routing: Компонентний підхід","/javascript/react/react-router/legacy-routing","03.javascript/04.react/05.react-router/07.legacy-routing",{"title":1995,"icon":132,"path":1996,"stem":1997,"children":1998},"Redux","/javascript/react/redux","03.javascript/04.react/06.redux/index",[1999,2001,2017,2046,2055,2076,2092,2121],{"title":2000,"path":1996,"stem":1997},"Redux: Еволюція управління станом",{"title":14,"icon":15,"path":2002,"stem":2003,"children":2004,"page":59},"/javascript/react/redux/fundamentals","03.javascript/04.react/06.redux/01.fundamentals",[2005,2009,2013],{"title":2006,"path":2007,"stem":2008},"Вступ до State Management","/javascript/react/redux/fundamentals/intro-state-management","03.javascript/04.react/06.redux/01.fundamentals/01.intro-state-management",{"title":2010,"path":2011,"stem":2012},"Філософія Redux та Три Принципи","/javascript/react/redux/fundamentals/redux-philosophy","03.javascript/04.react/06.redux/01.fundamentals/02.redux-philosophy",{"title":2014,"path":2015,"stem":2016},"Чисті функції та Іммутабельність","/javascript/react/redux/fundamentals/pure-functions-immutability","03.javascript/04.react/06.redux/01.fundamentals/03.pure-functions-immutability",{"title":2018,"icon":132,"path":2019,"stem":2020,"children":2021,"page":59},"Classic Redux","/javascript/react/redux/classic-redux","03.javascript/04.react/06.redux/02.classic-redux",[2022,2026,2030,2034,2038,2042],{"title":2023,"path":2024,"stem":2025},"Створення Store (Classic Redux)","/javascript/react/redux/classic-redux/store-setup","03.javascript/04.react/06.redux/02.classic-redux/01.store-setup",{"title":2027,"path":2028,"stem":2029},"Actions, Constants та Action Creators","/javascript/react/redux/classic-redux/actions-constants","03.javascript/04.react/06.redux/02.classic-redux/02.actions-constants",{"title":2031,"path":2032,"stem":2033},"Логіка Reducers","/javascript/react/redux/classic-redux/reducers","03.javascript/04.react/06.redux/02.classic-redux/03.reducers",{"title":2035,"path":2036,"stem":2037},"Комбінування Reducers (Root Reducer)","/javascript/react/redux/classic-redux/data-flow","03.javascript/04.react/06.redux/02.classic-redux/04.data-flow",{"title":2039,"path":2040,"stem":2041},"Підключення до React (React-Redux)","/javascript/react/redux/classic-redux/react-redux-connection","03.javascript/04.react/06.redux/02.classic-redux/05.react-redux-connection",{"title":2043,"path":2044,"stem":2045},"Middleware та Асинхронність (Redux Thunk)","/javascript/react/redux/classic-redux/middleware-thunk","03.javascript/04.react/06.redux/02.classic-redux/06.middleware-thunk",{"title":2047,"icon":132,"path":2048,"stem":2049,"children":2050,"page":59},"Transition To Rtk","/javascript/react/redux/transition-to-rtk","03.javascript/04.react/06.redux/03.transition-to-rtk",[2051],{"title":2052,"path":2053,"stem":2054},"Проблеми класичного Redux","/javascript/react/redux/transition-to-rtk/problems-with-classic","03.javascript/04.react/06.redux/03.transition-to-rtk/01.problems-with-classic",{"title":2056,"icon":132,"path":2057,"stem":2058,"children":2059,"page":59},"Redux Toolkit","/javascript/react/redux/redux-toolkit","03.javascript/04.react/06.redux/04.redux-toolkit",[2060,2064,2068,2072],{"title":2061,"path":2062,"stem":2063},"Налаштування Store з configureStore","/javascript/react/redux/redux-toolkit/configure-store","03.javascript/04.react/06.redux/04.redux-toolkit/01.configure-store",{"title":2065,"path":2066,"stem":2067},"createSlice: Революція в Redux","/javascript/react/redux/redux-toolkit/create-slice","03.javascript/04.react/06.redux/04.redux-toolkit/02.create-slice",{"title":2069,"path":2070,"stem":2071},"Асинхронність з createAsyncThunk","/javascript/react/redux/redux-toolkit/async-thunks","03.javascript/04.react/06.redux/04.redux-toolkit/03.async-thunks",{"title":2073,"path":2074,"stem":2075},"04. Entity Adapter: Керування нормалізованим станом","/javascript/react/redux/redux-toolkit/entity-adapter","03.javascript/04.react/06.redux/04.redux-toolkit/04.entity-adapter",{"title":2077,"icon":92,"path":2078,"stem":2079,"children":2080,"page":59},"Advanced","/javascript/react/redux/advanced","03.javascript/04.react/06.redux/05.advanced",[2081,2085,2089],{"title":2082,"path":2083,"stem":2084},"Мемоізація та Селектори: Повний Гайд по Reselect","/javascript/react/redux/advanced/selectors-reselect","03.javascript/04.react/06.redux/05.advanced/01.selectors-reselect",{"title":2086,"path":2087,"stem":2088},"RTK Query: Архітектура Серверного Кешу","/javascript/react/redux/advanced/rtk-query-intro","03.javascript/04.react/06.redux/05.advanced/02.rtk-query-intro",{"title":1936,"path":2090,"stem":2091},"/javascript/react/redux/advanced/architecture-best-practices","03.javascript/04.react/06.redux/05.advanced/03.architecture-best-practices",{"title":2093,"icon":132,"path":2094,"stem":2095,"children":2096,"page":59},"Project Kanban","/javascript/react/redux/project-kanban","03.javascript/04.react/06.redux/06.project-kanban",[2097,2101,2105,2109,2113,2117],{"title":2098,"path":2099,"stem":2100},"Проєкт: Kanban Board (Trello Clone)","/javascript/react/redux/project-kanban/project-overview","03.javascript/04.react/06.redux/06.project-kanban/01.project-overview",{"title":2102,"path":2103,"stem":2104},"Налаштування та Типізація","/javascript/react/redux/project-kanban/setup-and-types","03.javascript/04.react/06.redux/06.project-kanban/02.setup-and-types",{"title":2106,"path":2107,"stem":2108},"Board Slice: Серце Дошки","/javascript/react/redux/project-kanban/board-slice","03.javascript/04.react/06.redux/06.project-kanban/03.board-slice",{"title":2110,"path":2111,"stem":2112},"Логіка Drag & Drop","/javascript/react/redux/project-kanban/drag-and-drop-logic","03.javascript/04.react/06.redux/06.project-kanban/04.drag-and-drop-logic",{"title":2114,"path":2115,"stem":2116},"Інтеграція з RTK Query","/javascript/react/redux/project-kanban/rtk-query-integration","03.javascript/04.react/06.redux/06.project-kanban/05.rtk-query-integration",{"title":2118,"path":2119,"stem":2120},"Optimistic Updates","/javascript/react/redux/project-kanban/optimistic-updates","03.javascript/04.react/06.redux/06.project-kanban/06.optimistic-updates",{"title":2122,"icon":132,"path":2123,"stem":2124,"children":2125,"page":59},"Testing","/javascript/react/redux/testing","03.javascript/04.react/06.redux/07.testing",[2126,2130,2134,2138,2142],{"title":2127,"path":2128,"stem":2129},"Тестування Redux","/javascript/react/redux/testing/intro-testing","03.javascript/04.react/06.redux/07.testing/01.intro-testing",{"title":2131,"path":2132,"stem":2133},"Тестування Reducers","/javascript/react/redux/testing/testing-reducers","03.javascript/04.react/06.redux/07.testing/02.testing-reducers",{"title":2135,"path":2136,"stem":2137},"Тестування Селекторів","/javascript/react/redux/testing/testing-selectors","03.javascript/04.react/06.redux/07.testing/03.testing-selectors",{"title":2139,"path":2140,"stem":2141},"Тестування Компонентів (Integration)","/javascript/react/redux/testing/testing-components","03.javascript/04.react/06.redux/07.testing/04.testing-components",{"title":2143,"path":2144,"stem":2145},"Тестування Async Thunks","/javascript/react/redux/testing/testing-thunks","03.javascript/04.react/06.redux/07.testing/05.testing-thunks",{"title":2147,"icon":132,"path":2148,"stem":2149,"children":2150},"Ui Libraries","/javascript/react/ui-libraries","03.javascript/04.react/07.ui-libraries/index",[2151,2153,2157,2161,2165,2169,2173],{"title":2152,"path":2148,"stem":2149},"UI Бібліотеки в React",{"title":2154,"path":2155,"stem":2156},"Вступ до UI Бібліотек: Навіщо Винаходити Велосипед Двічі?","/javascript/react/ui-libraries/introduction-to-ui-libraries","03.javascript/04.react/07.ui-libraries/01.introduction-to-ui-libraries",{"title":2158,"path":2159,"stem":2160},"Філософія shadcn/ui: \"Not a Component Library\"","/javascript/react/ui-libraries/shadcn-philosophy","03.javascript/04.react/07.ui-libraries/02.shadcn-philosophy",{"title":2162,"path":2163,"stem":2164},"Установка та Налаштування shadcn/ui","/javascript/react/ui-libraries/shadcn-installation","03.javascript/04.react/07.ui-libraries/03.shadcn-installation",{"title":2166,"path":2167,"stem":2168},"Базові Компоненти shadcn/ui: Фундамент Інтерфейсу","/javascript/react/ui-libraries/shadcn-components-basics","03.javascript/04.react/07.ui-libraries/04.shadcn-components-basics",{"title":2170,"path":2171,"stem":2172},"Компоненти Форм: Побудова Інтерактивних Form","/javascript/react/ui-libraries/shadcn-components-forms","03.javascript/04.react/07.ui-libraries/05.shadcn-components-forms",{"title":2174,"path":2175,"stem":2176},"Складні Компоненти: Dialog, Dropdown, Table та Command","/javascript/react/ui-libraries/shadcn-components-advanced","03.javascript/04.react/07.ui-libraries/06.shadcn-components-advanced",{"title":2178,"icon":2179,"path":2180,"stem":2181,"children":2182,"page":59},"TypeScript","i-devicon-typescript","/javascript/typescript","03.javascript/05.typescript",[2183,2187,2191,2195,2199,2203,2207,2211],{"title":2184,"path":2185,"stem":2186},"TypeScript: Броня для вашого коду","/javascript/typescript/intro-and-basic-types","03.javascript/05.typescript/01.intro-and-basic-types",{"title":2188,"path":2189,"stem":2190},"Майстерність Моделювання Даних: Інтерфейси та Просунуті Типи","/javascript/typescript/interfaces-and-advanced-types","03.javascript/05.typescript/02.interfaces-and-advanced-types",{"title":2192,"path":2193,"stem":2194},"Алхімія Типів: Generics та Utility Types","/javascript/typescript/generics-and-utilities","03.javascript/05.typescript/03.generics-and-utilities",{"title":2196,"path":2197,"stem":2198},"Архітектура та Шаблони: Класи в TypeScript","/javascript/typescript/classes-and-oop","03.javascript/05.typescript/04.classes-and-oop",{"title":2200,"path":2201,"stem":2202},"Продакшн та Екосистема: Advanced Config & Workflow","/javascript/typescript/advanced-patterns-and-config","03.javascript/05.typescript/05.advanced-patterns-and-config",{"title":2204,"path":2205,"stem":2206},"TypeScript у світі React","/javascript/typescript/react-basics","03.javascript/05.typescript/06.react-basics",{"title":2208,"path":2209,"stem":2210},"React + TypeScript: Продвинуті патерни","/javascript/typescript/react-advanced","03.javascript/05.typescript/07.react-advanced",{"title":2212,"path":2213,"stem":2214},"React + TypeScript: Екосистема та бібліотеки","/javascript/typescript/react-ecosystem","03.javascript/05.typescript/08.react-ecosystem",{"title":2216,"path":2217,"stem":2218},"Atomic Design","/javascript/atomic-design","03.javascript/2.atomic-design",{"title":2220,"icon":2221,"path":2222,"stem":2223,"children":2224,"page":59},"Java","i-devicon-java","/java","04.java",[2225,2228,2231,2235,2239,2243,2247],{"title":162,"path":2226,"stem":2227},"/java/data-mapper-part1","04.java/01.data-mapper-part1",{"title":166,"path":2229,"stem":2230},"/java/data-mapper-part2","04.java/02.data-mapper-part2",{"title":2232,"path":2233,"stem":2234},"Service Layer: Організація бізнес-логіки","/java/service-layer","04.java/03.service-layer",{"title":2236,"path":2237,"stem":2238},"Rich Domain Model та State Pattern","/java/rich-domain-model","04.java/04.rich-domain-model",{"title":2240,"path":2241,"stem":2242},"Патерни для складної бізнес-логіки","/java/business-logic-patterns","04.java/05.business-logic-patterns",{"title":2244,"path":2245,"stem":2246},"Обробка помилок та валідація","/java/error-handling-validation","04.java/06.error-handling-validation",{"title":2248,"path":2249,"stem":2250,"children":2251,"page":59},"Проектування баз даних","/java/pr2","04.java/pr2",[2252,2256,2260,2264,2268,2272,2276,2280,2284,2288,2292,2296,2300,2304,2308,2312,2316,2320,2324,2328,2332,2336,2340,2344,2348],{"title":2253,"path":2254,"stem":2255},"Концептуальне моделювання: Мистецтво розуміння предметної області","/java/pr2/conceptual-modeling","04.java/pr2/01.conceptual-modeling",{"title":2257,"path":2258,"stem":2259},"Логічне моделювання: Від бізнес-ідей до структур даних","/java/pr2/logical-modeling","04.java/pr2/02.logical-modeling",{"title":2261,"path":2262,"stem":2263},"Нормалізація: Гігієна даних та боротьба з аномаліями","/java/pr2/normalization","04.java/pr2/03.normalization",{"title":2265,"path":2266,"stem":2267},"Фізична схема: Від абстракції до DDL","/java/pr2/physical-schema","04.java/pr2/04.physical-schema",{"title":2269,"path":2270,"stem":2271},"Архітектурна класифікація таблиць","/java/pr2/table-classification","04.java/pr2/05.table-classification",{"title":2273,"path":2274,"stem":2275},"Database Migrations: Версіонування схеми з Flyway","/java/pr2/database-migrations","04.java/pr2/06.database-migrations",{"title":2277,"path":2278,"stem":2279},"А що, якби це була не реляційна БД?","/java/pr2/beyond-relational","04.java/pr2/07.beyond-relational",{"title":2281,"path":2282,"stem":2283},"Object-Relational Impedance Mismatch: Два світи, що не хочуть дружити","/java/pr2/impedance-mismatch","04.java/pr2/09.impedance-mismatch",{"title":2285,"path":2286,"stem":2287},"JDBC: Перший контакт із базою даних","/java/pr2/jdbc-fundamentals","04.java/pr2/10.jdbc-fundamentals",{"title":2289,"path":2290,"stem":2291},"Якість коду: Spotless, SpotBugs та SonarQube","/java/pr2/10a.code-quality","04.java/pr2/10a.code-quality",{"title":2293,"path":2294,"stem":2295},"Connection Pool: Патерн Object Pool для JDBC-з'єднань","/java/pr2/connection-pool","04.java/pr2/11.connection-pool",{"title":2297,"path":2298,"stem":2299},"Row Data Gateway: Об'єкт як обгортка рядка таблиці","/java/pr2/row-data-gateway","04.java/pr2/12.row-data-gateway",{"title":2301,"path":2302,"stem":2303},"Table Data Gateway: Фасад таблиці як архітектурний відступ","/java/pr2/table-data-gateway","04.java/pr2/13.table-data-gateway",{"title":2305,"path":2306,"stem":2307},"Repository + Data Mapper: Правильна шарова архітектура з JDBC","/java/pr2/repository-data-mapper","04.java/pr2/14.repository-data-mapper",{"title":2309,"path":2310,"stem":2311},"Identity Map: Кешування сутностей у рамках сесії","/java/pr2/identity-map","04.java/pr2/15.identity-map",{"title":2313,"path":2314,"stem":2315},"Unit of Work: Відстеження змін і координація JDBC-транзакцій","/java/pr2/unit-of-work","04.java/pr2/16.unit-of-work",{"title":2317,"path":2318,"stem":2319},"Strategy: Замінювані SQL-стратегії для підтримки різних СУБД","/java/pr2/strategy-sql","04.java/pr2/17.strategy-sql",{"title":2321,"path":2322,"stem":2323},"Proxy: Lazy Loading для One-To-Many колекцій","/java/pr2/proxy-lazy-loading","04.java/pr2/18.proxy-lazy-loading",{"title":2325,"path":2326,"stem":2327},"Generic Repository через Java Reflection: анотації та динамічний SQL","/java/pr2/generic-repository-reflection","04.java/pr2/19.generic-repository-reflection",{"title":2329,"path":2330,"stem":2331},"Specification Pattern: Композиція бізнес-правил для складних запитів","/java/pr2/specification-pattern","04.java/pr2/20.specification-pattern",{"title":2333,"path":2334,"stem":2335},"Розширені можливості Specification Pattern: підзапити, агрегації та гібридний підхід","/java/pr2/20a.advanced-specifications","04.java/pr2/20a.advanced-specifications",{"title":2337,"path":2338,"stem":2339},"Асинхронність у JDBC: Від блокуючих викликів до CompletableFuture","/java/pr2/asynchronous-jdbc","04.java/pr2/21.asynchronous-jdbc",{"title":2341,"path":2342,"stem":2343},"Інтеграційне тестування JDBC-репозиторіїв: Embedded H2 та патерн AAA","/java/pr2/integration-testing-h2","04.java/pr2/22.integration-testing-h2",{"title":2345,"path":2346,"stem":2347},"Testcontainers: Тестування з реальною PostgreSQL у Docker-контейнерах","/java/pr2/integration-testing-testcontainers","04.java/pr2/23.integration-testing-testcontainers",{"title":2349,"path":2350,"stem":2351},"Модуль \"Проектування реляційних баз даних\" для 04.java/pr2","/java/pr2/implementation_plan","04.java/pr2/implementation_plan",{"title":2353,"icon":2354,"path":2355,"stem":2356,"children":2357,"page":59},"Бази даних","i-lucide-database","/databases","06.databases",[2358,2388,2411,2448,2477,2495,2529,2541,2550],{"title":2359,"icon":2360,"path":2361,"stem":2362,"children":2363,"page":59},"Intro","i-lucide-play","/databases/intro","06.databases/01.intro",[2364,2368,2372,2376,2380,2384],{"title":2365,"path":2366,"stem":2367},"Введення в теорію баз даних","/databases/intro/introduction-to-databases","06.databases/01.intro/01.introduction-to-databases",{"title":2369,"path":2370,"stem":2371},"Реляційна модель даних","/databases/intro/relational-model-theory","06.databases/01.intro/02.relational-model-theory",{"title":2373,"path":2374,"stem":2375},"ER-моделювання","/databases/intro/er-modeling","06.databases/01.intro/03.er-modeling",{"title":2377,"path":2378,"stem":2379},"Логічне проектування БД","/databases/intro/logical-schema","06.databases/01.intro/04.logical-schema",{"title":2381,"path":2382,"stem":2383},"Класифікація таблиць","/databases/intro/table-classification","06.databases/01.intro/05.table-classification",{"title":2385,"path":2386,"stem":2387},"PlantUML для баз даних","/databases/intro/plantuml-diagrams","06.databases/01.intro/06.plantuml-diagrams",{"title":2389,"icon":2354,"path":2390,"stem":2391,"children":2392,"page":59},"MS SQL Server Start","/databases/ms-sql-server-start","06.databases/02.ms-sql-server-start",[2393,2397,2403,2407],{"title":2394,"path":2395,"stem":2396},"Типи даних у MS SQL Server","/databases/ms-sql-server-start/data-types","06.databases/02.ms-sql-server-start/01.data-types",{"title":2398,"path":2399,"stem":2400,"children":2401},"Індекси у MS SQL Server","/databases/ms-sql-server-start/sql-indexes","06.databases/02.ms-sql-server-start/02.sql-indexes",[2402],{"title":2398,"path":2399,"stem":2400},{"title":2404,"path":2405,"stem":2406},"Системні бази даних MS SQL Server","/databases/ms-sql-server-start/system-databases","06.databases/02.ms-sql-server-start/03.system-databases",{"title":2408,"path":2409,"stem":2410},"Огляд мови SQL та запитів","/databases/ms-sql-server-start/sql-queries-overview","06.databases/02.ms-sql-server-start/04.sql-queries-overview",{"title":2412,"icon":2354,"path":2413,"stem":2414,"children":2415,"page":59},"SQL","/databases/sql","06.databases/03.sql",[2416,2420,2424,2428,2432,2436,2440,2444],{"title":2417,"path":2418,"stem":2419},"Налаштування демонстраційної бази даних","/databases/sql/sample-database-setup","06.databases/03.sql/00.sample-database-setup",{"title":2421,"path":2422,"stem":2423},"DDL - Створення таблиць (CREATE TABLE)","/databases/sql/ddl-create-table","06.databases/03.sql/01.ddl-create-table",{"title":2425,"path":2426,"stem":2427},"DDL - Зміна та видалення таблиць (ALTER, DROP)","/databases/sql/ddl-alter-drop-table","06.databases/03.sql/02.ddl-alter-drop-table",{"title":2429,"path":2430,"stem":2431},"SELECT запити - Основи","/databases/sql/select-queries-fundamentals","06.databases/03.sql/03.select-queries-fundamentals",{"title":2433,"path":2434,"stem":2435},"SELECT запити - Розширені можливості","/databases/sql/select-queries-advanced","06.databases/03.sql/04.select-queries-advanced",{"title":2437,"path":2438,"stem":2439},"INSERT запити - Додавання даних","/databases/sql/insert-queries","06.databases/03.sql/05.insert-queries",{"title":2441,"path":2442,"stem":2443},"UPDATE та DELETE запити","/databases/sql/update-delete-queries","06.databases/03.sql/06.update-delete-queries",{"title":2445,"path":2446,"stem":2447},"Транзакції в SQL","/databases/sql/transactions","06.databases/03.sql/07.transactions",{"title":2449,"icon":2354,"path":2450,"stem":2451,"children":2452,"page":59},"Multi Table Databases","/databases/multi-table-databases","06.databases/04.multi-table-databases",[2453,2457,2461,2465,2469,2473],{"title":2454,"path":2455,"stem":2456},"Зв'язки та нормалізація БД","/databases/multi-table-databases/relationships-and-normalization","06.databases/04.multi-table-databases/00.relationships-and-normalization",{"title":2458,"path":2459,"stem":2460},"INNER JOIN - З'єднання таблиць","/databases/multi-table-databases/inner-join","06.databases/04.multi-table-databases/01.inner-join",{"title":2462,"path":2463,"stem":2464},"OUTER JOINs - LEFT, RIGHT, FULL","/databases/multi-table-databases/outer-joins","06.databases/04.multi-table-databases/02.outer-joins",{"title":2466,"path":2467,"stem":2468},"CROSS та SELF JOINs","/databases/multi-table-databases/cross-self-joins","06.databases/04.multi-table-databases/03.cross-self-joins",{"title":2470,"path":2471,"stem":2472},"Підзапити (Subqueries)","/databases/multi-table-databases/subqueries","06.databases/04.multi-table-databases/04.subqueries",{"title":2474,"path":2475,"stem":2476},"Агрегації з JOIN","/databases/multi-table-databases/aggregations-with-joins","06.databases/04.multi-table-databases/05.aggregations-with-joins",{"title":2478,"icon":2479,"path":2480,"stem":2481,"children":2482,"page":59},"Aggregate Functions","i-lucide-calculator","/databases/aggregate-functions","06.databases/05.aggregate-functions",[2483,2487,2491],{"title":2484,"path":2485,"stem":2486},"Функції агрегування в MS SQL Server","/databases/aggregate-functions/introduction-aggregate-functions","06.databases/05.aggregate-functions/01.introduction-aggregate-functions",{"title":2488,"path":2489,"stem":2490},"Групування даних в MS SQL Server","/databases/aggregate-functions/grouping-data","06.databases/05.aggregate-functions/02.grouping-data",{"title":2492,"path":2493,"stem":2494},"Підзапити з агрегатними функціями","/databases/aggregate-functions/subqueries-aggregates","06.databases/05.aggregate-functions/03.subqueries-aggregates",{"title":2496,"icon":2497,"path":2498,"stem":2499,"children":2500,"page":59},"Тригери та зберігаємі процедури","i-lucide-database-zap","/databases/triggers-stored-procedures","06.databases/07.triggers-stored-procedures",[2501,2505,2509,2513,2517,2521,2525],{"title":2502,"path":2503,"stem":2504},"DML-тригери","/databases/triggers-stored-procedures/dml-triggers","06.databases/07.triggers-stored-procedures/01.dml-triggers",{"title":2506,"path":2507,"stem":2508},"DDL-тригери","/databases/triggers-stored-procedures/ddl-triggers","06.databases/07.triggers-stored-procedures/02.ddl-triggers",{"title":2510,"path":2511,"stem":2512},"Transact-SQL розширення","/databases/triggers-stored-procedures/transact-sql-extensions","06.databases/07.triggers-stored-procedures/03.transact-sql-extensions",{"title":2514,"path":2515,"stem":2516},"Транзакції","/databases/triggers-stored-procedures/transactions","06.databases/07.triggers-stored-procedures/04.transactions",{"title":2518,"path":2519,"stem":2520},"Зберігаємі процедури","/databases/triggers-stored-procedures/stored-procedures","06.databases/07.triggers-stored-procedures/05.stored-procedures",{"title":2522,"path":2523,"stem":2524},"Користувацькі функції","/databases/triggers-stored-procedures/user-defined-functions","06.databases/07.triggers-stored-procedures/06.user-defined-functions",{"title":2526,"path":2527,"stem":2528},"Безпека баз даних","/databases/triggers-stored-procedures/security","06.databases/07.triggers-stored-procedures/08.security",{"title":2526,"icon":793,"path":2530,"stem":2531,"children":2532,"page":59},"/databases/security","06.databases/08.security",[2533,2537],{"title":2534,"path":2535,"stem":2536},"Вступ до безпеки баз даних","/databases/security/introduction","06.databases/08.security/01.introduction",{"title":2538,"path":2539,"stem":2540},"Системні представлення та метадані","/databases/security/system-views","06.databases/08.security/02.system-views",{"title":2542,"icon":2543,"path":2544,"stem":2545,"children":2546,"page":59},"Резервне копіювання та відновлення","i-lucide-database-backup","/databases/backup-recovery","06.databases/09.backup-recovery",[2547],{"title":2542,"path":2548,"stem":2549},"/databases/backup-recovery/backup-restore","06.databases/09.backup-recovery/01.backup-restore",{"title":2551,"icon":2552,"path":2553,"stem":2554,"children":2555,"page":59},"Повнотекстовий пошук","i-lucide-search","/databases/full-text-search","06.databases/10.full-text-search",[2556],{"title":2551,"path":2557,"stem":2558},"/databases/full-text-search/full-text-search","06.databases/10.full-text-search/01.full-text-search",{"title":2560,"icon":2561,"path":2562,"stem":2563,"children":2564,"page":59},"Tools","i-lucide-wrench","/tools","07.tools",[2565],{"title":2566,"icon":2567,"path":2568,"stem":2569,"children":2570},"Docker","i-simple-icons-docker","/tools/docker","07.tools/01.docker/index",[2571,2573,2577,2581,2585,2589,2593,2597,2601,2605,2609,2613,2617,2621,2625,2629,2633,2637],{"title":2572,"path":2568,"stem":2569},"Docker: від нуля до production",{"title":2574,"path":2575,"stem":2576},"Контейнеризація — від проблеми до рішення","/tools/docker/containerization-concept","07.tools/01.docker/01.containerization-concept",{"title":2578,"path":2579,"stem":2580},"Docker — що це і навіщо?","/tools/docker/docker-what-and-why","07.tools/01.docker/02.docker-what-and-why",{"title":2582,"path":2583,"stem":2584},"Архітектура Docker Engine","/tools/docker/docker-architecture","07.tools/01.docker/03.docker-architecture",{"title":2586,"path":2587,"stem":2588},"Встановлення Docker","/tools/docker/installation","07.tools/01.docker/04.installation",{"title":2590,"path":2591,"stem":2592},"Перший контейнер — docker run","/tools/docker/first-container","07.tools/01.docker/05.first-container",{"title":2594,"path":2595,"stem":2596},"Життєвий цикл контейнера","/tools/docker/container-lifecycle","07.tools/01.docker/06.container-lifecycle",{"title":2598,"path":2599,"stem":2600},"Docker Images — фундаментальні концепції","/tools/docker/docker-images-fundamentals","07.tools/01.docker/07.docker-images-fundamentals",{"title":2602,"path":2603,"stem":2604},"Dockerfile — основи","/tools/docker/dockerfile-basics","07.tools/01.docker/08.dockerfile-basics",{"title":2606,"path":2607,"stem":2608},"Dockerfile — просунуті техніки","/tools/docker/dockerfile-advanced","07.tools/01.docker/09.dockerfile-advanced",{"title":2610,"path":2611,"stem":2612},"Build Context та кешування шарів","/tools/docker/build-context-and-cache","07.tools/01.docker/10.build-context-and-cache",{"title":2614,"path":2615,"stem":2616},"Реєстри Docker-образів","/tools/docker/image-registries","07.tools/01.docker/11.image-registries",{"title":2618,"path":2619,"stem":2620},"Контейнеризація .NET додатків","/tools/docker/dotnet-containerization","07.tools/01.docker/12.dotnet-containerization",{"title":2622,"path":2623,"stem":2624},"Томи та збереження даних","/tools/docker/volumes-and-data","07.tools/01.docker/13.volumes-and-data",{"title":2626,"path":2627,"stem":2628},"Основи мережі в Docker","/tools/docker/networking-basics","07.tools/01.docker/14.networking-basics",{"title":2630,"path":2631,"stem":2632},"Змінні оточення та конфігурація","/tools/docker/environment-and-configuration","07.tools/01.docker/15.environment-and-configuration",{"title":2634,"path":2635,"stem":2636},"Docker Compose — оркестрація контейнерів","/tools/docker/docker-compose-basics","07.tools/01.docker/16.docker-compose-basics",{"title":2638,"path":2639,"stem":2640},"Docker Compose — Multi-Service застосунки","/tools/docker/compose-multi-service","07.tools/01.docker/17.compose-multi-service",{"title":2642,"icon":2643,"path":2644,"stem":2645,"children":2646,"page":59},"Software Engineering","i-lucide-code-2","/software-engineering","09.software-engineering",[2647,2651,2655,2659,2663,2667,2671,2675,2679,2683,2687],{"title":2648,"path":2649,"stem":2650},"1. Аналіз предметної області. Експертні знання та складність","/software-engineering/intro.subdomains","09.software-engineering/01.intro.subdomains",{"title":2652,"path":2653,"stem":2654},"2. Обмежені контексти. Інтеграція обмежених контекстів","/software-engineering/integrating-limited-contexts","09.software-engineering/02.integrating-limited-contexts",{"title":2656,"path":2657,"stem":2658},"3. Реалізація простої бізнес-логіки","/software-engineering/simple","09.software-engineering/03.simple",{"title":2660,"path":2661,"stem":2662},"4. Опрацювання складної бізнес-логіки","/software-engineering/complex-business-logic","09.software-engineering/04.complex-business-logic",{"title":2664,"path":2665,"stem":2666},"5. Моделювання фактора часу. Подієво-орієнтована архітектура.","/software-engineering/modelling-the-time-factor","09.software-engineering/05.modelling-the-time-factor",{"title":2668,"path":2669,"stem":2670},"6. Архітектурні патерни","/software-engineering/architectural-patterns","09.software-engineering/06.architectural-patterns",{"title":2672,"path":2673,"stem":2674},"Паттерни взаємодії","/software-engineering/patterns-of-interaction","09.software-engineering/07.patterns-of-interaction",{"title":2676,"path":2677,"stem":2678},"Евристика проєктування","/software-engineering/design-heuristics","09.software-engineering/08.design-heuristics",{"title":2680,"path":2681,"stem":2682},"Еволюція проєктних рішень","/software-engineering/evolution-of-design-solutions","09.software-engineering/09.evolution-of-design-solutions",{"title":2684,"path":2685,"stem":2686},"EventStorming","/software-engineering/eventstorming","09.software-engineering/10.eventstorming",{"title":2688,"path":2689,"stem":2690},"DDD на практиці","/software-engineering/ddd-in-practice","09.software-engineering/11.ddd-in-practice",{"title":2692,"icon":943,"path":2693,"stem":2694,"children":2695,"page":59},"DDD","/ddd","10.ddd",[2696,2700,2704,2708,2712,2716,2720,2724,2728,2732,2736,2740,2744],{"title":2697,"path":2698,"stem":2699},"Аналіз предметної області","/ddd/domain-analysis","10.ddd/01.domain-analysis",{"title":2701,"path":2702,"stem":2703},"Експертні знання про предметну область","/ddd/domain-expert-knowledge","10.ddd/02.domain-expert-knowledge",{"title":2705,"path":2706,"stem":2707},"Як осмислити складність предметної області","/ddd/managing-domain-complexity","10.ddd/03.managing-domain-complexity",{"title":2709,"path":2710,"stem":2711},"Інтеграція обмежених контекстів","/ddd/bounded-context-integration","10.ddd/04.bounded-context-integration",{"title":2713,"path":2714,"stem":2715},"Реалізація простої бізнес-логіки","/ddd/simple-business-logic","10.ddd/05.simple-business-logic",{"title":2717,"path":2718,"stem":2719},"Обробка складної бізнес-логіки","/ddd/complex-business-logic","10.ddd/06.complex-business-logic",{"title":2721,"path":2722,"stem":2723},"Моделювання фактора часу","/ddd/time-modeling","10.ddd/07.time-modeling",{"title":2725,"path":2726,"stem":2727},"Глава 8. Архітектурні Патерни","/ddd/architectural-patterns","10.ddd/08.architectural-patterns",{"title":2729,"path":2730,"stem":2731},"Глава 9. Патерни Взаємодії","/ddd/interaction-patterns","10.ddd/09.interaction-patterns",{"title":2733,"path":2734,"stem":2735},"Глава 10. Проектні Евристики","/ddd/design-heuristics","10.ddd/10.design-heuristics",{"title":2737,"path":2738,"stem":2739},"Глава 11. Еволюція Проектних Рішень","/ddd/evolution-of-design-decisions","10.ddd/11.evolution-of-design-decisions",{"title":2741,"path":2742,"stem":2743},"Глава 12. EventStorming","/ddd/event-storming","10.ddd/12.event-storming",{"title":2745,"path":2746,"stem":2747},"Глава 13. DDD на Практиці","/ddd/ddd-in-practice","10.ddd/13.ddd-in-practice",{"title":2749,"icon":2750,"path":2751,"stem":2752,"children":2753,"page":59},"Media Streaming","i-lucide-video","/media-streaming","11.media-streaming",[2754,2758,2762,2766,2770,2774,2778],{"title":2755,"path":2756,"stem":2757},"01. Магія Стрімінгу: Що відбувається, коли ви натискаєте \"Play\"","/media-streaming/introduction","11.media-streaming/01.introduction",{"title":2759,"path":2760,"stem":2761},"02. Анатомія Медіа: Кодеки, Контейнери та Стиснення","/media-streaming/audio-video-anatomy","11.media-streaming/02.audio-video-anatomy",{"title":2763,"path":2764,"stem":2765},"03. The Gym: FFmpeg Deep Dive","/media-streaming/ffmpeg-gym","11.media-streaming/03.ffmpeg-gym",{"title":2767,"path":2768,"stem":2769},"04. HLS Protocol: HTTP Live Streaming у Деталях","/media-streaming/hls-protocol","11.media-streaming/04.hls-protocol",{"title":2771,"path":2772,"stem":2773},"05. DASH Protocol: Відкритий Стандарт","/media-streaming/dash-protocol","11.media-streaming/05.dash-protocol",{"title":2775,"path":2776,"stem":2777},"06. Масштабування: CDN та Adaptive Bitrate","/media-streaming/cdn-and-adaptive-bitrate","11.media-streaming/06.cdn-and-adaptive-bitrate",{"title":2779,"path":2780,"stem":2781},"07. Війна із Затримкою (Latency)","/media-streaming/realtime-latency","11.media-streaming/07.realtime-latency",{"title":2783,"icon":2784,"path":2785,"stem":2786,"children":2787,"page":59},"HTML & CSS","i-devicon-html5","/html-css","12.html-css",[2788,2792,2796,2800,2804,2808,2812,2816,2820,2824,2828,2832,2836,2840,2844,2848,2852,2856,2860,2864,2868,2872,2876,2880,2884,2888,2892,2896,2900,2904],{"title":2789,"path":2790,"stem":2791},"Вступ до HTML. Структура документа","/html-css/intro-html-structure","12.html-css/01.intro-html-structure",{"title":2793,"path":2794,"stem":2795},"Форматування тексту в HTML","/html-css/html-text-formatting","12.html-css/02.html-text-formatting",{"title":2797,"path":2798,"stem":2799},"Посилання та зображення в HTML","/html-css/html-links-images","12.html-css/03.html-links-images",{"title":2801,"path":2802,"stem":2803},"Списки та таблиці в HTML","/html-css/html-lists-tables","12.html-css/04.html-lists-tables",{"title":2805,"path":2806,"stem":2807},"Форми в HTML","/html-css/html-forms","12.html-css/05.html-forms",{"title":2809,"path":2810,"stem":2811},"Семантичні елементи HTML5","/html-css/html-semantic-elements","12.html-css/06.html-semantic-elements",{"title":2813,"path":2814,"stem":2815},"Мультимедіа та розширені елементи HTML","/html-css/html-multimedia-advanced","12.html-css/07.html-multimedia-advanced",{"title":2817,"path":2818,"stem":2819},"Мікророзмітка та SEO в HTML","/html-css/html-microdata-seo","12.html-css/08.html-microdata-seo",{"title":2821,"path":2822,"stem":2823},"Вступ до CSS. Селектори та специфічність","/html-css/css-intro-selectors","12.html-css/09.css-intro-selectors",{"title":2825,"path":2826,"stem":2827},"Блокова модель CSS. Відступи. Box Sizing","/html-css/css-box-model","12.html-css/10.css-box-model",{"title":2829,"path":2830,"stem":2831},"Розміри у CSS: повний довідник одиниць і ключових слів","/html-css/10a.css-sizing","12.html-css/10a.css-sizing",{"title":2833,"path":2834,"stem":2835},"Типографіка в CSS. Шрифти та текст","/html-css/css-typography","12.html-css/11.css-typography",{"title":2837,"path":2838,"stem":2839},"Кольори та фони в CSS","/html-css/css-colors-backgrounds","12.html-css/12.css-colors-backgrounds",{"title":2841,"path":2842,"stem":2843},"Тіні та фільтри в CSS","/html-css/12b.css-shadows-filters","12.html-css/12b.css-shadows-filters",{"title":2845,"path":2846,"stem":2847},"CSS Flexbox: Фундамент гнучких макетів","/html-css/css-flexbox-fundamentals","12.html-css/13.css-flexbox-fundamentals",{"title":2849,"path":2850,"stem":2851},"CSS Flexbox: Вирівнювання та Позиціонування","/html-css/css-flexbox-alignment-sizing-and-patterns","12.html-css/14.css-flexbox-alignment-sizing-and-patterns",{"title":2853,"path":2854,"stem":2855},"CSS Grid. Двовимірний макет. Частина 1","/html-css/css-layout-grid","12.html-css/15.css-layout-grid",{"title":2857,"path":2858,"stem":2859},"CSS Grid. Двовимірний макет. Частина 2","/html-css/css-layout-grid-advanced","12.html-css/16.css-layout-grid-advanced",{"title":2861,"path":2862,"stem":2863},"Позиціонування в CSS. Z-index. Stacking Context","/html-css/css-positioning","12.html-css/17.css-positioning",{"title":2865,"path":2866,"stem":2867},"CSS Анімації та Переходи","/html-css/css-animations-transitions","12.html-css/18.css-animations-transitions",{"title":2869,"path":2870,"stem":2871},"Адаптивний дизайн. Media Queries. Частина 1","/html-css/css-responsive-media-queries","12.html-css/19.css-responsive-media-queries",{"title":2873,"path":2874,"stem":2875},"Адаптивний дизайн. Частина 2: clamp(), Container Queries, @layer","/html-css/css-responsive-advanced","12.html-css/20.css-responsive-advanced",{"title":2877,"path":2878,"stem":2879},"CSS Custom Properties. Методології. Сучасний CSS","/html-css/css-variables-methodologies","12.html-css/21.css-variables-methodologies",{"title":2881,"path":2882,"stem":2883},"Сучасний CSS 2023–2025: Нові можливості","/html-css/css-modern-features","12.html-css/22.css-modern-features",{"title":2885,"path":2886,"stem":2887},"CSS Nesting, @layer, @scope та @property: нативний препроцесор","/html-css/22a.css-nesting-modern-syntax","12.html-css/22a.css-nesting-modern-syntax",{"title":2889,"path":2890,"stem":2891},"CSS для форм та інтерактивних станів","/html-css/css-forms-interactive-states","12.html-css/23.css-forms-interactive-states",{"title":2893,"path":2894,"stem":2895},"Доступність у CSS (CSS Accessibility)","/html-css/css-accessibility","12.html-css/24.css-accessibility",{"title":2897,"path":2898,"stem":2899},"CSS-функції та сучасні sizing primitives","/html-css/css-functions-sizing","12.html-css/25.css-functions-sizing",{"title":2901,"path":2902,"stem":2903},"Rendering Pipeline і CSS Performance","/html-css/css-rendering-performance","12.html-css/26.css-rendering-performance",{"title":2905,"path":2906,"stem":2907},"CSS Best Practices: типові ситуації та правильні рішення","/html-css/css-best-practices","12.html-css/27.css-best-practices",{"title":2909,"path":2910,"stem":2911,"children":2912,"page":59},"Tailwind","/tailwind","21.tailwind",[2913,2917,2921,2925,2929,2933,2937,2941],{"title":2914,"path":2915,"stem":2916},"Що таке Tailwind CSS і навіщо він потрібен","/tailwind/tailwind-intro-philosophy","21.tailwind/01.tailwind-intro-philosophy",{"title":2918,"path":2919,"stem":2920},"Встановлення та налаштування Tailwind CSS v4","/tailwind/tailwind-installation-setup","21.tailwind/02.tailwind-installation-setup",{"title":2922,"path":2923,"stem":2924},"Utility-класи: основи та система Tailwind","/tailwind/tailwind-utility-classes-core","21.tailwind/03.tailwind-utility-classes-core",{"title":2926,"path":2927,"stem":2928},"Layout: Flexbox та Grid через Tailwind","/tailwind/tailwind-flexbox-grid","21.tailwind/04.tailwind-flexbox-grid",{"title":2930,"path":2931,"stem":2932},"Кастомізація теми через @theme у Tailwind v4","/tailwind/tailwind-theme-customization","21.tailwind/05.tailwind-theme-customization",{"title":2934,"path":2935,"stem":2936},"Варіанти: hover, focus, responsive, dark mode та нові v4","/tailwind/tailwind-variants-states","21.tailwind/06.tailwind-variants-states",{"title":2938,"path":2939,"stem":2940},"Типографіка та система кольорів у Tailwind v4","/tailwind/tailwind-typography-colors","21.tailwind/07.tailwind-typography-colors",{"title":2942,"path":2943,"stem":2944},"Компоненти та повторюваність: @apply, @utility та патерни","/tailwind/tailwind-components-patterns","21.tailwind/08.tailwind-components-patterns",{"title":2946,"path":2947,"stem":2948},"Showcase Компонентів kostyl.dev","/test-new-components","98.test-new-components",{"id":2950,"title":2329,"body":2951,"description":15198,"extension":15199,"links":15200,"meta":15201,"navigation":3350,"path":2330,"seo":15202,"stem":2331,"__hash__":15203},"docs/04.java/pr2/20.specification-pattern.md",{"type":2952,"value":2953,"toc":15162},"minimark",[2954,2958,2963,2989,3102,3105,3124,3127,3224,3232,3256,3262,3269,3277,3280,3284,3289,3292,3306,3327,3647,3669,3673,3676,3975,3978,4365,4376,4378,4382,4385,4504,4511,4520,4522,4526,4537,5226,5231,5277,5279,5283,5290,5293,5725,5728,6114,6117,6396,6405,6537,6545,6547,6551,6558,6830,6837,7559,7564,7603,7605,7609,7622,7766,7781,7785,8334,8338,8858,8862,9157,9161,9465,9467,9471,9486,11675,11679,11746,11766,11768,11772,11777,13144,13147,13277,13282,13345,13347,13351,13357,13596,13607,13609,13613,13616,14150,14239,14241,14245,14248,14252,14257,14296,14317,14321,14324,14400,14403,14407,14410,14452,14459,14463,14466,14492,14495,14499,14502,14598,14604,14619,14621,14625,14628,14731,14738,14740,14744,14751,14754,14773,14779,14789,14799,14807,14810,14831,14834,14841,14856,14858,14862,15129,15131,15135,15158],[2955,2956,2329],"h1",{"id":2957},"specification-pattern-композиція-бізнес-правил-для-складних-запитів",[2959,2960,2962],"h2",{"id":2961},"вступ-коли-методів-пошуку-стає-забагато","Вступ: Коли методів пошуку стає забагато",[2964,2965,2966,2967,2971,2972,2975,2976,2975,2979,2975,2982,2975,2985,2988],"p",{},"У попередніх статтях ми побудували архітектуру Repository + Data Mapper, що успішно відокремила доменну модель від SQL-логіки. Репозиторій ",[2968,2969,2970],"code",{},"JdbcAuthorRepository"," містить стандартні CRUD-операції (",[2968,2973,2974],{},"findById",", ",[2968,2977,2978],{},"findAll",[2968,2980,2981],{},"save",[2968,2983,2984],{},"update",[2968,2986,2987],{},"deleteById",") та кілька специфічних методів пошуку:",[2990,2991,2996],"pre",{"className":2992,"code":2993,"language":2994,"meta":2995,"style":2995},"language-java shiki shiki-themes light-plus dark-plus dark-plus","public interface AuthorRepository extends Repository\u003CAuthor, UUID> {\n    List\u003CAuthor> findByLastName(String lastNamePart);\n    Optional\u003CAuthor> findByFullName(String lastName, String firstName);\n}\n","java","",[2968,2997,2998,3035,3065,3096],{"__ignoreMap":2995},[2999,3000,3003,3007,3010,3014,3017,3020,3024,3027,3029,3032],"span",{"class":3001,"line":3002},"line",1,[2999,3004,3006],{"class":3005},"su1O8","public",[2999,3008,3009],{"class":3005}," interface",[2999,3011,3013],{"class":3012},"sN1BT"," AuthorRepository",[2999,3015,3016],{"class":3005}," extends",[2999,3018,3019],{"class":3012}," Repository",[2999,3021,3023],{"class":3022},"sHH4Y","\u003C",[2999,3025,3026],{"class":3012},"Author",[2999,3028,2975],{"class":3022},[2999,3030,3031],{"class":3012},"UUID",[2999,3033,3034],{"class":3022},"> {\n",[2999,3036,3038,3041,3043,3045,3048,3052,3055,3058,3062],{"class":3001,"line":3037},2,[2999,3039,3040],{"class":3012},"    List",[2999,3042,3023],{"class":3022},[2999,3044,3026],{"class":3012},[2999,3046,3047],{"class":3022},"> ",[2999,3049,3051],{"class":3050},"s8Opu","findByLastName",[2999,3053,3054],{"class":3022},"(",[2999,3056,3057],{"class":3012},"String",[2999,3059,3061],{"class":3060},"siwwj"," lastNamePart",[2999,3063,3064],{"class":3022},");\n",[2999,3066,3068,3071,3073,3075,3077,3080,3082,3084,3087,3089,3091,3094],{"class":3001,"line":3067},3,[2999,3069,3070],{"class":3012},"    Optional",[2999,3072,3023],{"class":3022},[2999,3074,3026],{"class":3012},[2999,3076,3047],{"class":3022},[2999,3078,3079],{"class":3050},"findByFullName",[2999,3081,3054],{"class":3022},[2999,3083,3057],{"class":3012},[2999,3085,3086],{"class":3060}," lastName",[2999,3088,2975],{"class":3022},[2999,3090,3057],{"class":3012},[2999,3092,3093],{"class":3060}," firstName",[2999,3095,3064],{"class":3022},[2999,3097,3099],{"class":3001,"line":3098},4,[2999,3100,3101],{"class":3022},"}\n",[2964,3103,3104],{},"Це працює для простих сценаріїв. Але що станеться, коли бізнес-вимоги ускладнюються? Розглянемо реальні запити, що виникають у системі управління аудіоплатформою:",[3106,3107,3108,3114,3119],"card-group",{},[3109,3110,3113],"card",{"title":3111,"icon":3112},"Запит 1: Фільтрація авторів","i-heroicons-funnel","Знайти всіх авторів, чиє прізвище починається з «Шевч», які мають біографію, і у яких є хоча б одна опублікована аудіокнига.",[3109,3115,3118],{"title":3116,"icon":3117},"Запит 2: Пошук аудіокниг","i-heroicons-magnifying-glass","Знайти аудіокниги жанру «Поезія» або «Проза», опубліковані після 2020 року, з ціною від 100 до 500 грн, мовою «українська».",[3109,3120,3123],{"title":3121,"icon":3122},"Запит 3: Динамічний фільтр","i-heroicons-adjustments-horizontal","Користувач обирає фільтри у веб-інтерфейсі: жанр (опціонально), діапазон років (опціонально), мова (опціонально), максимальна ціна (опціонально). Кількість комбінацій — 2⁴ = 16 варіантів.",[2964,3125,3126],{},"Наївний підхід — створити окремий метод для кожної комбінації:",[2990,3128,3130],{"className":2992,"code":3129,"language":2994,"meta":2995,"style":2995},"List\u003CAudiobook> findByGenre(String genre);\nList\u003CAudiobook> findByGenreAndYear(String genre, Integer year);\nList\u003CAudiobook> findByGenreAndYearRange(String genre, Integer minYear, Integer maxYear);\nList\u003CAudiobook> findByGenreAndMinDuration(String genre, Integer minDuration);\n// ... ще десятки методів для всіх комбінацій\n",[2968,3131,3132,3150,3172,3197,3217],{"__ignoreMap":2995},[2999,3133,3134,3137,3140,3143,3145,3147],{"class":3001,"line":3002},[2999,3135,3136],{"class":3012},"List",[2999,3138,3139],{"class":3022},"\u003CAudiobook> ",[2999,3141,3142],{"class":3050},"findByGenre",[2999,3144,3054],{"class":3022},[2999,3146,3057],{"class":3012},[2999,3148,3149],{"class":3022}," genre);\n",[2999,3151,3152,3154,3156,3159,3161,3163,3166,3169],{"class":3001,"line":3037},[2999,3153,3136],{"class":3012},[2999,3155,3139],{"class":3022},[2999,3157,3158],{"class":3050},"findByGenreAndYear",[2999,3160,3054],{"class":3022},[2999,3162,3057],{"class":3012},[2999,3164,3165],{"class":3022}," genre, ",[2999,3167,3168],{"class":3012},"Integer",[2999,3170,3171],{"class":3022}," year);\n",[2999,3173,3174,3176,3178,3181,3183,3185,3187,3189,3192,3194],{"class":3001,"line":3067},[2999,3175,3136],{"class":3012},[2999,3177,3139],{"class":3022},[2999,3179,3180],{"class":3050},"findByGenreAndYearRange",[2999,3182,3054],{"class":3022},[2999,3184,3057],{"class":3012},[2999,3186,3165],{"class":3022},[2999,3188,3168],{"class":3012},[2999,3190,3191],{"class":3022}," minYear, ",[2999,3193,3168],{"class":3012},[2999,3195,3196],{"class":3022}," maxYear);\n",[2999,3198,3199,3201,3203,3206,3208,3210,3212,3214],{"class":3001,"line":3098},[2999,3200,3136],{"class":3012},[2999,3202,3139],{"class":3022},[2999,3204,3205],{"class":3050},"findByGenreAndMinDuration",[2999,3207,3054],{"class":3022},[2999,3209,3057],{"class":3012},[2999,3211,3165],{"class":3022},[2999,3213,3168],{"class":3012},[2999,3215,3216],{"class":3022}," minDuration);\n",[2999,3218,3220],{"class":3001,"line":3219},5,[2999,3221,3223],{"class":3222},"spJ8K","// ... ще десятки методів для всіх комбінацій\n",[2964,3225,3226,3227,3231],{},"Це призводить до ",[3228,3229,3230],"strong",{},"комбінаторного вибуху",": кожна нова вимога множить кількість методів. Якщо у системі 5 критеріїв пошуку, кількість можливих комбінацій — 2⁵ = 32. Для 10 критеріїв — 1024 методи. Репозиторій перетворюється на нечитабельний монстр із сотнями рядків однотипного коду.",[2964,3233,3234,3237,3238,3241,3242,2975,3245,2975,3248,3251,3252,3255],{},[3228,3235,3236],{},"Друга проблема — дублювання SQL-логіки."," Умова ",[2968,3239,3240],{},"WHERE price \u003C= ?"," повторюється у ",[2968,3243,3244],{},"findByMaxPrice()",[2968,3246,3247],{},"findByGenreAndMaxPrice()",[2968,3249,3250],{},"findByYearAndMaxPrice()"," тощо. Зміна логіки (наприклад, додавання перевірки ",[2968,3253,3254],{},"price > 0",") вимагає правки у десятках місць.",[2964,3257,3258,3261],{},[3228,3259,3260],{},"Третя проблема — відсутність повторного використання бізнес-правил."," Умова «аудіокнига доступна для покупки» може означати: ціна встановлена, мова підтримується платформою, жанр не заборонений у регіоні користувача. Це бізнес-правило, що має бути виражене один раз і використане скрізь — у пошуку, валідації, звітах.",[2964,3263,3264,3265,3268],{},"Саме для вирішення цих проблем Ерік Еванс у книзі «Domain-Driven Design» (2003) описав патерн ",[3228,3266,3267],{},"Specification"," — спосіб інкапсуляції бізнес-правил у композиційні об'єкти.",[3270,3271,3272,3273,3276],"note",{},"Specification Pattern є одним із ключових тактичних патернів Domain-Driven Design. На відміну від Repository (що є інфраструктурним патерном), Specification належить до ",[3228,3274,3275],{},"доменного шару"," — він виражає бізнес-логіку мовою предметної області.",[3278,3279],"hr",{},[2959,3281,3283],{"id":3282},"концепція-specification-pattern-за-еріком-евансом","Концепція: Specification Pattern за Еріком Евансом",[3285,3286,3288],"h3",{"id":3287},"визначення","Визначення",[2964,3290,3291],{},"У книзі «Domain-Driven Design: Tackling Complexity in the Heart of Software» (2003) Ерік Еванс визначає Specification так:",[3293,3294,3295,3301],"blockquote",{},[2964,3296,3297],{},[3298,3299,3300],"em",{},"«A Specification is a predicate that determines if an object satisfies certain criteria. It can be used to select objects from a collection, validate objects, or specify what kind of object should be created.»",[2964,3302,3303],{},[3298,3304,3305],{},"«Специфікація — це предикат, що визначає, чи задовольняє об'єкт певним критеріям. Вона може використовуватися для вибору об'єктів з колекції, валідації об'єктів або специфікації того, який об'єкт має бути створений.»",[2964,3307,3308,3309,3312,3313,3316,3317,2975,3320,2975,3323,3326],{},"Ключова ідея: ",[3228,3310,3311],{},"бізнес-правило інкапсульоване в окремий об'єкт",", що має метод ",[2968,3314,3315],{},"isSatisfiedBy(T candidate)",". Цей об'єкт можна комбінувати з іншими через логічні оператори (",[2968,3318,3319],{},"AND",[2968,3321,3322],{},"OR",[2968,3324,3325],{},"NOT","), створюючи складні умови з простих будівельних блоків.",[3328,3329,3330],"mermaid",{},[2990,3331,3334],{"className":3332,"code":3333,"language":3328,"meta":2995,"style":2995},"language-mermaid shiki shiki-themes light-plus dark-plus dark-plus","classDiagram\n    direction TB\n\n    class Specification~T~ {\n        \u003C\u003Cinterface>>\n        +isSatisfiedBy(T candidate) boolean\n        +and(Specification~T~ other) Specification~T~\n        +or(Specification~T~ other) Specification~T~\n        +not() Specification~T~\n    }\n\n    class PriceRangeSpec {\n        -BigDecimal minPrice\n        -BigDecimal maxPrice\n        +isSatisfiedBy(Audiobook book) boolean\n    }\n\n    class GenreSpec {\n        -String genreName\n        +isSatisfiedBy(Audiobook book) boolean\n    }\n\n    class LanguageSpec {\n        -String language\n        +isSatisfiedBy(Audiobook book) boolean\n    }\n\n    class AndSpecification~T~ {\n        -Specification~T~ left\n        -Specification~T~ right\n        +isSatisfiedBy(T candidate) boolean\n    }\n\n    class OrSpecification~T~ {\n        -Specification~T~ left\n        -Specification~T~ right\n        +isSatisfiedBy(T candidate) boolean\n    }\n\n    class NotSpecification~T~ {\n        -Specification~T~ spec\n        +isSatisfiedBy(T candidate) boolean\n    }\n\n    Specification~T~ \u003C|.. PriceRangeSpec\n    Specification~T~ \u003C|.. GenreSpec\n    Specification~T~ \u003C|.. LanguageSpec\n    Specification~T~ \u003C|.. AndSpecification~T~\n    Specification~T~ \u003C|.. OrSpecification~T~\n    Specification~T~ \u003C|.. NotSpecification~T~\n\n    AndSpecification~T~ o-- Specification~T~ : left\n    AndSpecification~T~ o-- Specification~T~ : right\n    OrSpecification~T~ o-- Specification~T~ : left\n    OrSpecification~T~ o-- Specification~T~ : right\n    NotSpecification~T~ o-- Specification~T~\n",[2968,3335,3336,3341,3346,3352,3357,3362,3368,3374,3380,3386,3392,3397,3403,3409,3415,3421,3426,3431,3437,3443,3448,3453,3458,3464,3470,3475,3480,3485,3491,3497,3503,3508,3513,3518,3524,3529,3534,3539,3544,3549,3555,3561,3566,3571,3576,3582,3588,3594,3600,3606,3612,3617,3623,3629,3635,3641],{"__ignoreMap":2995},[2999,3337,3338],{"class":3001,"line":3002},[2999,3339,3340],{},"classDiagram\n",[2999,3342,3343],{"class":3001,"line":3037},[2999,3344,3345],{},"    direction TB\n",[2999,3347,3348],{"class":3001,"line":3067},[2999,3349,3351],{"emptyLinePlaceholder":3350},true,"\n",[2999,3353,3354],{"class":3001,"line":3098},[2999,3355,3356],{},"    class Specification~T~ {\n",[2999,3358,3359],{"class":3001,"line":3219},[2999,3360,3361],{},"        \u003C\u003Cinterface>>\n",[2999,3363,3365],{"class":3001,"line":3364},6,[2999,3366,3367],{},"        +isSatisfiedBy(T candidate) boolean\n",[2999,3369,3371],{"class":3001,"line":3370},7,[2999,3372,3373],{},"        +and(Specification~T~ other) Specification~T~\n",[2999,3375,3377],{"class":3001,"line":3376},8,[2999,3378,3379],{},"        +or(Specification~T~ other) Specification~T~\n",[2999,3381,3383],{"class":3001,"line":3382},9,[2999,3384,3385],{},"        +not() Specification~T~\n",[2999,3387,3389],{"class":3001,"line":3388},10,[2999,3390,3391],{},"    }\n",[2999,3393,3395],{"class":3001,"line":3394},11,[2999,3396,3351],{"emptyLinePlaceholder":3350},[2999,3398,3400],{"class":3001,"line":3399},12,[2999,3401,3402],{},"    class PriceRangeSpec {\n",[2999,3404,3406],{"class":3001,"line":3405},13,[2999,3407,3408],{},"        -BigDecimal minPrice\n",[2999,3410,3412],{"class":3001,"line":3411},14,[2999,3413,3414],{},"        -BigDecimal maxPrice\n",[2999,3416,3418],{"class":3001,"line":3417},15,[2999,3419,3420],{},"        +isSatisfiedBy(Audiobook book) boolean\n",[2999,3422,3424],{"class":3001,"line":3423},16,[2999,3425,3391],{},[2999,3427,3429],{"class":3001,"line":3428},17,[2999,3430,3351],{"emptyLinePlaceholder":3350},[2999,3432,3434],{"class":3001,"line":3433},18,[2999,3435,3436],{},"    class GenreSpec {\n",[2999,3438,3440],{"class":3001,"line":3439},19,[2999,3441,3442],{},"        -String genreName\n",[2999,3444,3446],{"class":3001,"line":3445},20,[2999,3447,3420],{},[2999,3449,3451],{"class":3001,"line":3450},21,[2999,3452,3391],{},[2999,3454,3456],{"class":3001,"line":3455},22,[2999,3457,3351],{"emptyLinePlaceholder":3350},[2999,3459,3461],{"class":3001,"line":3460},23,[2999,3462,3463],{},"    class LanguageSpec {\n",[2999,3465,3467],{"class":3001,"line":3466},24,[2999,3468,3469],{},"        -String language\n",[2999,3471,3473],{"class":3001,"line":3472},25,[2999,3474,3420],{},[2999,3476,3478],{"class":3001,"line":3477},26,[2999,3479,3391],{},[2999,3481,3483],{"class":3001,"line":3482},27,[2999,3484,3351],{"emptyLinePlaceholder":3350},[2999,3486,3488],{"class":3001,"line":3487},28,[2999,3489,3490],{},"    class AndSpecification~T~ {\n",[2999,3492,3494],{"class":3001,"line":3493},29,[2999,3495,3496],{},"        -Specification~T~ left\n",[2999,3498,3500],{"class":3001,"line":3499},30,[2999,3501,3502],{},"        -Specification~T~ right\n",[2999,3504,3506],{"class":3001,"line":3505},31,[2999,3507,3367],{},[2999,3509,3511],{"class":3001,"line":3510},32,[2999,3512,3391],{},[2999,3514,3516],{"class":3001,"line":3515},33,[2999,3517,3351],{"emptyLinePlaceholder":3350},[2999,3519,3521],{"class":3001,"line":3520},34,[2999,3522,3523],{},"    class OrSpecification~T~ {\n",[2999,3525,3527],{"class":3001,"line":3526},35,[2999,3528,3496],{},[2999,3530,3532],{"class":3001,"line":3531},36,[2999,3533,3502],{},[2999,3535,3537],{"class":3001,"line":3536},37,[2999,3538,3367],{},[2999,3540,3542],{"class":3001,"line":3541},38,[2999,3543,3391],{},[2999,3545,3547],{"class":3001,"line":3546},39,[2999,3548,3351],{"emptyLinePlaceholder":3350},[2999,3550,3552],{"class":3001,"line":3551},40,[2999,3553,3554],{},"    class NotSpecification~T~ {\n",[2999,3556,3558],{"class":3001,"line":3557},41,[2999,3559,3560],{},"        -Specification~T~ spec\n",[2999,3562,3564],{"class":3001,"line":3563},42,[2999,3565,3367],{},[2999,3567,3569],{"class":3001,"line":3568},43,[2999,3570,3391],{},[2999,3572,3574],{"class":3001,"line":3573},44,[2999,3575,3351],{"emptyLinePlaceholder":3350},[2999,3577,3579],{"class":3001,"line":3578},45,[2999,3580,3581],{},"    Specification~T~ \u003C|.. PriceRangeSpec\n",[2999,3583,3585],{"class":3001,"line":3584},46,[2999,3586,3587],{},"    Specification~T~ \u003C|.. GenreSpec\n",[2999,3589,3591],{"class":3001,"line":3590},47,[2999,3592,3593],{},"    Specification~T~ \u003C|.. LanguageSpec\n",[2999,3595,3597],{"class":3001,"line":3596},48,[2999,3598,3599],{},"    Specification~T~ \u003C|.. AndSpecification~T~\n",[2999,3601,3603],{"class":3001,"line":3602},49,[2999,3604,3605],{},"    Specification~T~ \u003C|.. OrSpecification~T~\n",[2999,3607,3609],{"class":3001,"line":3608},50,[2999,3610,3611],{},"    Specification~T~ \u003C|.. NotSpecification~T~\n",[2999,3613,3615],{"class":3001,"line":3614},51,[2999,3616,3351],{"emptyLinePlaceholder":3350},[2999,3618,3620],{"class":3001,"line":3619},52,[2999,3621,3622],{},"    AndSpecification~T~ o-- Specification~T~ : left\n",[2999,3624,3626],{"class":3001,"line":3625},53,[2999,3627,3628],{},"    AndSpecification~T~ o-- Specification~T~ : right\n",[2999,3630,3632],{"class":3001,"line":3631},54,[2999,3633,3634],{},"    OrSpecification~T~ o-- Specification~T~ : left\n",[2999,3636,3638],{"class":3001,"line":3637},55,[2999,3639,3640],{},"    OrSpecification~T~ o-- Specification~T~ : right\n",[2999,3642,3644],{"class":3001,"line":3643},56,[2999,3645,3646],{},"    NotSpecification~T~ o-- Specification~T~\n",[2964,3648,3649,3650,2975,3653,3656,3657,3660,3661,3664,3665,3668],{},"Зверніть увагу на композиційну структуру: ",[2968,3651,3652],{},"AndSpecification",[2968,3654,3655],{},"OrSpecification"," та ",[2968,3658,3659],{},"NotSpecification"," самі реалізують ",[2968,3662,3663],{},"Specification\u003CT>"," і містять посилання на інші специфікації. Це класичний ",[3228,3666,3667],{},"Composite Pattern"," (GoF) — дерево об'єктів, де листки (прості специфікації) і вузли (логічні оператори) мають однаковий інтерфейс.",[3285,3670,3672],{"id":3671},"приклад-використання","Приклад використання",[2964,3674,3675],{},"Розглянемо, як виглядає клієнтський код із Specification:",[2990,3677,3679],{"className":2992,"code":3678,"language":2994,"meta":2995,"style":2995},"// Прості специфікації — будівельні блоки\nSpecification\u003CAudiobook> poetry = new GenreSpec(\"Поезія\");\nSpecification\u003CAudiobook> prose  = new GenreSpec(\"Проза\");\nSpecification\u003CAudiobook> after2020 = new YearAfterSpec(2020);\nSpecification\u003CAudiobook> affordable = new PriceRangeSpec(\n    BigDecimal.valueOf(100), \n    BigDecimal.valueOf(500)\n);\nSpecification\u003CAudiobook> ukrainian = new LanguageSpec(\"українська\");\n\n// Композиція через логічні оператори\nSpecification\u003CAudiobook> poetryOrProse = poetry.or(prose);\nSpecification\u003CAudiobook> complexQuery = poetryOrProse\n    .and(after2020)\n    .and(affordable)\n    .and(ukrainian);\n\n// Використання у репозиторії\nList\u003CAudiobook> results = audiobookRepository.findAll(complexQuery);\n",[2968,3680,3681,3686,3718,3745,3773,3796,3815,3831,3835,3862,3866,3871,3896,3912,3923,3932,3941,3945,3950],{"__ignoreMap":2995},[2999,3682,3683],{"class":3001,"line":3002},[2999,3684,3685],{"class":3222},"// Прості специфікації — будівельні блоки\n",[2999,3687,3688,3690,3692,3695,3697,3700,3703,3707,3710,3712,3716],{"class":3001,"line":3037},[2999,3689,3267],{"class":3012},[2999,3691,3023],{"class":3022},[2999,3693,3694],{"class":3012},"Audiobook",[2999,3696,3047],{"class":3022},[2999,3698,3699],{"class":3060},"poetry",[2999,3701,3702],{"class":3022}," = ",[2999,3704,3706],{"class":3705},"sCDza","new",[2999,3708,3709],{"class":3050}," GenreSpec",[2999,3711,3054],{"class":3022},[2999,3713,3715],{"class":3714},"sbdoH","\"Поезія\"",[2999,3717,3064],{"class":3022},[2999,3719,3720,3722,3724,3726,3728,3731,3734,3736,3738,3740,3743],{"class":3001,"line":3067},[2999,3721,3267],{"class":3012},[2999,3723,3023],{"class":3022},[2999,3725,3694],{"class":3012},[2999,3727,3047],{"class":3022},[2999,3729,3730],{"class":3060},"prose",[2999,3732,3733],{"class":3022},"  = ",[2999,3735,3706],{"class":3705},[2999,3737,3709],{"class":3050},[2999,3739,3054],{"class":3022},[2999,3741,3742],{"class":3714},"\"Проза\"",[2999,3744,3064],{"class":3022},[2999,3746,3747,3749,3751,3753,3755,3758,3760,3762,3765,3767,3771],{"class":3001,"line":3098},[2999,3748,3267],{"class":3012},[2999,3750,3023],{"class":3022},[2999,3752,3694],{"class":3012},[2999,3754,3047],{"class":3022},[2999,3756,3757],{"class":3060},"after2020",[2999,3759,3702],{"class":3022},[2999,3761,3706],{"class":3705},[2999,3763,3764],{"class":3050}," YearAfterSpec",[2999,3766,3054],{"class":3022},[2999,3768,3770],{"class":3769},"sJj4R","2020",[2999,3772,3064],{"class":3022},[2999,3774,3775,3777,3779,3781,3783,3786,3788,3790,3793],{"class":3001,"line":3219},[2999,3776,3267],{"class":3012},[2999,3778,3023],{"class":3022},[2999,3780,3694],{"class":3012},[2999,3782,3047],{"class":3022},[2999,3784,3785],{"class":3060},"affordable",[2999,3787,3702],{"class":3022},[2999,3789,3706],{"class":3705},[2999,3791,3792],{"class":3050}," PriceRangeSpec",[2999,3794,3795],{"class":3022},"(\n",[2999,3797,3798,3801,3804,3807,3809,3812],{"class":3001,"line":3364},[2999,3799,3800],{"class":3060},"    BigDecimal",[2999,3802,3803],{"class":3022},".",[2999,3805,3806],{"class":3050},"valueOf",[2999,3808,3054],{"class":3022},[2999,3810,3811],{"class":3769},"100",[2999,3813,3814],{"class":3022},"), \n",[2999,3816,3817,3819,3821,3823,3825,3828],{"class":3001,"line":3370},[2999,3818,3800],{"class":3060},[2999,3820,3803],{"class":3022},[2999,3822,3806],{"class":3050},[2999,3824,3054],{"class":3022},[2999,3826,3827],{"class":3769},"500",[2999,3829,3830],{"class":3022},")\n",[2999,3832,3833],{"class":3001,"line":3376},[2999,3834,3064],{"class":3022},[2999,3836,3837,3839,3841,3843,3845,3848,3850,3852,3855,3857,3860],{"class":3001,"line":3382},[2999,3838,3267],{"class":3012},[2999,3840,3023],{"class":3022},[2999,3842,3694],{"class":3012},[2999,3844,3047],{"class":3022},[2999,3846,3847],{"class":3060},"ukrainian",[2999,3849,3702],{"class":3022},[2999,3851,3706],{"class":3705},[2999,3853,3854],{"class":3050}," LanguageSpec",[2999,3856,3054],{"class":3022},[2999,3858,3859],{"class":3714},"\"українська\"",[2999,3861,3064],{"class":3022},[2999,3863,3864],{"class":3001,"line":3388},[2999,3865,3351],{"emptyLinePlaceholder":3350},[2999,3867,3868],{"class":3001,"line":3394},[2999,3869,3870],{"class":3222},"// Композиція через логічні оператори\n",[2999,3872,3873,3875,3877,3879,3881,3884,3886,3888,3890,3893],{"class":3001,"line":3399},[2999,3874,3267],{"class":3012},[2999,3876,3023],{"class":3022},[2999,3878,3694],{"class":3012},[2999,3880,3047],{"class":3022},[2999,3882,3883],{"class":3060},"poetryOrProse",[2999,3885,3702],{"class":3022},[2999,3887,3699],{"class":3060},[2999,3889,3803],{"class":3022},[2999,3891,3892],{"class":3050},"or",[2999,3894,3895],{"class":3022},"(prose);\n",[2999,3897,3898,3900,3902,3904,3906,3909],{"class":3001,"line":3405},[2999,3899,3267],{"class":3012},[2999,3901,3023],{"class":3022},[2999,3903,3694],{"class":3012},[2999,3905,3047],{"class":3022},[2999,3907,3908],{"class":3060},"complexQuery",[2999,3910,3911],{"class":3022}," = poetryOrProse\n",[2999,3913,3914,3917,3920],{"class":3001,"line":3411},[2999,3915,3916],{"class":3022},"    .",[2999,3918,3919],{"class":3050},"and",[2999,3921,3922],{"class":3022},"(after2020)\n",[2999,3924,3925,3927,3929],{"class":3001,"line":3417},[2999,3926,3916],{"class":3022},[2999,3928,3919],{"class":3050},[2999,3930,3931],{"class":3022},"(affordable)\n",[2999,3933,3934,3936,3938],{"class":3001,"line":3423},[2999,3935,3916],{"class":3022},[2999,3937,3919],{"class":3050},[2999,3939,3940],{"class":3022},"(ukrainian);\n",[2999,3942,3943],{"class":3001,"line":3428},[2999,3944,3351],{"emptyLinePlaceholder":3350},[2999,3946,3947],{"class":3001,"line":3433},[2999,3948,3949],{"class":3222},"// Використання у репозиторії\n",[2999,3951,3952,3954,3956,3958,3960,3963,3965,3968,3970,3972],{"class":3001,"line":3439},[2999,3953,3136],{"class":3012},[2999,3955,3023],{"class":3022},[2999,3957,3694],{"class":3012},[2999,3959,3047],{"class":3022},[2999,3961,3962],{"class":3060},"results",[2999,3964,3702],{"class":3022},[2999,3966,3967],{"class":3060},"audiobookRepository",[2999,3969,3803],{"class":3022},[2999,3971,2978],{"class":3050},[2999,3973,3974],{"class":3022},"(complexQuery);\n",[2964,3976,3977],{},"Порівняймо з наївним підходом:",[3979,3980,3981,4205],"tabs",{},[3982,3983,3985],"tabs-item",{"label":3984},"Без Specification",[2990,3986,3988],{"className":2992,"code":3987,"language":2994,"meta":2995,"style":2995},"// Жорстко закодований метод у репозиторії\nList\u003CAudiobook> findByGenresAndYearAndPriceAndLanguage(\n    List\u003CString> genres,\n    Integer minYear,\n    BigDecimal minPrice,\n    BigDecimal maxPrice,\n    String language\n) {\n    // SQL з багатьма умовами WHERE\n    String sql = \"\"\"\n        SELECT ... FROM audiobooks ab\n        JOIN genres g ON ab.genre_id = g.id\n        WHERE (g.name = ? OR g.name = ?)\n          AND ab.year > ?\n          AND ab.price BETWEEN ? AND ?\n          AND ab.language = ?\n        \"\"\";\n    // ... JDBC-код\n}\n\n// Виклик\nList\u003CAudiobook> results = repo.findByGenresAndYearAndPriceAndLanguage(\n    List.of(\"Поезія\", \"Проза\"), 2020, \n    BigDecimal.valueOf(100), BigDecimal.valueOf(500), \n    \"українська\"\n);\n",[2968,3989,3990,3995,4006,4013,4021,4028,4035,4043,4048,4053,4065,4070,4075,4080,4085,4090,4095,4103,4108,4112,4116,4121,4144,4169,4196,4201],{"__ignoreMap":2995},[2999,3991,3992],{"class":3001,"line":3002},[2999,3993,3994],{"class":3222},"// Жорстко закодований метод у репозиторії\n",[2999,3996,3997,3999,4001,4004],{"class":3001,"line":3037},[2999,3998,3136],{"class":3012},[2999,4000,3139],{"class":3022},[2999,4002,4003],{"class":3050},"findByGenresAndYearAndPriceAndLanguage",[2999,4005,3795],{"class":3022},[2999,4007,4008,4010],{"class":3001,"line":3067},[2999,4009,3040],{"class":3012},[2999,4011,4012],{"class":3022},"\u003CString> genres,\n",[2999,4014,4015,4018],{"class":3001,"line":3098},[2999,4016,4017],{"class":3012},"    Integer",[2999,4019,4020],{"class":3022}," minYear,\n",[2999,4022,4023,4025],{"class":3001,"line":3219},[2999,4024,3800],{"class":3012},[2999,4026,4027],{"class":3022}," minPrice,\n",[2999,4029,4030,4032],{"class":3001,"line":3364},[2999,4031,3800],{"class":3012},[2999,4033,4034],{"class":3022}," maxPrice,\n",[2999,4036,4037,4040],{"class":3001,"line":3370},[2999,4038,4039],{"class":3012},"    String",[2999,4041,4042],{"class":3022}," language\n",[2999,4044,4045],{"class":3001,"line":3376},[2999,4046,4047],{"class":3022},") {\n",[2999,4049,4050],{"class":3001,"line":3382},[2999,4051,4052],{"class":3222},"    // SQL з багатьма умовами WHERE\n",[2999,4054,4055,4057,4060,4062],{"class":3001,"line":3388},[2999,4056,4039],{"class":3012},[2999,4058,4059],{"class":3060}," sql",[2999,4061,3702],{"class":3022},[2999,4063,4064],{"class":3714},"\"\"\"\n",[2999,4066,4067],{"class":3001,"line":3394},[2999,4068,4069],{"class":3714},"        SELECT ... FROM audiobooks ab\n",[2999,4071,4072],{"class":3001,"line":3399},[2999,4073,4074],{"class":3714},"        JOIN genres g ON ab.genre_id = g.id\n",[2999,4076,4077],{"class":3001,"line":3405},[2999,4078,4079],{"class":3714},"        WHERE (g.name = ? OR g.name = ?)\n",[2999,4081,4082],{"class":3001,"line":3411},[2999,4083,4084],{"class":3714},"          AND ab.year > ?\n",[2999,4086,4087],{"class":3001,"line":3417},[2999,4088,4089],{"class":3714},"          AND ab.price BETWEEN ? AND ?\n",[2999,4091,4092],{"class":3001,"line":3423},[2999,4093,4094],{"class":3714},"          AND ab.language = ?\n",[2999,4096,4097,4100],{"class":3001,"line":3428},[2999,4098,4099],{"class":3714},"        \"\"\"",[2999,4101,4102],{"class":3022},";\n",[2999,4104,4105],{"class":3001,"line":3433},[2999,4106,4107],{"class":3222},"    // ... JDBC-код\n",[2999,4109,4110],{"class":3001,"line":3439},[2999,4111,3101],{"class":3022},[2999,4113,4114],{"class":3001,"line":3445},[2999,4115,3351],{"emptyLinePlaceholder":3350},[2999,4117,4118],{"class":3001,"line":3450},[2999,4119,4120],{"class":3222},"// Виклик\n",[2999,4122,4123,4125,4127,4129,4131,4133,4135,4138,4140,4142],{"class":3001,"line":3455},[2999,4124,3136],{"class":3012},[2999,4126,3023],{"class":3022},[2999,4128,3694],{"class":3012},[2999,4130,3047],{"class":3022},[2999,4132,3962],{"class":3060},[2999,4134,3702],{"class":3022},[2999,4136,4137],{"class":3060},"repo",[2999,4139,3803],{"class":3022},[2999,4141,4003],{"class":3050},[2999,4143,3795],{"class":3022},[2999,4145,4146,4148,4150,4153,4155,4157,4159,4161,4164,4166],{"class":3001,"line":3460},[2999,4147,3040],{"class":3060},[2999,4149,3803],{"class":3022},[2999,4151,4152],{"class":3050},"of",[2999,4154,3054],{"class":3022},[2999,4156,3715],{"class":3714},[2999,4158,2975],{"class":3022},[2999,4160,3742],{"class":3714},[2999,4162,4163],{"class":3022},"), ",[2999,4165,3770],{"class":3769},[2999,4167,4168],{"class":3022},", \n",[2999,4170,4171,4173,4175,4177,4179,4181,4183,4186,4188,4190,4192,4194],{"class":3001,"line":3466},[2999,4172,3800],{"class":3060},[2999,4174,3803],{"class":3022},[2999,4176,3806],{"class":3050},[2999,4178,3054],{"class":3022},[2999,4180,3811],{"class":3769},[2999,4182,4163],{"class":3022},[2999,4184,4185],{"class":3060},"BigDecimal",[2999,4187,3803],{"class":3022},[2999,4189,3806],{"class":3050},[2999,4191,3054],{"class":3022},[2999,4193,3827],{"class":3769},[2999,4195,3814],{"class":3022},[2999,4197,4198],{"class":3001,"line":3472},[2999,4199,4200],{"class":3714},"    \"українська\"\n",[2999,4202,4203],{"class":3001,"line":3477},[2999,4204,3064],{"class":3022},[3982,4206,4208],{"label":4207},"З Specification",[2990,4209,4211],{"className":2992,"code":4210,"language":2994,"meta":2995,"style":2995},"// Гнучка композиція — жодних нових методів у репозиторії\nSpecification\u003CAudiobook> spec = new GenreSpec(\"Поезія\")\n    .or(new GenreSpec(\"Проза\"))\n    .and(new YearAfterSpec(2020))\n    .and(new PriceRangeSpec(BigDecimal.valueOf(100), BigDecimal.valueOf(500)))\n    .and(new LanguageSpec(\"українська\"));\n\nList\u003CAudiobook> results = repo.findAll(spec);\n",[2968,4212,4213,4218,4243,4262,4280,4319,4338,4342],{"__ignoreMap":2995},[2999,4214,4215],{"class":3001,"line":3002},[2999,4216,4217],{"class":3222},"// Гнучка композиція — жодних нових методів у репозиторії\n",[2999,4219,4220,4222,4224,4226,4228,4231,4233,4235,4237,4239,4241],{"class":3001,"line":3037},[2999,4221,3267],{"class":3012},[2999,4223,3023],{"class":3022},[2999,4225,3694],{"class":3012},[2999,4227,3047],{"class":3022},[2999,4229,4230],{"class":3060},"spec",[2999,4232,3702],{"class":3022},[2999,4234,3706],{"class":3705},[2999,4236,3709],{"class":3050},[2999,4238,3054],{"class":3022},[2999,4240,3715],{"class":3714},[2999,4242,3830],{"class":3022},[2999,4244,4245,4247,4249,4251,4253,4255,4257,4259],{"class":3001,"line":3067},[2999,4246,3916],{"class":3022},[2999,4248,3892],{"class":3050},[2999,4250,3054],{"class":3022},[2999,4252,3706],{"class":3705},[2999,4254,3709],{"class":3050},[2999,4256,3054],{"class":3022},[2999,4258,3742],{"class":3714},[2999,4260,4261],{"class":3022},"))\n",[2999,4263,4264,4266,4268,4270,4272,4274,4276,4278],{"class":3001,"line":3098},[2999,4265,3916],{"class":3022},[2999,4267,3919],{"class":3050},[2999,4269,3054],{"class":3022},[2999,4271,3706],{"class":3705},[2999,4273,3764],{"class":3050},[2999,4275,3054],{"class":3022},[2999,4277,3770],{"class":3769},[2999,4279,4261],{"class":3022},[2999,4281,4282,4284,4286,4288,4290,4292,4294,4296,4298,4300,4302,4304,4306,4308,4310,4312,4314,4316],{"class":3001,"line":3219},[2999,4283,3916],{"class":3022},[2999,4285,3919],{"class":3050},[2999,4287,3054],{"class":3022},[2999,4289,3706],{"class":3705},[2999,4291,3792],{"class":3050},[2999,4293,3054],{"class":3022},[2999,4295,4185],{"class":3060},[2999,4297,3803],{"class":3022},[2999,4299,3806],{"class":3050},[2999,4301,3054],{"class":3022},[2999,4303,3811],{"class":3769},[2999,4305,4163],{"class":3022},[2999,4307,4185],{"class":3060},[2999,4309,3803],{"class":3022},[2999,4311,3806],{"class":3050},[2999,4313,3054],{"class":3022},[2999,4315,3827],{"class":3769},[2999,4317,4318],{"class":3022},")))\n",[2999,4320,4321,4323,4325,4327,4329,4331,4333,4335],{"class":3001,"line":3364},[2999,4322,3916],{"class":3022},[2999,4324,3919],{"class":3050},[2999,4326,3054],{"class":3022},[2999,4328,3706],{"class":3705},[2999,4330,3854],{"class":3050},[2999,4332,3054],{"class":3022},[2999,4334,3859],{"class":3714},[2999,4336,4337],{"class":3022},"));\n",[2999,4339,4340],{"class":3001,"line":3370},[2999,4341,3351],{"emptyLinePlaceholder":3350},[2999,4343,4344,4346,4348,4350,4352,4354,4356,4358,4360,4362],{"class":3001,"line":3376},[2999,4345,3136],{"class":3012},[2999,4347,3023],{"class":3022},[2999,4349,3694],{"class":3012},[2999,4351,3047],{"class":3022},[2999,4353,3962],{"class":3060},[2999,4355,3702],{"class":3022},[2999,4357,4137],{"class":3060},[2999,4359,3803],{"class":3022},[2999,4361,2978],{"class":3050},[2999,4363,4364],{"class":3022},"(spec);\n",[2964,4366,4367,4368,4371,4372,4375],{},"Ключова перевага: ",[3228,4369,4370],{},"репозиторій має лише один метод"," ",[2968,4373,4374],{},"findAll(Specification\u003CT> spec)"," замість десятків спеціалізованих методів. Нові комбінації критеріїв не вимагають змін у репозиторії — лише створення нових специфікацій або композиції існуючих.",[3278,4377],{},[2959,4379,4381],{"id":4380},"два-підходи-до-реалізації-in-memory-vs-sql","Два підходи до реалізації: In-Memory vs SQL",[2964,4383,4384],{},"Specification Pattern може бути реалізований двома принципово різними способами, залежно від того, де виконується фільтрація:",[3106,4386,4387,4450],{},[3109,4388,4391,4396,4418,4423,4434,4439],{"title":4389,"icon":4390},"In-Memory Specification","i-heroicons-cpu-chip",[2964,4392,4393],{},[3228,4394,4395],{},"Фільтрація у Java-пам'яті:",[4397,4398,4399,4405,4412],"ul",{},[4400,4401,4402,4404],"li",{},[2968,4403,3315],{}," перевіряє об'єкт у пам'яті",[4400,4406,4407,4408,4411],{},"Репозиторій завантажує ",[3228,4409,4410],{},"всі"," записи з БД",[4400,4413,4414,4415],{},"Фільтрація виконується через ",[2968,4416,4417],{},"stream().filter(spec::isSatisfiedBy)",[2964,4419,4420],{},[3228,4421,4422],{},"Переваги:",[4397,4424,4425,4428,4431],{},[4400,4426,4427],{},"✅ Простота реалізації",[4400,4429,4430],{},"✅ Не залежить від SQL-діалекту",[4400,4432,4433],{},"✅ Працює з будь-яким джерелом даних",[2964,4435,4436],{},[3228,4437,4438],{},"Недоліки:",[4397,4440,4441,4444,4447],{},[4400,4442,4443],{},"❌ Завантажує всі записи з БД (неефективно для великих таблиць)",[4400,4445,4446],{},"❌ Фільтрація на стороні Java (повільно)",[4400,4448,4449],{},"❌ Неможливо використати індекси БД",[3109,4451,4454,4459,4474,4478,4489,4493],{"title":4452,"icon":4453},"SQL Specification","i-heroicons-circle-stack",[2964,4455,4456],{},[3228,4457,4458],{},"Фільтрація на рівні БД:",[4397,4460,4461,4468,4471],{},[4400,4462,4463,4464,4467],{},"Специфікація генерує SQL-фрагмент (",[2968,4465,4466],{},"WHERE"," умову)",[4400,4469,4470],{},"Репозиторій будує повний SQL-запит із цим фрагментом",[4400,4472,4473],{},"БД виконує фільтрацію через індекси",[2964,4475,4476],{},[3228,4477,4422],{},[4397,4479,4480,4483,4486],{},[4400,4481,4482],{},"✅ Ефективно для великих таблиць",[4400,4484,4485],{},"✅ Використовує індекси БД",[4400,4487,4488],{},"✅ Мінімальне навантаження на мережу",[2964,4490,4491],{},[3228,4492,4438],{},[4397,4494,4495,4498,4501],{},[4400,4496,4497],{},"❌ Складніша реалізація",[4400,4499,4500],{},"❌ Залежність від SQL-діалекту",[4400,4502,4503],{},"❌ Не працює з in-memory колекціями",[2964,4505,4506,4507,4510],{},"Для enterprise-систем із великими обсягами даних ",[3228,4508,4509],{},"SQL Specification є єдиним прийнятним варіантом",". Завантаження 100 000 записів у пам'ять для фільтрації 10 з них — неприпустима марнотратність.",[2964,4512,4513,4514,4516,4517,4519],{},"У цій статті ми реалізуємо ",[3228,4515,4452],{}," — підхід, що генерує ",[2968,4518,4466],{},"-умови для JDBC-запитів.",[3278,4521],{},[2959,4523,4525],{"id":4524},"реалізація-інтерфейс-specificationt","Реалізація: Інтерфейс Specification\u003CT>",[2964,4527,4528,4529,4532,4533,4536],{},"Почнемо з базового інтерфейсу. На відміну від класичного визначення Еванса (що містить лише ",[2968,4530,4531],{},"isSatisfiedBy","), наш інтерфейс буде ",[3228,4534,4535],{},"dual-mode",": підтримувати і in-memory перевірку, і SQL-генерацію.",[2990,4538,4541],{"className":2992,"code":4539,"language":2994,"meta":4540,"style":2995},"package com.example.audiobook.specification;\n\n/**\n * Базовий інтерфейс Specification Pattern за Еріком Евансом (DDD, 2003).\n * \u003Cp>\n * Specification інкапсулює бізнес-правило у вигляді предиката, що може бути:\n * \u003Cul>\n *   \u003Cli>Застосований до об'єкта у пам'яті ({@link #isSatisfiedBy});\u003C/li>\n *   \u003Cli>Перетворений на SQL WHERE-умову ({@link #toSql});\u003C/li>\n *   \u003Cli>Скомбінований з іншими специфікаціями ({@link #and}, {@link #or}, {@link #not}).\u003C/li>\n * \u003C/ul>\n * \u003Cp>\n * \u003Cb>Приклад використання:\u003C/b>\n * \u003Cpre>{@code\n * Specification\u003CAudiobook> affordable = new PriceRangeSpec(100, 500);\n * Specification\u003CAudiobook> ukrainian  = new LanguageSpec(\"українська\");\n * Specification\u003CAudiobook> query = affordable.and(ukrainian);\n * \n * // In-memory фільтрація\n * boolean matches = query.isSatisfiedBy(someBook);\n * \n * // SQL-генерація\n * String whereClause = query.toSql();  // \"price BETWEEN ? AND ? AND language = ?\"\n * List\u003CObject> params = query.getParameters();  // [100, 500, \"українська\"]\n * }\u003C/pre>\n *\n * @param \u003CT> тип доменної сутності (Author, Genre, Audiobook)\n */\npublic interface Specification\u003CT> {\n\n    /**\n     * Перевіряє, чи задовольняє об'єкт умовам специфікації (in-memory).\n     * \u003Cp>\n     * Використовується для валідації об'єктів у пам'яті або фільтрації\n     * невеликих колекцій через {@code stream().filter(spec::isSatisfiedBy)}.\n     *\n     * @param candidate об'єкт для перевірки\n     * @return {@code true} якщо об'єкт задовольняє умовам\n     */\n    boolean isSatisfiedBy(T candidate);\n\n    /**\n     * Генерує SQL WHERE-умову для цієї специфікації.\n     * \u003Cp>\n     * Повертає фрагмент SQL без ключового слова {@code WHERE}.\n     * Параметри представлені як {@code ?} (JDBC placeholders).\n     * \u003Cp>\n     * \u003Cb>Приклад:\u003C/b> {@code \"price \u003C= ? AND language = ?\"}\n     *\n     * @return SQL-фрагмент для WHERE-умови\n     */\n    String toSql();\n\n    /**\n     * Повертає список параметрів для {@link #toSql()} у порядку появи {@code ?}.\n     * \u003Cp>\n     * Ці значення передаються у {@link java.sql.PreparedStatement#setObject}.\n     *\n     * @return список параметрів (може бути порожнім)\n     */\n    java.util.List\u003CObject> getParameters();\n\n    /**\n     * Логічне AND: обидві специфікації мають бути задоволені.\n     *\n     * @param other друга специфікація\n     * @return нова специфікація {@code this AND other}\n     */\n    default Specification\u003CT> and(Specification\u003CT> other) {\n        return new AndSpecification\u003C>(this, other);\n    }\n\n    /**\n     * Логічне OR: хоча б одна специфікація має бути задоволена.\n     *\n     * @param other друга специфікація\n     * @return нова специфікація {@code this OR other}\n     */\n    default Specification\u003CT> or(Specification\u003CT> other) {\n        return new OrSpecification\u003C>(this, other);\n    }\n\n    /**\n     * Логічне NOT: інверсія умови.\n     *\n     * @return нова специфікація {@code NOT this}\n     */\n    default Specification\u003CT> not() {\n        return new NotSpecification\u003C>(this);\n    }\n}\n","showLineNumbers",[2968,4542,4543,4551,4555,4560,4565,4570,4575,4580,4585,4590,4595,4600,4604,4609,4614,4619,4624,4629,4634,4642,4647,4651,4658,4666,4674,4679,4684,4698,4703,4719,4723,4728,4733,4738,4743,4748,4753,4766,4776,4781,4797,4801,4805,4810,4814,4819,4824,4828,4833,4837,4846,4850,4860,4864,4868,4885,4889,4895,4900,4910,4915,4942,4947,4952,4958,4963,4976,4986,4991,5022,5043,5048,5053,5058,5064,5069,5080,5090,5095,5124,5140,5145,5150,5155,5161,5166,5176,5181,5200,5216,5221],{"__ignoreMap":2995},[2999,4544,4545,4548],{"class":3001,"line":3002},[2999,4546,4547],{"class":3005},"package",[2999,4549,4550],{"class":3022}," com.example.audiobook.specification;\n",[2999,4552,4553],{"class":3001,"line":3037},[2999,4554,3351],{"emptyLinePlaceholder":3350},[2999,4556,4557],{"class":3001,"line":3067},[2999,4558,4559],{"class":3222},"/**\n",[2999,4561,4562],{"class":3001,"line":3098},[2999,4563,4564],{"class":3222}," * Базовий інтерфейс Specification Pattern за Еріком Евансом (DDD, 2003).\n",[2999,4566,4567],{"class":3001,"line":3219},[2999,4568,4569],{"class":3222}," * \u003Cp>\n",[2999,4571,4572],{"class":3001,"line":3364},[2999,4573,4574],{"class":3222}," * Specification інкапсулює бізнес-правило у вигляді предиката, що може бути:\n",[2999,4576,4577],{"class":3001,"line":3370},[2999,4578,4579],{"class":3222}," * \u003Cul>\n",[2999,4581,4582],{"class":3001,"line":3376},[2999,4583,4584],{"class":3222}," *   \u003Cli>Застосований до об'єкта у пам'яті ({@link #isSatisfiedBy});\u003C/li>\n",[2999,4586,4587],{"class":3001,"line":3382},[2999,4588,4589],{"class":3222}," *   \u003Cli>Перетворений на SQL WHERE-умову ({@link #toSql});\u003C/li>\n",[2999,4591,4592],{"class":3001,"line":3388},[2999,4593,4594],{"class":3222}," *   \u003Cli>Скомбінований з іншими специфікаціями ({@link #and}, {@link #or}, {@link #not}).\u003C/li>\n",[2999,4596,4597],{"class":3001,"line":3394},[2999,4598,4599],{"class":3222}," * \u003C/ul>\n",[2999,4601,4602],{"class":3001,"line":3399},[2999,4603,4569],{"class":3222},[2999,4605,4606],{"class":3001,"line":3405},[2999,4607,4608],{"class":3222}," * \u003Cb>Приклад використання:\u003C/b>\n",[2999,4610,4611],{"class":3001,"line":3411},[2999,4612,4613],{"class":3222}," * \u003Cpre>{@code\n",[2999,4615,4616],{"class":3001,"line":3417},[2999,4617,4618],{"class":3222}," * Specification\u003CAudiobook> affordable = new PriceRangeSpec(100, 500);\n",[2999,4620,4621],{"class":3001,"line":3423},[2999,4622,4623],{"class":3222}," * Specification\u003CAudiobook> ukrainian  = new LanguageSpec(\"українська\");\n",[2999,4625,4626],{"class":3001,"line":3428},[2999,4627,4628],{"class":3222}," * Specification\u003CAudiobook> query = affordable.and(ukrainian);\n",[2999,4630,4631],{"class":3001,"line":3433},[2999,4632,4633],{"class":3222}," * \n",[2999,4635,4636,4639],{"class":3001,"line":3439},[2999,4637,4638],{"class":3222}," *",[2999,4640,4641],{"class":3222}," // In-memory фільтрація\n",[2999,4643,4644],{"class":3001,"line":3445},[2999,4645,4646],{"class":3222}," * boolean matches = query.isSatisfiedBy(someBook);\n",[2999,4648,4649],{"class":3001,"line":3450},[2999,4650,4633],{"class":3222},[2999,4652,4653,4655],{"class":3001,"line":3455},[2999,4654,4638],{"class":3222},[2999,4656,4657],{"class":3222}," // SQL-генерація\n",[2999,4659,4660,4663],{"class":3001,"line":3460},[2999,4661,4662],{"class":3222}," * String whereClause = query.toSql();",[2999,4664,4665],{"class":3222},"  // \"price BETWEEN ? AND ? AND language = ?\"\n",[2999,4667,4668,4671],{"class":3001,"line":3466},[2999,4669,4670],{"class":3222}," * List\u003CObject> params = query.getParameters();",[2999,4672,4673],{"class":3222},"  // [100, 500, \"українська\"]\n",[2999,4675,4676],{"class":3001,"line":3472},[2999,4677,4678],{"class":3222}," * }\u003C/pre>\n",[2999,4680,4681],{"class":3001,"line":3477},[2999,4682,4683],{"class":3222}," *\n",[2999,4685,4686,4689,4692,4695],{"class":3001,"line":3482},[2999,4687,4688],{"class":3222}," * ",[2999,4690,4691],{"class":3005},"@param",[2999,4693,4694],{"class":3060}," \u003CT>",[2999,4696,4697],{"class":3222}," тип доменної сутності (Author, Genre, Audiobook)\n",[2999,4699,4700],{"class":3001,"line":3487},[2999,4701,4702],{"class":3222}," */\n",[2999,4704,4705,4707,4709,4712,4714,4717],{"class":3001,"line":3493},[2999,4706,3006],{"class":3005},[2999,4708,3009],{"class":3005},[2999,4710,4711],{"class":3012}," Specification",[2999,4713,3023],{"class":3022},[2999,4715,4716],{"class":3012},"T",[2999,4718,3034],{"class":3022},[2999,4720,4721],{"class":3001,"line":3499},[2999,4722,3351],{"emptyLinePlaceholder":3350},[2999,4724,4725],{"class":3001,"line":3505},[2999,4726,4727],{"class":3222},"    /**\n",[2999,4729,4730],{"class":3001,"line":3510},[2999,4731,4732],{"class":3222},"     * Перевіряє, чи задовольняє об'єкт умовам специфікації (in-memory).\n",[2999,4734,4735],{"class":3001,"line":3515},[2999,4736,4737],{"class":3222},"     * \u003Cp>\n",[2999,4739,4740],{"class":3001,"line":3520},[2999,4741,4742],{"class":3222},"     * Використовується для валідації об'єктів у пам'яті або фільтрації\n",[2999,4744,4745],{"class":3001,"line":3526},[2999,4746,4747],{"class":3222},"     * невеликих колекцій через {@code stream().filter(spec::isSatisfiedBy)}.\n",[2999,4749,4750],{"class":3001,"line":3531},[2999,4751,4752],{"class":3222},"     *\n",[2999,4754,4755,4758,4760,4763],{"class":3001,"line":3536},[2999,4756,4757],{"class":3222},"     * ",[2999,4759,4691],{"class":3005},[2999,4761,4762],{"class":3060}," candidate",[2999,4764,4765],{"class":3222}," об'єкт для перевірки\n",[2999,4767,4768,4770,4773],{"class":3001,"line":3541},[2999,4769,4757],{"class":3222},[2999,4771,4772],{"class":3005},"@return",[2999,4774,4775],{"class":3222}," {@code true} якщо об'єкт задовольняє умовам\n",[2999,4777,4778],{"class":3001,"line":3546},[2999,4779,4780],{"class":3222},"     */\n",[2999,4782,4783,4786,4789,4791,4793,4795],{"class":3001,"line":3551},[2999,4784,4785],{"class":3012},"    boolean",[2999,4787,4788],{"class":3050}," isSatisfiedBy",[2999,4790,3054],{"class":3022},[2999,4792,4716],{"class":3012},[2999,4794,4762],{"class":3060},[2999,4796,3064],{"class":3022},[2999,4798,4799],{"class":3001,"line":3557},[2999,4800,3351],{"emptyLinePlaceholder":3350},[2999,4802,4803],{"class":3001,"line":3563},[2999,4804,4727],{"class":3222},[2999,4806,4807],{"class":3001,"line":3568},[2999,4808,4809],{"class":3222},"     * Генерує SQL WHERE-умову для цієї специфікації.\n",[2999,4811,4812],{"class":3001,"line":3573},[2999,4813,4737],{"class":3222},[2999,4815,4816],{"class":3001,"line":3578},[2999,4817,4818],{"class":3222},"     * Повертає фрагмент SQL без ключового слова {@code WHERE}.\n",[2999,4820,4821],{"class":3001,"line":3584},[2999,4822,4823],{"class":3222},"     * Параметри представлені як {@code ?} (JDBC placeholders).\n",[2999,4825,4826],{"class":3001,"line":3590},[2999,4827,4737],{"class":3222},[2999,4829,4830],{"class":3001,"line":3596},[2999,4831,4832],{"class":3222},"     * \u003Cb>Приклад:\u003C/b> {@code \"price \u003C= ? AND language = ?\"}\n",[2999,4834,4835],{"class":3001,"line":3602},[2999,4836,4752],{"class":3222},[2999,4838,4839,4841,4843],{"class":3001,"line":3608},[2999,4840,4757],{"class":3222},[2999,4842,4772],{"class":3005},[2999,4844,4845],{"class":3222}," SQL-фрагмент для WHERE-умови\n",[2999,4847,4848],{"class":3001,"line":3614},[2999,4849,4780],{"class":3222},[2999,4851,4852,4854,4857],{"class":3001,"line":3619},[2999,4853,4039],{"class":3012},[2999,4855,4856],{"class":3050}," toSql",[2999,4858,4859],{"class":3022},"();\n",[2999,4861,4862],{"class":3001,"line":3625},[2999,4863,3351],{"emptyLinePlaceholder":3350},[2999,4865,4866],{"class":3001,"line":3631},[2999,4867,4727],{"class":3222},[2999,4869,4870,4873,4876,4879,4882],{"class":3001,"line":3637},[2999,4871,4872],{"class":3222},"     * Повертає список параметрів для {",[2999,4874,4875],{"class":3005},"@link",[2999,4877,4878],{"class":3222}," #",[2999,4880,4881],{"class":3060},"toSql()",[2999,4883,4884],{"class":3222},"} у порядку появи {@code ?}.\n",[2999,4886,4887],{"class":3001,"line":3643},[2999,4888,4737],{"class":3222},[2999,4890,4892],{"class":3001,"line":4891},57,[2999,4893,4894],{"class":3222},"     * Ці значення передаються у {@link java.sql.PreparedStatement#setObject}.\n",[2999,4896,4898],{"class":3001,"line":4897},58,[2999,4899,4752],{"class":3222},[2999,4901,4903,4905,4907],{"class":3001,"line":4902},59,[2999,4904,4757],{"class":3222},[2999,4906,4772],{"class":3005},[2999,4908,4909],{"class":3222}," список параметрів (може бути порожнім)\n",[2999,4911,4913],{"class":3001,"line":4912},60,[2999,4914,4780],{"class":3222},[2999,4916,4918,4921,4923,4926,4928,4930,4932,4935,4937,4940],{"class":3001,"line":4917},61,[2999,4919,4920],{"class":3012},"    java",[2999,4922,3803],{"class":3022},[2999,4924,4925],{"class":3012},"util",[2999,4927,3803],{"class":3022},[2999,4929,3136],{"class":3012},[2999,4931,3023],{"class":3022},[2999,4933,4934],{"class":3012},"Object",[2999,4936,3047],{"class":3022},[2999,4938,4939],{"class":3050},"getParameters",[2999,4941,4859],{"class":3022},[2999,4943,4945],{"class":3001,"line":4944},62,[2999,4946,3351],{"emptyLinePlaceholder":3350},[2999,4948,4950],{"class":3001,"line":4949},63,[2999,4951,4727],{"class":3222},[2999,4953,4955],{"class":3001,"line":4954},64,[2999,4956,4957],{"class":3222},"     * Логічне AND: обидві специфікації мають бути задоволені.\n",[2999,4959,4961],{"class":3001,"line":4960},65,[2999,4962,4752],{"class":3222},[2999,4964,4966,4968,4970,4973],{"class":3001,"line":4965},66,[2999,4967,4757],{"class":3222},[2999,4969,4691],{"class":3005},[2999,4971,4972],{"class":3060}," other",[2999,4974,4975],{"class":3222}," друга специфікація\n",[2999,4977,4979,4981,4983],{"class":3001,"line":4978},67,[2999,4980,4757],{"class":3222},[2999,4982,4772],{"class":3005},[2999,4984,4985],{"class":3222}," нова специфікація {@code this AND other}\n",[2999,4987,4989],{"class":3001,"line":4988},68,[2999,4990,4780],{"class":3222},[2999,4992,4994,4997,4999,5001,5003,5005,5007,5009,5011,5013,5015,5017,5020],{"class":3001,"line":4993},69,[2999,4995,4996],{"class":3005},"    default",[2999,4998,4711],{"class":3012},[2999,5000,3023],{"class":3022},[2999,5002,4716],{"class":3012},[2999,5004,3047],{"class":3022},[2999,5006,3919],{"class":3050},[2999,5008,3054],{"class":3022},[2999,5010,3267],{"class":3012},[2999,5012,3023],{"class":3022},[2999,5014,4716],{"class":3012},[2999,5016,3047],{"class":3022},[2999,5018,5019],{"class":3060},"other",[2999,5021,4047],{"class":3022},[2999,5023,5025,5028,5031,5034,5037,5040],{"class":3001,"line":5024},70,[2999,5026,5027],{"class":3705},"        return",[2999,5029,5030],{"class":3705}," new",[2999,5032,5033],{"class":3012}," AndSpecification",[2999,5035,5036],{"class":3022},"\u003C>(",[2999,5038,5039],{"class":3005},"this",[2999,5041,5042],{"class":3022},", other);\n",[2999,5044,5046],{"class":3001,"line":5045},71,[2999,5047,3391],{"class":3022},[2999,5049,5051],{"class":3001,"line":5050},72,[2999,5052,3351],{"emptyLinePlaceholder":3350},[2999,5054,5056],{"class":3001,"line":5055},73,[2999,5057,4727],{"class":3222},[2999,5059,5061],{"class":3001,"line":5060},74,[2999,5062,5063],{"class":3222},"     * Логічне OR: хоча б одна специфікація має бути задоволена.\n",[2999,5065,5067],{"class":3001,"line":5066},75,[2999,5068,4752],{"class":3222},[2999,5070,5072,5074,5076,5078],{"class":3001,"line":5071},76,[2999,5073,4757],{"class":3222},[2999,5075,4691],{"class":3005},[2999,5077,4972],{"class":3060},[2999,5079,4975],{"class":3222},[2999,5081,5083,5085,5087],{"class":3001,"line":5082},77,[2999,5084,4757],{"class":3222},[2999,5086,4772],{"class":3005},[2999,5088,5089],{"class":3222}," нова специфікація {@code this OR other}\n",[2999,5091,5093],{"class":3001,"line":5092},78,[2999,5094,4780],{"class":3222},[2999,5096,5098,5100,5102,5104,5106,5108,5110,5112,5114,5116,5118,5120,5122],{"class":3001,"line":5097},79,[2999,5099,4996],{"class":3005},[2999,5101,4711],{"class":3012},[2999,5103,3023],{"class":3022},[2999,5105,4716],{"class":3012},[2999,5107,3047],{"class":3022},[2999,5109,3892],{"class":3050},[2999,5111,3054],{"class":3022},[2999,5113,3267],{"class":3012},[2999,5115,3023],{"class":3022},[2999,5117,4716],{"class":3012},[2999,5119,3047],{"class":3022},[2999,5121,5019],{"class":3060},[2999,5123,4047],{"class":3022},[2999,5125,5127,5129,5131,5134,5136,5138],{"class":3001,"line":5126},80,[2999,5128,5027],{"class":3705},[2999,5130,5030],{"class":3705},[2999,5132,5133],{"class":3012}," OrSpecification",[2999,5135,5036],{"class":3022},[2999,5137,5039],{"class":3005},[2999,5139,5042],{"class":3022},[2999,5141,5143],{"class":3001,"line":5142},81,[2999,5144,3391],{"class":3022},[2999,5146,5148],{"class":3001,"line":5147},82,[2999,5149,3351],{"emptyLinePlaceholder":3350},[2999,5151,5153],{"class":3001,"line":5152},83,[2999,5154,4727],{"class":3222},[2999,5156,5158],{"class":3001,"line":5157},84,[2999,5159,5160],{"class":3222},"     * Логічне NOT: інверсія умови.\n",[2999,5162,5164],{"class":3001,"line":5163},85,[2999,5165,4752],{"class":3222},[2999,5167,5169,5171,5173],{"class":3001,"line":5168},86,[2999,5170,4757],{"class":3222},[2999,5172,4772],{"class":3005},[2999,5174,5175],{"class":3222}," нова специфікація {@code NOT this}\n",[2999,5177,5179],{"class":3001,"line":5178},87,[2999,5180,4780],{"class":3222},[2999,5182,5184,5186,5188,5190,5192,5194,5197],{"class":3001,"line":5183},88,[2999,5185,4996],{"class":3005},[2999,5187,4711],{"class":3012},[2999,5189,3023],{"class":3022},[2999,5191,4716],{"class":3012},[2999,5193,3047],{"class":3022},[2999,5195,5196],{"class":3050},"not",[2999,5198,5199],{"class":3022},"() {\n",[2999,5201,5203,5205,5207,5210,5212,5214],{"class":3001,"line":5202},89,[2999,5204,5027],{"class":3705},[2999,5206,5030],{"class":3705},[2999,5208,5209],{"class":3012}," NotSpecification",[2999,5211,5036],{"class":3022},[2999,5213,5039],{"class":3005},[2999,5215,3064],{"class":3022},[2999,5217,5219],{"class":3001,"line":5218},90,[2999,5220,3391],{"class":3022},[2999,5222,5224],{"class":3001,"line":5223},91,[2999,5225,3101],{"class":3022},[2964,5227,5228],{},[3228,5229,5230],{},"Ключові архітектурні рішення:",[4397,5232,5233,5246,5257],{},[4400,5234,5235,5238,5239,5241,5242,5245],{},[3228,5236,5237],{},"Рядки 15–16"," (",[2968,5240,4531],{}," + ",[2968,5243,5244],{},"toSql","): dual-mode інтерфейс дозволяє використовувати ту саму специфікацію і для in-memory фільтрації, і для SQL-запитів. Це корисно для тестування: можна перевірити логіку специфікації без бази даних.",[4400,5247,5248,5238,5251,5253,5254,3803],{},[3228,5249,5250],{},"Рядок 24",[2968,5252,4939],{},"): параметри відокремлені від SQL-рядка — це забезпечує безпеку від SQL-ін'єкцій. Репозиторій передає їх у ",[2968,5255,5256],{},"PreparedStatement.setObject()",[4400,5258,5259,5262,5263,2975,5265,2975,5267,5269,5270,5273,5274,5276],{},[3228,5260,5261],{},"Рядки 32–47"," (default-методи ",[2968,5264,3919],{},[2968,5266,3892],{},[2968,5268,5196],{},"): композиційні оператори реалізовані як default-методи Java 8+. Це дозволяє писати ",[2968,5271,5272],{},"spec1.and(spec2).or(spec3)"," без явного створення ",[2968,5275,3652],{}," вручну.",[3278,5278],{},[2959,5280,5282],{"id":5281},"композиційні-специфікації-and-or-not","Композиційні специфікації: AND, OR, NOT",[2964,5284,5285,5286,5289],{},"Тепер реалізуємо три логічні оператори. Вони є ",[3228,5287,5288],{},"декораторами"," (Decorator Pattern, GoF) — обгортають інші специфікації і змінюють їх поведінку.",[3285,5291,3652],{"id":5292},"andspecification",[2990,5294,5296],{"className":2992,"code":5295,"language":2994,"meta":4540,"style":2995},"package com.example.audiobook.specification;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Логічне AND: обидві специфікації мають бути задоволені.\n * \u003Cp>\n * SQL-генерація: {@code (left) AND (right)}.\n * Дужки обов'язкові для правильного пріоритету операторів.\n */\npublic class AndSpecification\u003CT> implements Specification\u003CT> {\n\n    private final Specification\u003CT> left;\n    private final Specification\u003CT> right;\n\n    public AndSpecification(Specification\u003CT> left, Specification\u003CT> right) {\n        this.left = left;\n        this.right = right;\n    }\n\n    @Override\n    public boolean isSatisfiedBy(T candidate) {\n        // Обидві умови мають бути true\n        return left.isSatisfiedBy(candidate) && right.isSatisfiedBy(candidate);\n    }\n\n    @Override\n    public String toSql() {\n        // Дужки критично важливі: (a OR b) AND c ≠ a OR (b AND c)\n        return \"(\" + left.toSql() + \") AND (\" + right.toSql() + \")\";\n    }\n\n    @Override\n    public List\u003CObject> getParameters() {\n        // Об'єднуємо параметри обох специфікацій у порядку появи\n        List\u003CObject> params = new ArrayList\u003C>();\n        params.addAll(left.getParameters());\n        params.addAll(right.getParameters());\n        return params;\n    }\n}\n",[2968,5297,5298,5304,5308,5316,5323,5327,5331,5336,5340,5345,5350,5354,5380,5384,5405,5424,5428,5461,5473,5484,5488,5492,5500,5517,5522,5545,5549,5553,5559,5570,5575,5611,5615,5619,5625,5642,5647,5671,5692,5710,5717,5721],{"__ignoreMap":2995},[2999,5299,5300,5302],{"class":3001,"line":3002},[2999,5301,4547],{"class":3005},[2999,5303,4550],{"class":3022},[2999,5305,5306],{"class":3001,"line":3037},[2999,5307,3351],{"emptyLinePlaceholder":3350},[2999,5309,5310,5313],{"class":3001,"line":3067},[2999,5311,5312],{"class":3005},"import",[2999,5314,5315],{"class":3022}," java.util.ArrayList;\n",[2999,5317,5318,5320],{"class":3001,"line":3098},[2999,5319,5312],{"class":3005},[2999,5321,5322],{"class":3022}," java.util.List;\n",[2999,5324,5325],{"class":3001,"line":3219},[2999,5326,3351],{"emptyLinePlaceholder":3350},[2999,5328,5329],{"class":3001,"line":3364},[2999,5330,4559],{"class":3222},[2999,5332,5333],{"class":3001,"line":3370},[2999,5334,5335],{"class":3222}," * Логічне AND: обидві специфікації мають бути задоволені.\n",[2999,5337,5338],{"class":3001,"line":3376},[2999,5339,4569],{"class":3222},[2999,5341,5342],{"class":3001,"line":3382},[2999,5343,5344],{"class":3222}," * SQL-генерація: {@code (left) AND (right)}.\n",[2999,5346,5347],{"class":3001,"line":3388},[2999,5348,5349],{"class":3222}," * Дужки обов'язкові для правильного пріоритету операторів.\n",[2999,5351,5352],{"class":3001,"line":3394},[2999,5353,4702],{"class":3222},[2999,5355,5356,5358,5361,5363,5365,5367,5369,5372,5374,5376,5378],{"class":3001,"line":3399},[2999,5357,3006],{"class":3005},[2999,5359,5360],{"class":3005}," class",[2999,5362,5033],{"class":3012},[2999,5364,3023],{"class":3022},[2999,5366,4716],{"class":3012},[2999,5368,3047],{"class":3022},[2999,5370,5371],{"class":3005},"implements",[2999,5373,4711],{"class":3012},[2999,5375,3023],{"class":3022},[2999,5377,4716],{"class":3012},[2999,5379,3034],{"class":3022},[2999,5381,5382],{"class":3001,"line":3405},[2999,5383,3351],{"emptyLinePlaceholder":3350},[2999,5385,5386,5389,5392,5394,5396,5398,5400,5403],{"class":3001,"line":3411},[2999,5387,5388],{"class":3005},"    private",[2999,5390,5391],{"class":3005}," final",[2999,5393,4711],{"class":3012},[2999,5395,3023],{"class":3022},[2999,5397,4716],{"class":3012},[2999,5399,3047],{"class":3022},[2999,5401,5402],{"class":3060},"left",[2999,5404,4102],{"class":3022},[2999,5406,5407,5409,5411,5413,5415,5417,5419,5422],{"class":3001,"line":3417},[2999,5408,5388],{"class":3005},[2999,5410,5391],{"class":3005},[2999,5412,4711],{"class":3012},[2999,5414,3023],{"class":3022},[2999,5416,4716],{"class":3012},[2999,5418,3047],{"class":3022},[2999,5420,5421],{"class":3060},"right",[2999,5423,4102],{"class":3022},[2999,5425,5426],{"class":3001,"line":3423},[2999,5427,3351],{"emptyLinePlaceholder":3350},[2999,5429,5430,5433,5435,5437,5439,5441,5443,5445,5447,5449,5451,5453,5455,5457,5459],{"class":3001,"line":3428},[2999,5431,5432],{"class":3005},"    public",[2999,5434,5033],{"class":3050},[2999,5436,3054],{"class":3022},[2999,5438,3267],{"class":3012},[2999,5440,3023],{"class":3022},[2999,5442,4716],{"class":3012},[2999,5444,3047],{"class":3022},[2999,5446,5402],{"class":3060},[2999,5448,2975],{"class":3022},[2999,5450,3267],{"class":3012},[2999,5452,3023],{"class":3022},[2999,5454,4716],{"class":3012},[2999,5456,3047],{"class":3022},[2999,5458,5421],{"class":3060},[2999,5460,4047],{"class":3022},[2999,5462,5463,5466,5468,5470],{"class":3001,"line":3433},[2999,5464,5465],{"class":3005},"        this",[2999,5467,3803],{"class":3022},[2999,5469,5402],{"class":3060},[2999,5471,5472],{"class":3022}," = left;\n",[2999,5474,5475,5477,5479,5481],{"class":3001,"line":3439},[2999,5476,5465],{"class":3005},[2999,5478,3803],{"class":3022},[2999,5480,5421],{"class":3060},[2999,5482,5483],{"class":3022}," = right;\n",[2999,5485,5486],{"class":3001,"line":3445},[2999,5487,3391],{"class":3022},[2999,5489,5490],{"class":3001,"line":3450},[2999,5491,3351],{"emptyLinePlaceholder":3350},[2999,5493,5494,5497],{"class":3001,"line":3455},[2999,5495,5496],{"class":3022},"    @",[2999,5498,5499],{"class":3012},"Override\n",[2999,5501,5502,5504,5507,5509,5511,5513,5515],{"class":3001,"line":3460},[2999,5503,5432],{"class":3005},[2999,5505,5506],{"class":3012}," boolean",[2999,5508,4788],{"class":3050},[2999,5510,3054],{"class":3022},[2999,5512,4716],{"class":3012},[2999,5514,4762],{"class":3060},[2999,5516,4047],{"class":3022},[2999,5518,5519],{"class":3001,"line":3466},[2999,5520,5521],{"class":3222},"        // Обидві умови мають бути true\n",[2999,5523,5524,5526,5529,5531,5533,5536,5538,5540,5542],{"class":3001,"line":3472},[2999,5525,5027],{"class":3705},[2999,5527,5528],{"class":3060}," left",[2999,5530,3803],{"class":3022},[2999,5532,4531],{"class":3050},[2999,5534,5535],{"class":3022},"(candidate) && ",[2999,5537,5421],{"class":3060},[2999,5539,3803],{"class":3022},[2999,5541,4531],{"class":3050},[2999,5543,5544],{"class":3022},"(candidate);\n",[2999,5546,5547],{"class":3001,"line":3477},[2999,5548,3391],{"class":3022},[2999,5550,5551],{"class":3001,"line":3482},[2999,5552,3351],{"emptyLinePlaceholder":3350},[2999,5554,5555,5557],{"class":3001,"line":3487},[2999,5556,5496],{"class":3022},[2999,5558,5499],{"class":3012},[2999,5560,5561,5563,5566,5568],{"class":3001,"line":3493},[2999,5562,5432],{"class":3005},[2999,5564,5565],{"class":3012}," String",[2999,5567,4856],{"class":3050},[2999,5569,5199],{"class":3022},[2999,5571,5572],{"class":3001,"line":3499},[2999,5573,5574],{"class":3222},"        // Дужки критично важливі: (a OR b) AND c ≠ a OR (b AND c)\n",[2999,5576,5577,5579,5582,5584,5586,5588,5590,5593,5596,5598,5600,5602,5604,5606,5609],{"class":3001,"line":3505},[2999,5578,5027],{"class":3705},[2999,5580,5581],{"class":3714}," \"(\"",[2999,5583,5241],{"class":3022},[2999,5585,5402],{"class":3060},[2999,5587,3803],{"class":3022},[2999,5589,5244],{"class":3050},[2999,5591,5592],{"class":3022},"() + ",[2999,5594,5595],{"class":3714},"\") AND (\"",[2999,5597,5241],{"class":3022},[2999,5599,5421],{"class":3060},[2999,5601,3803],{"class":3022},[2999,5603,5244],{"class":3050},[2999,5605,5592],{"class":3022},[2999,5607,5608],{"class":3714},"\")\"",[2999,5610,4102],{"class":3022},[2999,5612,5613],{"class":3001,"line":3510},[2999,5614,3391],{"class":3022},[2999,5616,5617],{"class":3001,"line":3515},[2999,5618,3351],{"emptyLinePlaceholder":3350},[2999,5620,5621,5623],{"class":3001,"line":3520},[2999,5622,5496],{"class":3022},[2999,5624,5499],{"class":3012},[2999,5626,5627,5629,5632,5634,5636,5638,5640],{"class":3001,"line":3526},[2999,5628,5432],{"class":3005},[2999,5630,5631],{"class":3012}," List",[2999,5633,3023],{"class":3022},[2999,5635,4934],{"class":3012},[2999,5637,3047],{"class":3022},[2999,5639,4939],{"class":3050},[2999,5641,5199],{"class":3022},[2999,5643,5644],{"class":3001,"line":3531},[2999,5645,5646],{"class":3222},"        // Об'єднуємо параметри обох специфікацій у порядку появи\n",[2999,5648,5649,5652,5654,5656,5658,5661,5663,5665,5668],{"class":3001,"line":3536},[2999,5650,5651],{"class":3012},"        List",[2999,5653,3023],{"class":3022},[2999,5655,4934],{"class":3012},[2999,5657,3047],{"class":3022},[2999,5659,5660],{"class":3060},"params",[2999,5662,3702],{"class":3022},[2999,5664,3706],{"class":3705},[2999,5666,5667],{"class":3012}," ArrayList",[2999,5669,5670],{"class":3022},"\u003C>();\n",[2999,5672,5673,5676,5678,5681,5683,5685,5687,5689],{"class":3001,"line":3541},[2999,5674,5675],{"class":3060},"        params",[2999,5677,3803],{"class":3022},[2999,5679,5680],{"class":3050},"addAll",[2999,5682,3054],{"class":3022},[2999,5684,5402],{"class":3060},[2999,5686,3803],{"class":3022},[2999,5688,4939],{"class":3050},[2999,5690,5691],{"class":3022},"());\n",[2999,5693,5694,5696,5698,5700,5702,5704,5706,5708],{"class":3001,"line":3546},[2999,5695,5675],{"class":3060},[2999,5697,3803],{"class":3022},[2999,5699,5680],{"class":3050},[2999,5701,3054],{"class":3022},[2999,5703,5421],{"class":3060},[2999,5705,3803],{"class":3022},[2999,5707,4939],{"class":3050},[2999,5709,5691],{"class":3022},[2999,5711,5712,5714],{"class":3001,"line":3551},[2999,5713,5027],{"class":3705},[2999,5715,5716],{"class":3022}," params;\n",[2999,5718,5719],{"class":3001,"line":3557},[2999,5720,3391],{"class":3022},[2999,5722,5723],{"class":3001,"line":3563},[2999,5724,3101],{"class":3022},[3285,5726,3655],{"id":5727},"orspecification",[2990,5729,5731],{"className":2992,"code":5730,"language":2994,"meta":4540,"style":2995},"package com.example.audiobook.specification;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Логічне OR: хоча б одна специфікація має бути задоволена.\n * \u003Cp>\n * SQL-генерація: {@code (left) OR (right)}.\n */\npublic class OrSpecification\u003CT> implements Specification\u003CT> {\n\n    private final Specification\u003CT> left;\n    private final Specification\u003CT> right;\n\n    public OrSpecification(Specification\u003CT> left, Specification\u003CT> right) {\n        this.left = left;\n        this.right = right;\n    }\n\n    @Override\n    public boolean isSatisfiedBy(T candidate) {\n        // Достатньо однієї true\n        return left.isSatisfiedBy(candidate) || right.isSatisfiedBy(candidate);\n    }\n\n    @Override\n    public String toSql() {\n        return \"(\" + left.toSql() + \") OR (\" + right.toSql() + \")\";\n    }\n\n    @Override\n    public List\u003CObject> getParameters() {\n        List\u003CObject> params = new ArrayList\u003C>();\n        params.addAll(left.getParameters());\n        params.addAll(right.getParameters());\n        return params;\n    }\n}\n",[2968,5732,5733,5739,5743,5749,5755,5759,5763,5768,5772,5777,5781,5805,5809,5827,5845,5849,5881,5891,5901,5905,5909,5915,5931,5936,5957,5961,5965,5971,5981,6014,6018,6022,6028,6044,6064,6082,6100,6106,6110],{"__ignoreMap":2995},[2999,5734,5735,5737],{"class":3001,"line":3002},[2999,5736,4547],{"class":3005},[2999,5738,4550],{"class":3022},[2999,5740,5741],{"class":3001,"line":3037},[2999,5742,3351],{"emptyLinePlaceholder":3350},[2999,5744,5745,5747],{"class":3001,"line":3067},[2999,5746,5312],{"class":3005},[2999,5748,5315],{"class":3022},[2999,5750,5751,5753],{"class":3001,"line":3098},[2999,5752,5312],{"class":3005},[2999,5754,5322],{"class":3022},[2999,5756,5757],{"class":3001,"line":3219},[2999,5758,3351],{"emptyLinePlaceholder":3350},[2999,5760,5761],{"class":3001,"line":3364},[2999,5762,4559],{"class":3222},[2999,5764,5765],{"class":3001,"line":3370},[2999,5766,5767],{"class":3222}," * Логічне OR: хоча б одна специфікація має бути задоволена.\n",[2999,5769,5770],{"class":3001,"line":3376},[2999,5771,4569],{"class":3222},[2999,5773,5774],{"class":3001,"line":3382},[2999,5775,5776],{"class":3222}," * SQL-генерація: {@code (left) OR (right)}.\n",[2999,5778,5779],{"class":3001,"line":3388},[2999,5780,4702],{"class":3222},[2999,5782,5783,5785,5787,5789,5791,5793,5795,5797,5799,5801,5803],{"class":3001,"line":3394},[2999,5784,3006],{"class":3005},[2999,5786,5360],{"class":3005},[2999,5788,5133],{"class":3012},[2999,5790,3023],{"class":3022},[2999,5792,4716],{"class":3012},[2999,5794,3047],{"class":3022},[2999,5796,5371],{"class":3005},[2999,5798,4711],{"class":3012},[2999,5800,3023],{"class":3022},[2999,5802,4716],{"class":3012},[2999,5804,3034],{"class":3022},[2999,5806,5807],{"class":3001,"line":3399},[2999,5808,3351],{"emptyLinePlaceholder":3350},[2999,5810,5811,5813,5815,5817,5819,5821,5823,5825],{"class":3001,"line":3405},[2999,5812,5388],{"class":3005},[2999,5814,5391],{"class":3005},[2999,5816,4711],{"class":3012},[2999,5818,3023],{"class":3022},[2999,5820,4716],{"class":3012},[2999,5822,3047],{"class":3022},[2999,5824,5402],{"class":3060},[2999,5826,4102],{"class":3022},[2999,5828,5829,5831,5833,5835,5837,5839,5841,5843],{"class":3001,"line":3411},[2999,5830,5388],{"class":3005},[2999,5832,5391],{"class":3005},[2999,5834,4711],{"class":3012},[2999,5836,3023],{"class":3022},[2999,5838,4716],{"class":3012},[2999,5840,3047],{"class":3022},[2999,5842,5421],{"class":3060},[2999,5844,4102],{"class":3022},[2999,5846,5847],{"class":3001,"line":3417},[2999,5848,3351],{"emptyLinePlaceholder":3350},[2999,5850,5851,5853,5855,5857,5859,5861,5863,5865,5867,5869,5871,5873,5875,5877,5879],{"class":3001,"line":3423},[2999,5852,5432],{"class":3005},[2999,5854,5133],{"class":3050},[2999,5856,3054],{"class":3022},[2999,5858,3267],{"class":3012},[2999,5860,3023],{"class":3022},[2999,5862,4716],{"class":3012},[2999,5864,3047],{"class":3022},[2999,5866,5402],{"class":3060},[2999,5868,2975],{"class":3022},[2999,5870,3267],{"class":3012},[2999,5872,3023],{"class":3022},[2999,5874,4716],{"class":3012},[2999,5876,3047],{"class":3022},[2999,5878,5421],{"class":3060},[2999,5880,4047],{"class":3022},[2999,5882,5883,5885,5887,5889],{"class":3001,"line":3428},[2999,5884,5465],{"class":3005},[2999,5886,3803],{"class":3022},[2999,5888,5402],{"class":3060},[2999,5890,5472],{"class":3022},[2999,5892,5893,5895,5897,5899],{"class":3001,"line":3433},[2999,5894,5465],{"class":3005},[2999,5896,3803],{"class":3022},[2999,5898,5421],{"class":3060},[2999,5900,5483],{"class":3022},[2999,5902,5903],{"class":3001,"line":3439},[2999,5904,3391],{"class":3022},[2999,5906,5907],{"class":3001,"line":3445},[2999,5908,3351],{"emptyLinePlaceholder":3350},[2999,5910,5911,5913],{"class":3001,"line":3450},[2999,5912,5496],{"class":3022},[2999,5914,5499],{"class":3012},[2999,5916,5917,5919,5921,5923,5925,5927,5929],{"class":3001,"line":3455},[2999,5918,5432],{"class":3005},[2999,5920,5506],{"class":3012},[2999,5922,4788],{"class":3050},[2999,5924,3054],{"class":3022},[2999,5926,4716],{"class":3012},[2999,5928,4762],{"class":3060},[2999,5930,4047],{"class":3022},[2999,5932,5933],{"class":3001,"line":3460},[2999,5934,5935],{"class":3222},"        // Достатньо однієї true\n",[2999,5937,5938,5940,5942,5944,5946,5949,5951,5953,5955],{"class":3001,"line":3466},[2999,5939,5027],{"class":3705},[2999,5941,5528],{"class":3060},[2999,5943,3803],{"class":3022},[2999,5945,4531],{"class":3050},[2999,5947,5948],{"class":3022},"(candidate) || ",[2999,5950,5421],{"class":3060},[2999,5952,3803],{"class":3022},[2999,5954,4531],{"class":3050},[2999,5956,5544],{"class":3022},[2999,5958,5959],{"class":3001,"line":3472},[2999,5960,3391],{"class":3022},[2999,5962,5963],{"class":3001,"line":3477},[2999,5964,3351],{"emptyLinePlaceholder":3350},[2999,5966,5967,5969],{"class":3001,"line":3482},[2999,5968,5496],{"class":3022},[2999,5970,5499],{"class":3012},[2999,5972,5973,5975,5977,5979],{"class":3001,"line":3487},[2999,5974,5432],{"class":3005},[2999,5976,5565],{"class":3012},[2999,5978,4856],{"class":3050},[2999,5980,5199],{"class":3022},[2999,5982,5983,5985,5987,5989,5991,5993,5995,5997,6000,6002,6004,6006,6008,6010,6012],{"class":3001,"line":3493},[2999,5984,5027],{"class":3705},[2999,5986,5581],{"class":3714},[2999,5988,5241],{"class":3022},[2999,5990,5402],{"class":3060},[2999,5992,3803],{"class":3022},[2999,5994,5244],{"class":3050},[2999,5996,5592],{"class":3022},[2999,5998,5999],{"class":3714},"\") OR (\"",[2999,6001,5241],{"class":3022},[2999,6003,5421],{"class":3060},[2999,6005,3803],{"class":3022},[2999,6007,5244],{"class":3050},[2999,6009,5592],{"class":3022},[2999,6011,5608],{"class":3714},[2999,6013,4102],{"class":3022},[2999,6015,6016],{"class":3001,"line":3499},[2999,6017,3391],{"class":3022},[2999,6019,6020],{"class":3001,"line":3505},[2999,6021,3351],{"emptyLinePlaceholder":3350},[2999,6023,6024,6026],{"class":3001,"line":3510},[2999,6025,5496],{"class":3022},[2999,6027,5499],{"class":3012},[2999,6029,6030,6032,6034,6036,6038,6040,6042],{"class":3001,"line":3515},[2999,6031,5432],{"class":3005},[2999,6033,5631],{"class":3012},[2999,6035,3023],{"class":3022},[2999,6037,4934],{"class":3012},[2999,6039,3047],{"class":3022},[2999,6041,4939],{"class":3050},[2999,6043,5199],{"class":3022},[2999,6045,6046,6048,6050,6052,6054,6056,6058,6060,6062],{"class":3001,"line":3520},[2999,6047,5651],{"class":3012},[2999,6049,3023],{"class":3022},[2999,6051,4934],{"class":3012},[2999,6053,3047],{"class":3022},[2999,6055,5660],{"class":3060},[2999,6057,3702],{"class":3022},[2999,6059,3706],{"class":3705},[2999,6061,5667],{"class":3012},[2999,6063,5670],{"class":3022},[2999,6065,6066,6068,6070,6072,6074,6076,6078,6080],{"class":3001,"line":3526},[2999,6067,5675],{"class":3060},[2999,6069,3803],{"class":3022},[2999,6071,5680],{"class":3050},[2999,6073,3054],{"class":3022},[2999,6075,5402],{"class":3060},[2999,6077,3803],{"class":3022},[2999,6079,4939],{"class":3050},[2999,6081,5691],{"class":3022},[2999,6083,6084,6086,6088,6090,6092,6094,6096,6098],{"class":3001,"line":3531},[2999,6085,5675],{"class":3060},[2999,6087,3803],{"class":3022},[2999,6089,5680],{"class":3050},[2999,6091,3054],{"class":3022},[2999,6093,5421],{"class":3060},[2999,6095,3803],{"class":3022},[2999,6097,4939],{"class":3050},[2999,6099,5691],{"class":3022},[2999,6101,6102,6104],{"class":3001,"line":3536},[2999,6103,5027],{"class":3705},[2999,6105,5716],{"class":3022},[2999,6107,6108],{"class":3001,"line":3541},[2999,6109,3391],{"class":3022},[2999,6111,6112],{"class":3001,"line":3546},[2999,6113,3101],{"class":3022},[3285,6115,3659],{"id":6116},"notspecification",[2990,6118,6120],{"className":2992,"code":6119,"language":2994,"meta":4540,"style":2995},"package com.example.audiobook.specification;\n\nimport java.util.List;\n\n/**\n * Логічне NOT: інверсія умови.\n * \u003Cp>\n * SQL-генерація: {@code NOT (spec)}.\n */\npublic class NotSpecification\u003CT> implements Specification\u003CT> {\n\n    private final Specification\u003CT> spec;\n\n    public NotSpecification(Specification\u003CT> spec) {\n        this.spec = spec;\n    }\n\n    @Override\n    public boolean isSatisfiedBy(T candidate) {\n        // Інверсія результату\n        return !spec.isSatisfiedBy(candidate);\n    }\n\n    @Override\n    public String toSql() {\n        return \"NOT (\" + spec.toSql() + \")\";\n    }\n\n    @Override\n    public List\u003CObject> getParameters() {\n        // Параметри залишаються незмінними\n        return spec.getParameters();\n    }\n}\n",[2968,6121,6122,6128,6132,6138,6142,6146,6151,6155,6160,6164,6188,6192,6210,6214,6234,6245,6249,6253,6259,6275,6280,6295,6299,6303,6309,6319,6340,6344,6348,6354,6370,6375,6388,6392],{"__ignoreMap":2995},[2999,6123,6124,6126],{"class":3001,"line":3002},[2999,6125,4547],{"class":3005},[2999,6127,4550],{"class":3022},[2999,6129,6130],{"class":3001,"line":3037},[2999,6131,3351],{"emptyLinePlaceholder":3350},[2999,6133,6134,6136],{"class":3001,"line":3067},[2999,6135,5312],{"class":3005},[2999,6137,5322],{"class":3022},[2999,6139,6140],{"class":3001,"line":3098},[2999,6141,3351],{"emptyLinePlaceholder":3350},[2999,6143,6144],{"class":3001,"line":3219},[2999,6145,4559],{"class":3222},[2999,6147,6148],{"class":3001,"line":3364},[2999,6149,6150],{"class":3222}," * Логічне NOT: інверсія умови.\n",[2999,6152,6153],{"class":3001,"line":3370},[2999,6154,4569],{"class":3222},[2999,6156,6157],{"class":3001,"line":3376},[2999,6158,6159],{"class":3222}," * SQL-генерація: {@code NOT (spec)}.\n",[2999,6161,6162],{"class":3001,"line":3382},[2999,6163,4702],{"class":3222},[2999,6165,6166,6168,6170,6172,6174,6176,6178,6180,6182,6184,6186],{"class":3001,"line":3388},[2999,6167,3006],{"class":3005},[2999,6169,5360],{"class":3005},[2999,6171,5209],{"class":3012},[2999,6173,3023],{"class":3022},[2999,6175,4716],{"class":3012},[2999,6177,3047],{"class":3022},[2999,6179,5371],{"class":3005},[2999,6181,4711],{"class":3012},[2999,6183,3023],{"class":3022},[2999,6185,4716],{"class":3012},[2999,6187,3034],{"class":3022},[2999,6189,6190],{"class":3001,"line":3394},[2999,6191,3351],{"emptyLinePlaceholder":3350},[2999,6193,6194,6196,6198,6200,6202,6204,6206,6208],{"class":3001,"line":3399},[2999,6195,5388],{"class":3005},[2999,6197,5391],{"class":3005},[2999,6199,4711],{"class":3012},[2999,6201,3023],{"class":3022},[2999,6203,4716],{"class":3012},[2999,6205,3047],{"class":3022},[2999,6207,4230],{"class":3060},[2999,6209,4102],{"class":3022},[2999,6211,6212],{"class":3001,"line":3405},[2999,6213,3351],{"emptyLinePlaceholder":3350},[2999,6215,6216,6218,6220,6222,6224,6226,6228,6230,6232],{"class":3001,"line":3411},[2999,6217,5432],{"class":3005},[2999,6219,5209],{"class":3050},[2999,6221,3054],{"class":3022},[2999,6223,3267],{"class":3012},[2999,6225,3023],{"class":3022},[2999,6227,4716],{"class":3012},[2999,6229,3047],{"class":3022},[2999,6231,4230],{"class":3060},[2999,6233,4047],{"class":3022},[2999,6235,6236,6238,6240,6242],{"class":3001,"line":3417},[2999,6237,5465],{"class":3005},[2999,6239,3803],{"class":3022},[2999,6241,4230],{"class":3060},[2999,6243,6244],{"class":3022}," = spec;\n",[2999,6246,6247],{"class":3001,"line":3423},[2999,6248,3391],{"class":3022},[2999,6250,6251],{"class":3001,"line":3428},[2999,6252,3351],{"emptyLinePlaceholder":3350},[2999,6254,6255,6257],{"class":3001,"line":3433},[2999,6256,5496],{"class":3022},[2999,6258,5499],{"class":3012},[2999,6260,6261,6263,6265,6267,6269,6271,6273],{"class":3001,"line":3439},[2999,6262,5432],{"class":3005},[2999,6264,5506],{"class":3012},[2999,6266,4788],{"class":3050},[2999,6268,3054],{"class":3022},[2999,6270,4716],{"class":3012},[2999,6272,4762],{"class":3060},[2999,6274,4047],{"class":3022},[2999,6276,6277],{"class":3001,"line":3445},[2999,6278,6279],{"class":3222},"        // Інверсія результату\n",[2999,6281,6282,6284,6287,6289,6291,6293],{"class":3001,"line":3450},[2999,6283,5027],{"class":3705},[2999,6285,6286],{"class":3022}," !",[2999,6288,4230],{"class":3060},[2999,6290,3803],{"class":3022},[2999,6292,4531],{"class":3050},[2999,6294,5544],{"class":3022},[2999,6296,6297],{"class":3001,"line":3455},[2999,6298,3391],{"class":3022},[2999,6300,6301],{"class":3001,"line":3460},[2999,6302,3351],{"emptyLinePlaceholder":3350},[2999,6304,6305,6307],{"class":3001,"line":3466},[2999,6306,5496],{"class":3022},[2999,6308,5499],{"class":3012},[2999,6310,6311,6313,6315,6317],{"class":3001,"line":3472},[2999,6312,5432],{"class":3005},[2999,6314,5565],{"class":3012},[2999,6316,4856],{"class":3050},[2999,6318,5199],{"class":3022},[2999,6320,6321,6323,6326,6328,6330,6332,6334,6336,6338],{"class":3001,"line":3477},[2999,6322,5027],{"class":3705},[2999,6324,6325],{"class":3714}," \"NOT (\"",[2999,6327,5241],{"class":3022},[2999,6329,4230],{"class":3060},[2999,6331,3803],{"class":3022},[2999,6333,5244],{"class":3050},[2999,6335,5592],{"class":3022},[2999,6337,5608],{"class":3714},[2999,6339,4102],{"class":3022},[2999,6341,6342],{"class":3001,"line":3482},[2999,6343,3391],{"class":3022},[2999,6345,6346],{"class":3001,"line":3487},[2999,6347,3351],{"emptyLinePlaceholder":3350},[2999,6349,6350,6352],{"class":3001,"line":3493},[2999,6351,5496],{"class":3022},[2999,6353,5499],{"class":3012},[2999,6355,6356,6358,6360,6362,6364,6366,6368],{"class":3001,"line":3499},[2999,6357,5432],{"class":3005},[2999,6359,5631],{"class":3012},[2999,6361,3023],{"class":3022},[2999,6363,4934],{"class":3012},[2999,6365,3047],{"class":3022},[2999,6367,4939],{"class":3050},[2999,6369,5199],{"class":3022},[2999,6371,6372],{"class":3001,"line":3505},[2999,6373,6374],{"class":3222},"        // Параметри залишаються незмінними\n",[2999,6376,6377,6379,6382,6384,6386],{"class":3001,"line":3510},[2999,6378,5027],{"class":3705},[2999,6380,6381],{"class":3060}," spec",[2999,6383,3803],{"class":3022},[2999,6385,4939],{"class":3050},[2999,6387,4859],{"class":3022},[2999,6389,6390],{"class":3001,"line":3515},[2999,6391,3391],{"class":3022},[2999,6393,6394],{"class":3001,"line":3520},[2999,6395,3101],{"class":3022},[2964,6397,6398,6404],{},[3228,6399,6400,6401,6403],{},"Чому дужки у ",[2968,6402,4881],{},"?"," Розглянемо вираз без дужок:",[2990,6406,6410],{"className":6407,"code":6408,"language":6409,"meta":2995,"style":2995},"language-sql shiki shiki-themes light-plus dark-plus dark-plus","-- Без дужок (НЕПРАВИЛЬНО):\nSELECT * FROM audiobooks WHERE price \u003C= 500 AND language = 'uk' OR year > 2020\n\n-- SQL інтерпретує як:\n(price \u003C= 500 AND language = 'uk') OR (year > 2020)\n\n-- Але ми хотіли:\nprice \u003C= 500 AND (language = 'uk' OR year > 2020)\n","sql",[2968,6411,6412,6417,6460,6464,6469,6500,6504,6509],{"__ignoreMap":2995},[2999,6413,6414],{"class":3001,"line":3002},[2999,6415,6416],{"class":3222},"-- Без дужок (НЕПРАВИЛЬНО):\n",[2999,6418,6419,6422,6424,6427,6430,6432,6435,6437,6440,6443,6445,6448,6451,6454,6457],{"class":3001,"line":3037},[2999,6420,6421],{"class":3005},"SELECT",[2999,6423,4688],{"class":3022},[2999,6425,6426],{"class":3005},"FROM",[2999,6428,6429],{"class":3022}," audiobooks ",[2999,6431,4466],{"class":3005},[2999,6433,6434],{"class":3022}," price \u003C= ",[2999,6436,3827],{"class":3769},[2999,6438,6439],{"class":3005}," AND",[2999,6441,6442],{"class":3005}," language",[2999,6444,3702],{"class":3022},[2999,6446,6447],{"class":3714},"'uk'",[2999,6449,6450],{"class":3005}," OR",[2999,6452,6453],{"class":3005}," year",[2999,6455,6456],{"class":3022}," > ",[2999,6458,6459],{"class":3769},"2020\n",[2999,6461,6462],{"class":3001,"line":3067},[2999,6463,3351],{"emptyLinePlaceholder":3350},[2999,6465,6466],{"class":3001,"line":3098},[2999,6467,6468],{"class":3222},"-- SQL інтерпретує як:\n",[2999,6470,6471,6474,6476,6478,6480,6482,6484,6487,6489,6491,6494,6496,6498],{"class":3001,"line":3219},[2999,6472,6473],{"class":3022},"(price \u003C= ",[2999,6475,3827],{"class":3769},[2999,6477,6439],{"class":3005},[2999,6479,6442],{"class":3005},[2999,6481,3702],{"class":3022},[2999,6483,6447],{"class":3714},[2999,6485,6486],{"class":3022},") ",[2999,6488,3322],{"class":3005},[2999,6490,5238],{"class":3022},[2999,6492,6493],{"class":3005},"year",[2999,6495,6456],{"class":3022},[2999,6497,3770],{"class":3769},[2999,6499,3830],{"class":3022},[2999,6501,6502],{"class":3001,"line":3364},[2999,6503,3351],{"emptyLinePlaceholder":3350},[2999,6505,6506],{"class":3001,"line":3370},[2999,6507,6508],{"class":3222},"-- Але ми хотіли:\n",[2999,6510,6511,6514,6516,6518,6520,6523,6525,6527,6529,6531,6533,6535],{"class":3001,"line":3376},[2999,6512,6513],{"class":3022},"price \u003C= ",[2999,6515,3827],{"class":3769},[2999,6517,6439],{"class":3005},[2999,6519,5238],{"class":3022},[2999,6521,6522],{"class":3005},"language",[2999,6524,3702],{"class":3022},[2999,6526,6447],{"class":3714},[2999,6528,6450],{"class":3005},[2999,6530,6453],{"class":3005},[2999,6532,6456],{"class":3022},[2999,6534,3770],{"class":3769},[2999,6536,3830],{"class":3022},[2964,6538,6539,6540,3656,6542,6544],{},"Дужки у ",[2968,6541,3652],{},[2968,6543,3655],{}," гарантують правильний пріоритет операторів незалежно від порядку композиції.",[3278,6546],{},[2959,6548,6550],{"id":6549},"інтеграція-з-repository","Інтеграція з Repository",[2964,6552,6553,6554,6557],{},"Тепер найважливіша частина: як репозиторій використовує специфікації для побудови SQL-запитів? Додамо новий метод до інтерфейсу ",[2968,6555,6556],{},"Repository\u003CT, ID>",":",[2990,6559,6561],{"className":2992,"code":6560,"language":2994,"meta":4540,"style":2995},"package com.example.audiobook.repository;\n\nimport com.example.audiobook.specification.Specification;\n\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * Базовий контракт репозиторію з підтримкою Specification Pattern.\n */\npublic interface Repository\u003CT, ID> {\n\n    Optional\u003CT> findById(ID id);\n    List\u003CT> findAll();\n    void save(T entity);\n    void update(T entity);\n    boolean deleteById(ID id);\n    long count();\n    boolean existsById(ID id);\n\n    /**\n     * Знаходить всі сутності, що задовольняють заданій специфікації.\n     * \u003Cp>\n     * Специфікація перетворюється на SQL WHERE-умову, що виконується на рівні БД.\n     * Це забезпечує ефективність для великих таблиць (використання індексів).\n     *\n     * @param spec специфікація для фільтрації\n     * @return список сутностей, що задовольняють умовам (може бути порожнім)\n     */\n    List\u003CT> findAll(Specification\u003CT> spec);\n}\n",[2968,6562,6563,6570,6574,6581,6585,6591,6598,6602,6606,6611,6615,6634,6638,6659,6673,6690,6705,6720,6730,6745,6749,6753,6758,6762,6767,6772,6776,6787,6796,6800,6826],{"__ignoreMap":2995},[2999,6564,6565,6567],{"class":3001,"line":3002},[2999,6566,4547],{"class":3005},[2999,6568,6569],{"class":3022}," com.example.audiobook.repository;\n",[2999,6571,6572],{"class":3001,"line":3037},[2999,6573,3351],{"emptyLinePlaceholder":3350},[2999,6575,6576,6578],{"class":3001,"line":3067},[2999,6577,5312],{"class":3005},[2999,6579,6580],{"class":3022}," com.example.audiobook.specification.Specification;\n",[2999,6582,6583],{"class":3001,"line":3098},[2999,6584,3351],{"emptyLinePlaceholder":3350},[2999,6586,6587,6589],{"class":3001,"line":3219},[2999,6588,5312],{"class":3005},[2999,6590,5322],{"class":3022},[2999,6592,6593,6595],{"class":3001,"line":3364},[2999,6594,5312],{"class":3005},[2999,6596,6597],{"class":3022}," java.util.Optional;\n",[2999,6599,6600],{"class":3001,"line":3370},[2999,6601,3351],{"emptyLinePlaceholder":3350},[2999,6603,6604],{"class":3001,"line":3376},[2999,6605,4559],{"class":3222},[2999,6607,6608],{"class":3001,"line":3382},[2999,6609,6610],{"class":3222}," * Базовий контракт репозиторію з підтримкою Specification Pattern.\n",[2999,6612,6613],{"class":3001,"line":3388},[2999,6614,4702],{"class":3222},[2999,6616,6617,6619,6621,6623,6625,6627,6629,6632],{"class":3001,"line":3394},[2999,6618,3006],{"class":3005},[2999,6620,3009],{"class":3005},[2999,6622,3019],{"class":3012},[2999,6624,3023],{"class":3022},[2999,6626,4716],{"class":3012},[2999,6628,2975],{"class":3022},[2999,6630,6631],{"class":3012},"ID",[2999,6633,3034],{"class":3022},[2999,6635,6636],{"class":3001,"line":3399},[2999,6637,3351],{"emptyLinePlaceholder":3350},[2999,6639,6640,6642,6644,6646,6648,6650,6652,6654,6657],{"class":3001,"line":3405},[2999,6641,3070],{"class":3012},[2999,6643,3023],{"class":3022},[2999,6645,4716],{"class":3012},[2999,6647,3047],{"class":3022},[2999,6649,2974],{"class":3050},[2999,6651,3054],{"class":3022},[2999,6653,6631],{"class":3012},[2999,6655,6656],{"class":3060}," id",[2999,6658,3064],{"class":3022},[2999,6660,6661,6663,6665,6667,6669,6671],{"class":3001,"line":3411},[2999,6662,3040],{"class":3012},[2999,6664,3023],{"class":3022},[2999,6666,4716],{"class":3012},[2999,6668,3047],{"class":3022},[2999,6670,2978],{"class":3050},[2999,6672,4859],{"class":3022},[2999,6674,6675,6678,6681,6683,6685,6688],{"class":3001,"line":3417},[2999,6676,6677],{"class":3012},"    void",[2999,6679,6680],{"class":3050}," save",[2999,6682,3054],{"class":3022},[2999,6684,4716],{"class":3012},[2999,6686,6687],{"class":3060}," entity",[2999,6689,3064],{"class":3022},[2999,6691,6692,6694,6697,6699,6701,6703],{"class":3001,"line":3423},[2999,6693,6677],{"class":3012},[2999,6695,6696],{"class":3050}," update",[2999,6698,3054],{"class":3022},[2999,6700,4716],{"class":3012},[2999,6702,6687],{"class":3060},[2999,6704,3064],{"class":3022},[2999,6706,6707,6709,6712,6714,6716,6718],{"class":3001,"line":3428},[2999,6708,4785],{"class":3012},[2999,6710,6711],{"class":3050}," deleteById",[2999,6713,3054],{"class":3022},[2999,6715,6631],{"class":3012},[2999,6717,6656],{"class":3060},[2999,6719,3064],{"class":3022},[2999,6721,6722,6725,6728],{"class":3001,"line":3433},[2999,6723,6724],{"class":3012},"    long",[2999,6726,6727],{"class":3050}," count",[2999,6729,4859],{"class":3022},[2999,6731,6732,6734,6737,6739,6741,6743],{"class":3001,"line":3439},[2999,6733,4785],{"class":3012},[2999,6735,6736],{"class":3050}," existsById",[2999,6738,3054],{"class":3022},[2999,6740,6631],{"class":3012},[2999,6742,6656],{"class":3060},[2999,6744,3064],{"class":3022},[2999,6746,6747],{"class":3001,"line":3445},[2999,6748,3351],{"emptyLinePlaceholder":3350},[2999,6750,6751],{"class":3001,"line":3450},[2999,6752,4727],{"class":3222},[2999,6754,6755],{"class":3001,"line":3455},[2999,6756,6757],{"class":3222},"     * Знаходить всі сутності, що задовольняють заданій специфікації.\n",[2999,6759,6760],{"class":3001,"line":3460},[2999,6761,4737],{"class":3222},[2999,6763,6764],{"class":3001,"line":3466},[2999,6765,6766],{"class":3222},"     * Специфікація перетворюється на SQL WHERE-умову, що виконується на рівні БД.\n",[2999,6768,6769],{"class":3001,"line":3472},[2999,6770,6771],{"class":3222},"     * Це забезпечує ефективність для великих таблиць (використання індексів).\n",[2999,6773,6774],{"class":3001,"line":3477},[2999,6775,4752],{"class":3222},[2999,6777,6778,6780,6782,6784],{"class":3001,"line":3482},[2999,6779,4757],{"class":3222},[2999,6781,4691],{"class":3005},[2999,6783,6381],{"class":3060},[2999,6785,6786],{"class":3222}," специфікація для фільтрації\n",[2999,6788,6789,6791,6793],{"class":3001,"line":3487},[2999,6790,4757],{"class":3222},[2999,6792,4772],{"class":3005},[2999,6794,6795],{"class":3222}," список сутностей, що задовольняють умовам (може бути порожнім)\n",[2999,6797,6798],{"class":3001,"line":3493},[2999,6799,4780],{"class":3222},[2999,6801,6802,6804,6806,6808,6810,6812,6814,6816,6818,6820,6822,6824],{"class":3001,"line":3499},[2999,6803,3040],{"class":3012},[2999,6805,3023],{"class":3022},[2999,6807,4716],{"class":3012},[2999,6809,3047],{"class":3022},[2999,6811,2978],{"class":3050},[2999,6813,3054],{"class":3022},[2999,6815,3267],{"class":3012},[2999,6817,3023],{"class":3022},[2999,6819,4716],{"class":3012},[2999,6821,3047],{"class":3022},[2999,6823,4230],{"class":3060},[2999,6825,3064],{"class":3022},[2999,6827,6828],{"class":3001,"line":3505},[2999,6829,3101],{"class":3022},[2964,6831,6832,6833,6836],{},"Тепер реалізуємо цей метод у ",[2968,6834,6835],{},"AbstractJdbcRepository",". Ключова ідея: базовий SELECT-запит доповнюється WHERE-умовою зі специфікації.",[2990,6838,6840],{"className":2992,"code":6839,"language":2994,"meta":4540,"style":2995},"package com.example.audiobook.repository.jdbc;\n\nimport com.example.audiobook.db.ConnectionManager;\nimport com.example.audiobook.db.DatabaseException;\nimport com.example.audiobook.repository.Repository;\nimport com.example.audiobook.specification.Specification;\n\nimport java.sql.*;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * Абстрактний JDBC-репозиторій з підтримкою Specification Pattern.\n */\npublic abstract class AbstractJdbcRepository\u003CT, ID> implements Repository\u003CT, ID> {\n\n    protected final ConnectionManager connectionManager;\n\n    protected AbstractJdbcRepository(ConnectionManager connectionManager) {\n        this.connectionManager = connectionManager;\n    }\n\n    // ... існуючі методи findById, findAll, save, update, deleteById ...\n\n    /**\n     * Знаходить сутності за специфікацією.\n     * \u003Cp>\n     * Алгоритм:\n     * \u003Col>\n     *   \u003Cli>Отримати базовий SELECT-запит від підкласу ({@link #getSelectAllSql()});\u003C/li>\n     *   \u003Cli>Додати WHERE-умову зі специфікації ({@code spec.toSql()});\u003C/li>\n     *   \u003Cli>Встановити параметри ({@code spec.getParameters()});\u003C/li>\n     *   \u003Cli>Виконати запит і змаппити результати.\u003C/li>\n     * \u003C/ol>\n     */\n    @Override\n    public List\u003CT> findAll(Specification\u003CT> spec) {\n        // Базовий SELECT без WHERE (наприклад, \"SELECT ... FROM audiobooks ab JOIN ...\")\n        String baseSql = getSelectAllSql();\n        \n        // Додаємо WHERE-умову зі специфікації\n        String sql = baseSql + \" WHERE \" + spec.toSql();\n\n        List\u003CT> results = new ArrayList\u003C>();\n\n        try (Connection conn = connectionManager.getConnection();\n             PreparedStatement stmt = conn.prepareStatement(sql)) {\n\n            // Встановлюємо параметри зі специфікації\n            List\u003CObject> params = spec.getParameters();\n            for (int i = 0; i \u003C params.size(); i++) {\n                stmt.setObject(i + 1, params.get(i));\n            }\n\n            try (ResultSet rs = stmt.executeQuery()) {\n                while (rs.next()) {\n                    results.add(mapRow(rs));\n                }\n            }\n\n        } catch (SQLException e) {\n            throw new DatabaseException(\n                \"Помилка findAll(Specification) для таблиці \" + getTableName(), e);\n        }\n\n        return results;\n    }\n\n    // Абстрактні методи, що підкласи зобов'язані реалізувати\n    protected abstract T mapRow(ResultSet rs) throws SQLException;\n    protected abstract String getTableName();\n    protected abstract String getSelectAllSql();\n    // ... інші абстрактні методи\n}\n",[2968,6841,6842,6849,6853,6860,6867,6874,6880,6884,6891,6897,6903,6909,6913,6917,6922,6926,6962,6966,6981,6985,7000,7012,7016,7020,7025,7029,7033,7038,7042,7047,7052,7067,7072,7077,7082,7087,7091,7097,7125,7130,7145,7150,7155,7177,7181,7201,7205,7229,7250,7254,7259,7282,7313,7341,7346,7350,7376,7393,7411,7416,7420,7424,7442,7454,7467,7472,7476,7483,7487,7491,7496,7524,7537,7550,7555],{"__ignoreMap":2995},[2999,6843,6844,6846],{"class":3001,"line":3002},[2999,6845,4547],{"class":3005},[2999,6847,6848],{"class":3022}," com.example.audiobook.repository.jdbc;\n",[2999,6850,6851],{"class":3001,"line":3037},[2999,6852,3351],{"emptyLinePlaceholder":3350},[2999,6854,6855,6857],{"class":3001,"line":3067},[2999,6856,5312],{"class":3005},[2999,6858,6859],{"class":3022}," com.example.audiobook.db.ConnectionManager;\n",[2999,6861,6862,6864],{"class":3001,"line":3098},[2999,6863,5312],{"class":3005},[2999,6865,6866],{"class":3022}," com.example.audiobook.db.DatabaseException;\n",[2999,6868,6869,6871],{"class":3001,"line":3219},[2999,6870,5312],{"class":3005},[2999,6872,6873],{"class":3022}," com.example.audiobook.repository.Repository;\n",[2999,6875,6876,6878],{"class":3001,"line":3364},[2999,6877,5312],{"class":3005},[2999,6879,6580],{"class":3022},[2999,6881,6882],{"class":3001,"line":3370},[2999,6883,3351],{"emptyLinePlaceholder":3350},[2999,6885,6886,6888],{"class":3001,"line":3376},[2999,6887,5312],{"class":3005},[2999,6889,6890],{"class":3022}," java.sql.*;\n",[2999,6892,6893,6895],{"class":3001,"line":3382},[2999,6894,5312],{"class":3005},[2999,6896,5315],{"class":3022},[2999,6898,6899,6901],{"class":3001,"line":3388},[2999,6900,5312],{"class":3005},[2999,6902,5322],{"class":3022},[2999,6904,6905,6907],{"class":3001,"line":3394},[2999,6906,5312],{"class":3005},[2999,6908,6597],{"class":3022},[2999,6910,6911],{"class":3001,"line":3399},[2999,6912,3351],{"emptyLinePlaceholder":3350},[2999,6914,6915],{"class":3001,"line":3405},[2999,6916,4559],{"class":3222},[2999,6918,6919],{"class":3001,"line":3411},[2999,6920,6921],{"class":3222}," * Абстрактний JDBC-репозиторій з підтримкою Specification Pattern.\n",[2999,6923,6924],{"class":3001,"line":3417},[2999,6925,4702],{"class":3222},[2999,6927,6928,6930,6933,6935,6938,6940,6942,6944,6946,6948,6950,6952,6954,6956,6958,6960],{"class":3001,"line":3423},[2999,6929,3006],{"class":3005},[2999,6931,6932],{"class":3005}," abstract",[2999,6934,5360],{"class":3005},[2999,6936,6937],{"class":3012}," AbstractJdbcRepository",[2999,6939,3023],{"class":3022},[2999,6941,4716],{"class":3012},[2999,6943,2975],{"class":3022},[2999,6945,6631],{"class":3012},[2999,6947,3047],{"class":3022},[2999,6949,5371],{"class":3005},[2999,6951,3019],{"class":3012},[2999,6953,3023],{"class":3022},[2999,6955,4716],{"class":3012},[2999,6957,2975],{"class":3022},[2999,6959,6631],{"class":3012},[2999,6961,3034],{"class":3022},[2999,6963,6964],{"class":3001,"line":3428},[2999,6965,3351],{"emptyLinePlaceholder":3350},[2999,6967,6968,6971,6973,6976,6979],{"class":3001,"line":3433},[2999,6969,6970],{"class":3005},"    protected",[2999,6972,5391],{"class":3005},[2999,6974,6975],{"class":3012}," ConnectionManager",[2999,6977,6978],{"class":3060}," connectionManager",[2999,6980,4102],{"class":3022},[2999,6982,6983],{"class":3001,"line":3439},[2999,6984,3351],{"emptyLinePlaceholder":3350},[2999,6986,6987,6989,6991,6993,6996,6998],{"class":3001,"line":3445},[2999,6988,6970],{"class":3005},[2999,6990,6937],{"class":3050},[2999,6992,3054],{"class":3022},[2999,6994,6995],{"class":3012},"ConnectionManager",[2999,6997,6978],{"class":3060},[2999,6999,4047],{"class":3022},[2999,7001,7002,7004,7006,7009],{"class":3001,"line":3450},[2999,7003,5465],{"class":3005},[2999,7005,3803],{"class":3022},[2999,7007,7008],{"class":3060},"connectionManager",[2999,7010,7011],{"class":3022}," = connectionManager;\n",[2999,7013,7014],{"class":3001,"line":3455},[2999,7015,3391],{"class":3022},[2999,7017,7018],{"class":3001,"line":3460},[2999,7019,3351],{"emptyLinePlaceholder":3350},[2999,7021,7022],{"class":3001,"line":3466},[2999,7023,7024],{"class":3222},"    // ... існуючі методи findById, findAll, save, update, deleteById ...\n",[2999,7026,7027],{"class":3001,"line":3472},[2999,7028,3351],{"emptyLinePlaceholder":3350},[2999,7030,7031],{"class":3001,"line":3477},[2999,7032,4727],{"class":3222},[2999,7034,7035],{"class":3001,"line":3482},[2999,7036,7037],{"class":3222},"     * Знаходить сутності за специфікацією.\n",[2999,7039,7040],{"class":3001,"line":3487},[2999,7041,4737],{"class":3222},[2999,7043,7044],{"class":3001,"line":3493},[2999,7045,7046],{"class":3222},"     * Алгоритм:\n",[2999,7048,7049],{"class":3001,"line":3499},[2999,7050,7051],{"class":3222},"     * \u003Col>\n",[2999,7053,7054,7057,7059,7061,7064],{"class":3001,"line":3505},[2999,7055,7056],{"class":3222},"     *   \u003Cli>Отримати базовий SELECT-запит від підкласу ({",[2999,7058,4875],{"class":3005},[2999,7060,4878],{"class":3222},[2999,7062,7063],{"class":3060},"getSelectAllSql()",[2999,7065,7066],{"class":3222},"});\u003C/li>\n",[2999,7068,7069],{"class":3001,"line":3510},[2999,7070,7071],{"class":3222},"     *   \u003Cli>Додати WHERE-умову зі специфікації ({@code spec.toSql()});\u003C/li>\n",[2999,7073,7074],{"class":3001,"line":3515},[2999,7075,7076],{"class":3222},"     *   \u003Cli>Встановити параметри ({@code spec.getParameters()});\u003C/li>\n",[2999,7078,7079],{"class":3001,"line":3520},[2999,7080,7081],{"class":3222},"     *   \u003Cli>Виконати запит і змаппити результати.\u003C/li>\n",[2999,7083,7084],{"class":3001,"line":3526},[2999,7085,7086],{"class":3222},"     * \u003C/ol>\n",[2999,7088,7089],{"class":3001,"line":3531},[2999,7090,4780],{"class":3222},[2999,7092,7093,7095],{"class":3001,"line":3536},[2999,7094,5496],{"class":3022},[2999,7096,5499],{"class":3012},[2999,7098,7099,7101,7103,7105,7107,7109,7111,7113,7115,7117,7119,7121,7123],{"class":3001,"line":3541},[2999,7100,5432],{"class":3005},[2999,7102,5631],{"class":3012},[2999,7104,3023],{"class":3022},[2999,7106,4716],{"class":3012},[2999,7108,3047],{"class":3022},[2999,7110,2978],{"class":3050},[2999,7112,3054],{"class":3022},[2999,7114,3267],{"class":3012},[2999,7116,3023],{"class":3022},[2999,7118,4716],{"class":3012},[2999,7120,3047],{"class":3022},[2999,7122,4230],{"class":3060},[2999,7124,4047],{"class":3022},[2999,7126,7127],{"class":3001,"line":3546},[2999,7128,7129],{"class":3222},"        // Базовий SELECT без WHERE (наприклад, \"SELECT ... FROM audiobooks ab JOIN ...\")\n",[2999,7131,7132,7135,7138,7140,7143],{"class":3001,"line":3551},[2999,7133,7134],{"class":3012},"        String",[2999,7136,7137],{"class":3060}," baseSql",[2999,7139,3702],{"class":3022},[2999,7141,7142],{"class":3050},"getSelectAllSql",[2999,7144,4859],{"class":3022},[2999,7146,7147],{"class":3001,"line":3557},[2999,7148,7149],{"class":3022},"        \n",[2999,7151,7152],{"class":3001,"line":3563},[2999,7153,7154],{"class":3222},"        // Додаємо WHERE-умову зі специфікації\n",[2999,7156,7157,7159,7161,7164,7167,7169,7171,7173,7175],{"class":3001,"line":3568},[2999,7158,7134],{"class":3012},[2999,7160,4059],{"class":3060},[2999,7162,7163],{"class":3022}," = baseSql + ",[2999,7165,7166],{"class":3714},"\" WHERE \"",[2999,7168,5241],{"class":3022},[2999,7170,4230],{"class":3060},[2999,7172,3803],{"class":3022},[2999,7174,5244],{"class":3050},[2999,7176,4859],{"class":3022},[2999,7178,7179],{"class":3001,"line":3573},[2999,7180,3351],{"emptyLinePlaceholder":3350},[2999,7182,7183,7185,7187,7189,7191,7193,7195,7197,7199],{"class":3001,"line":3578},[2999,7184,5651],{"class":3012},[2999,7186,3023],{"class":3022},[2999,7188,4716],{"class":3012},[2999,7190,3047],{"class":3022},[2999,7192,3962],{"class":3060},[2999,7194,3702],{"class":3022},[2999,7196,3706],{"class":3705},[2999,7198,5667],{"class":3012},[2999,7200,5670],{"class":3022},[2999,7202,7203],{"class":3001,"line":3584},[2999,7204,3351],{"emptyLinePlaceholder":3350},[2999,7206,7207,7210,7212,7215,7218,7220,7222,7224,7227],{"class":3001,"line":3590},[2999,7208,7209],{"class":3705},"        try",[2999,7211,5238],{"class":3022},[2999,7213,7214],{"class":3012},"Connection",[2999,7216,7217],{"class":3060}," conn",[2999,7219,3702],{"class":3022},[2999,7221,7008],{"class":3060},[2999,7223,3803],{"class":3022},[2999,7225,7226],{"class":3050},"getConnection",[2999,7228,4859],{"class":3022},[2999,7230,7231,7234,7237,7239,7242,7244,7247],{"class":3001,"line":3596},[2999,7232,7233],{"class":3012},"             PreparedStatement",[2999,7235,7236],{"class":3060}," stmt",[2999,7238,3702],{"class":3022},[2999,7240,7241],{"class":3060},"conn",[2999,7243,3803],{"class":3022},[2999,7245,7246],{"class":3050},"prepareStatement",[2999,7248,7249],{"class":3022},"(sql)) {\n",[2999,7251,7252],{"class":3001,"line":3602},[2999,7253,3351],{"emptyLinePlaceholder":3350},[2999,7255,7256],{"class":3001,"line":3608},[2999,7257,7258],{"class":3222},"            // Встановлюємо параметри зі специфікації\n",[2999,7260,7261,7264,7266,7268,7270,7272,7274,7276,7278,7280],{"class":3001,"line":3614},[2999,7262,7263],{"class":3012},"            List",[2999,7265,3023],{"class":3022},[2999,7267,4934],{"class":3012},[2999,7269,3047],{"class":3022},[2999,7271,5660],{"class":3060},[2999,7273,3702],{"class":3022},[2999,7275,4230],{"class":3060},[2999,7277,3803],{"class":3022},[2999,7279,4939],{"class":3050},[2999,7281,4859],{"class":3022},[2999,7283,7284,7287,7289,7292,7295,7297,7300,7303,7305,7307,7310],{"class":3001,"line":3619},[2999,7285,7286],{"class":3705},"            for",[2999,7288,5238],{"class":3022},[2999,7290,7291],{"class":3012},"int",[2999,7293,7294],{"class":3060}," i",[2999,7296,3702],{"class":3022},[2999,7298,7299],{"class":3769},"0",[2999,7301,7302],{"class":3022},"; i \u003C ",[2999,7304,5660],{"class":3060},[2999,7306,3803],{"class":3022},[2999,7308,7309],{"class":3050},"size",[2999,7311,7312],{"class":3022},"(); i++) {\n",[2999,7314,7315,7318,7320,7323,7326,7329,7331,7333,7335,7338],{"class":3001,"line":3625},[2999,7316,7317],{"class":3060},"                stmt",[2999,7319,3803],{"class":3022},[2999,7321,7322],{"class":3050},"setObject",[2999,7324,7325],{"class":3022},"(i + ",[2999,7327,7328],{"class":3769},"1",[2999,7330,2975],{"class":3022},[2999,7332,5660],{"class":3060},[2999,7334,3803],{"class":3022},[2999,7336,7337],{"class":3050},"get",[2999,7339,7340],{"class":3022},"(i));\n",[2999,7342,7343],{"class":3001,"line":3631},[2999,7344,7345],{"class":3022},"            }\n",[2999,7347,7348],{"class":3001,"line":3637},[2999,7349,3351],{"emptyLinePlaceholder":3350},[2999,7351,7352,7355,7357,7360,7363,7365,7368,7370,7373],{"class":3001,"line":3643},[2999,7353,7354],{"class":3705},"            try",[2999,7356,5238],{"class":3022},[2999,7358,7359],{"class":3012},"ResultSet",[2999,7361,7362],{"class":3060}," rs",[2999,7364,3702],{"class":3022},[2999,7366,7367],{"class":3060},"stmt",[2999,7369,3803],{"class":3022},[2999,7371,7372],{"class":3050},"executeQuery",[2999,7374,7375],{"class":3022},"()) {\n",[2999,7377,7378,7381,7383,7386,7388,7391],{"class":3001,"line":4891},[2999,7379,7380],{"class":3705},"                while",[2999,7382,5238],{"class":3022},[2999,7384,7385],{"class":3060},"rs",[2999,7387,3803],{"class":3022},[2999,7389,7390],{"class":3050},"next",[2999,7392,7375],{"class":3022},[2999,7394,7395,7398,7400,7403,7405,7408],{"class":3001,"line":4897},[2999,7396,7397],{"class":3060},"                    results",[2999,7399,3803],{"class":3022},[2999,7401,7402],{"class":3050},"add",[2999,7404,3054],{"class":3022},[2999,7406,7407],{"class":3050},"mapRow",[2999,7409,7410],{"class":3022},"(rs));\n",[2999,7412,7413],{"class":3001,"line":4902},[2999,7414,7415],{"class":3022},"                }\n",[2999,7417,7418],{"class":3001,"line":4912},[2999,7419,7345],{"class":3022},[2999,7421,7422],{"class":3001,"line":4917},[2999,7423,3351],{"emptyLinePlaceholder":3350},[2999,7425,7426,7429,7432,7434,7437,7440],{"class":3001,"line":4944},[2999,7427,7428],{"class":3022},"        } ",[2999,7430,7431],{"class":3705},"catch",[2999,7433,5238],{"class":3022},[2999,7435,7436],{"class":3012},"SQLException",[2999,7438,7439],{"class":3060}," e",[2999,7441,4047],{"class":3022},[2999,7443,7444,7447,7449,7452],{"class":3001,"line":4949},[2999,7445,7446],{"class":3705},"            throw",[2999,7448,5030],{"class":3705},[2999,7450,7451],{"class":3050}," DatabaseException",[2999,7453,3795],{"class":3022},[2999,7455,7456,7459,7461,7464],{"class":3001,"line":4954},[2999,7457,7458],{"class":3714},"                \"Помилка findAll(Specification) для таблиці \"",[2999,7460,5241],{"class":3022},[2999,7462,7463],{"class":3050},"getTableName",[2999,7465,7466],{"class":3022},"(), e);\n",[2999,7468,7469],{"class":3001,"line":4960},[2999,7470,7471],{"class":3022},"        }\n",[2999,7473,7474],{"class":3001,"line":4965},[2999,7475,3351],{"emptyLinePlaceholder":3350},[2999,7477,7478,7480],{"class":3001,"line":4978},[2999,7479,5027],{"class":3705},[2999,7481,7482],{"class":3022}," results;\n",[2999,7484,7485],{"class":3001,"line":4988},[2999,7486,3391],{"class":3022},[2999,7488,7489],{"class":3001,"line":4993},[2999,7490,3351],{"emptyLinePlaceholder":3350},[2999,7492,7493],{"class":3001,"line":5024},[2999,7494,7495],{"class":3222},"    // Абстрактні методи, що підкласи зобов'язані реалізувати\n",[2999,7497,7498,7500,7502,7505,7508,7510,7512,7514,7516,7519,7522],{"class":3001,"line":5045},[2999,7499,6970],{"class":3005},[2999,7501,6932],{"class":3005},[2999,7503,7504],{"class":3012}," T",[2999,7506,7507],{"class":3050}," mapRow",[2999,7509,3054],{"class":3022},[2999,7511,7359],{"class":3012},[2999,7513,7362],{"class":3060},[2999,7515,6486],{"class":3022},[2999,7517,7518],{"class":3005},"throws",[2999,7520,7521],{"class":3012}," SQLException",[2999,7523,4102],{"class":3022},[2999,7525,7526,7528,7530,7532,7535],{"class":3001,"line":5050},[2999,7527,6970],{"class":3005},[2999,7529,6932],{"class":3005},[2999,7531,5565],{"class":3012},[2999,7533,7534],{"class":3050}," getTableName",[2999,7536,4859],{"class":3022},[2999,7538,7539,7541,7543,7545,7548],{"class":3001,"line":5055},[2999,7540,6970],{"class":3005},[2999,7542,6932],{"class":3005},[2999,7544,5565],{"class":3012},[2999,7546,7547],{"class":3050}," getSelectAllSql",[2999,7549,4859],{"class":3022},[2999,7551,7552],{"class":3001,"line":5060},[2999,7553,7554],{"class":3222},"    // ... інші абстрактні методи\n",[2999,7556,7557],{"class":3001,"line":5066},[2999,7558,3101],{"class":3022},[2964,7560,7561],{},[3228,7562,7563],{},"Ключові моменти реалізації:",[4397,7565,7566,7584,7594],{},[4400,7567,7568,5238,7571,7574,7575,7578,7579,7581,7582,3803],{},[3228,7569,7570],{},"Рядок 42",[2968,7572,7573],{},"baseSql + \" WHERE \" + spec.toSql()","): базовий SELECT доповнюється WHERE-умовою. Якщо базовий запит вже містить WHERE (наприклад, для soft-delete: ",[2968,7576,7577],{},"WHERE deleted_at IS NULL","), потрібно використовувати ",[2968,7580,3319],{}," замість ",[2968,7583,4466],{},[4400,7585,7586,7589,7590,7593],{},[3228,7587,7588],{},"Рядки 49–52"," (встановлення параметрів): параметри зі специфікації передаються у ",[2968,7591,7592],{},"PreparedStatement"," у порядку появи. Це забезпечує безпеку від SQL-ін'єкцій.",[4400,7595,7596,5238,7599,7602],{},[3228,7597,7598],{},"Рядок 55",[2968,7600,7601],{},"mapRow(rs)","): маппінг залишається незмінним — специфікація не впливає на структуру ResultSet, лише на кількість рядків.",[3278,7604],{},[2959,7606,7608],{"id":7607},"конкретні-специфікації-для-audiobook-на-основі-реальної-схеми","Конкретні специфікації для Audiobook (на основі реальної схеми)",[2964,7610,7611,7612,7614,7615,7618,7619,6557],{},"Тепер реалізуємо прості (листкові) специфікації для доменної моделі ",[2968,7613,3694],{},", використовуючи ",[3228,7616,7617],{},"реальні поля зі схеми БД",". Нагадаємо структуру таблиці ",[2968,7620,7621],{},"audiobooks",[2990,7623,7625],{"className":6407,"code":7624,"language":6409,"meta":2995,"style":2995},"CREATE TABLE audiobooks (\n    id               UUID PRIMARY KEY,\n    author_id        UUID NOT NULL REFERENCES authors(id),\n    genre_id         UUID NOT NULL REFERENCES genres(id),\n    title            VARCHAR(255) NOT NULL,\n    duration         INTEGER NOT NULL CHECK (duration > 0),\n    release_year     INTEGER NOT NULL CHECK (release_year >= 1900),\n    description      TEXT,\n    cover_image_path VARCHAR(2048)\n);\n",[2968,7626,7627,7641,7652,7666,7678,7697,7719,7738,7748,7762],{"__ignoreMap":2995},[2999,7628,7629,7632,7635,7638],{"class":3001,"line":3002},[2999,7630,7631],{"class":3005},"CREATE",[2999,7633,7634],{"class":3005}," TABLE",[2999,7636,7637],{"class":3050}," audiobooks",[2999,7639,7640],{"class":3022}," (\n",[2999,7642,7643,7646,7649],{"class":3001,"line":3037},[2999,7644,7645],{"class":3022},"    id               UUID ",[2999,7647,7648],{"class":3005},"PRIMARY KEY",[2999,7650,7651],{"class":3022},",\n",[2999,7653,7654,7657,7660,7663],{"class":3001,"line":3067},[2999,7655,7656],{"class":3022},"    author_id        UUID ",[2999,7658,7659],{"class":3005},"NOT NULL",[2999,7661,7662],{"class":3005}," REFERENCES",[2999,7664,7665],{"class":3022}," authors(id),\n",[2999,7667,7668,7671,7673,7675],{"class":3001,"line":3098},[2999,7669,7670],{"class":3022},"    genre_id         UUID ",[2999,7672,7659],{"class":3005},[2999,7674,7662],{"class":3005},[2999,7676,7677],{"class":3022}," genres(id),\n",[2999,7679,7680,7683,7686,7688,7691,7693,7695],{"class":3001,"line":3219},[2999,7681,7682],{"class":3022},"    title            ",[2999,7684,7685],{"class":3005},"VARCHAR",[2999,7687,3054],{"class":3022},[2999,7689,7690],{"class":3769},"255",[2999,7692,6486],{"class":3022},[2999,7694,7659],{"class":3005},[2999,7696,7651],{"class":3022},[2999,7698,7699,7702,7705,7708,7711,7714,7716],{"class":3001,"line":3364},[2999,7700,7701],{"class":3022},"    duration         ",[2999,7703,7704],{"class":3005},"INTEGER",[2999,7706,7707],{"class":3005}," NOT NULL",[2999,7709,7710],{"class":3005}," CHECK",[2999,7712,7713],{"class":3022}," (duration > ",[2999,7715,7299],{"class":3769},[2999,7717,7718],{"class":3022},"),\n",[2999,7720,7721,7724,7726,7728,7730,7733,7736],{"class":3001,"line":3370},[2999,7722,7723],{"class":3022},"    release_year     ",[2999,7725,7704],{"class":3005},[2999,7727,7707],{"class":3005},[2999,7729,7710],{"class":3005},[2999,7731,7732],{"class":3022}," (release_year >= ",[2999,7734,7735],{"class":3769},"1900",[2999,7737,7718],{"class":3022},[2999,7739,7740,7743,7746],{"class":3001,"line":3376},[2999,7741,7742],{"class":3005},"    description",[2999,7744,7745],{"class":3005},"      TEXT",[2999,7747,7651],{"class":3022},[2999,7749,7750,7753,7755,7757,7760],{"class":3001,"line":3382},[2999,7751,7752],{"class":3022},"    cover_image_path ",[2999,7754,7685],{"class":3005},[2999,7756,3054],{"class":3022},[2999,7758,7759],{"class":3769},"2048",[2999,7761,3830],{"class":3022},[2999,7763,7764],{"class":3001,"line":3388},[2999,7765,3064],{"class":3022},[2964,7767,7768,7769,2975,7772,2975,7775,2975,7778,3803],{},"Ключові поля для специфікацій: ",[2968,7770,7771],{},"duration",[2968,7773,7774],{},"release_year",[2968,7776,7777],{},"genre_id",[2968,7779,7780],{},"title",[3285,7782,7784],{"id":7783},"durationrangespecification","DurationRangeSpecification",[2990,7786,7788],{"className":2992,"code":7787,"language":2994,"meta":4540,"style":2995},"package com.example.audiobook.specification.audiobook;\n\nimport com.example.audiobook.domain.Audiobook;\nimport com.example.audiobook.specification.Specification;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Специфікація: аудіокнига з тривалістю у заданому діапазоні (у секундах).\n * \u003Cp>\n * SQL: {@code duration BETWEEN ? AND ?} або {@code duration >= ?} / {@code duration \u003C= ?}.\n */\npublic class DurationRangeSpecification implements Specification\u003CAudiobook> {\n\n    private final Integer minDuration; // у секундах\n    private final Integer maxDuration; // у секундах\n\n    /**\n     * @param minDuration мінімальна тривалість (включно), може бути {@code null} — без нижньої межі\n     * @param maxDuration максимальна тривалість (включно), може бути {@code null} — без верхньої межі\n     */\n    public DurationRangeSpecification(Integer minDuration, Integer maxDuration) {\n        this.minDuration = minDuration;\n        this.maxDuration = maxDuration;\n    }\n\n    @Override\n    public boolean isSatisfiedBy(Audiobook candidate) {\n        Integer duration = candidate.getDuration();\n        if (duration == null) return false;\n\n        boolean satisfiesMin = (minDuration == null) || duration >= minDuration;\n        boolean satisfiesMax = (maxDuration == null) || duration \u003C= maxDuration;\n\n        return satisfiesMin && satisfiesMax;\n    }\n\n    @Override\n    public String toSql() {\n        if (minDuration != null && maxDuration != null) {\n            return \"ab.duration BETWEEN ? AND ?\";\n        } else if (minDuration != null) {\n            return \"ab.duration >= ?\";\n        } else if (maxDuration != null) {\n            return \"ab.duration \u003C= ?\";\n        } else {\n            return \"1=1\"; // завжди true — немає обмежень\n        }\n    }\n\n    @Override\n    public List\u003CObject> getParameters() {\n        List\u003CObject> params = new ArrayList\u003C>();\n        if (minDuration != null) params.add(minDuration);\n        if (maxDuration != null) params.add(maxDuration);\n        return params;\n    }\n}\n",[2968,7789,7790,7797,7801,7808,7814,7818,7824,7830,7834,7838,7843,7847,7852,7856,7876,7880,7898,7913,7917,7921,7932,7943,7947,7967,7979,7991,7995,7999,8005,8021,8041,8062,8066,8082,8097,8101,8108,8112,8116,8122,8132,8148,8158,8174,8183,8198,8207,8216,8228,8232,8236,8240,8246,8262,8282,8301,8320,8326,8330],{"__ignoreMap":2995},[2999,7791,7792,7794],{"class":3001,"line":3002},[2999,7793,4547],{"class":3005},[2999,7795,7796],{"class":3022}," com.example.audiobook.specification.audiobook;\n",[2999,7798,7799],{"class":3001,"line":3037},[2999,7800,3351],{"emptyLinePlaceholder":3350},[2999,7802,7803,7805],{"class":3001,"line":3067},[2999,7804,5312],{"class":3005},[2999,7806,7807],{"class":3022}," com.example.audiobook.domain.Audiobook;\n",[2999,7809,7810,7812],{"class":3001,"line":3098},[2999,7811,5312],{"class":3005},[2999,7813,6580],{"class":3022},[2999,7815,7816],{"class":3001,"line":3219},[2999,7817,3351],{"emptyLinePlaceholder":3350},[2999,7819,7820,7822],{"class":3001,"line":3364},[2999,7821,5312],{"class":3005},[2999,7823,5315],{"class":3022},[2999,7825,7826,7828],{"class":3001,"line":3370},[2999,7827,5312],{"class":3005},[2999,7829,5322],{"class":3022},[2999,7831,7832],{"class":3001,"line":3376},[2999,7833,3351],{"emptyLinePlaceholder":3350},[2999,7835,7836],{"class":3001,"line":3382},[2999,7837,4559],{"class":3222},[2999,7839,7840],{"class":3001,"line":3388},[2999,7841,7842],{"class":3222}," * Специфікація: аудіокнига з тривалістю у заданому діапазоні (у секундах).\n",[2999,7844,7845],{"class":3001,"line":3394},[2999,7846,4569],{"class":3222},[2999,7848,7849],{"class":3001,"line":3399},[2999,7850,7851],{"class":3222}," * SQL: {@code duration BETWEEN ? AND ?} або {@code duration >= ?} / {@code duration \u003C= ?}.\n",[2999,7853,7854],{"class":3001,"line":3405},[2999,7855,4702],{"class":3222},[2999,7857,7858,7860,7862,7865,7868,7870,7872,7874],{"class":3001,"line":3411},[2999,7859,3006],{"class":3005},[2999,7861,5360],{"class":3005},[2999,7863,7864],{"class":3012}," DurationRangeSpecification",[2999,7866,7867],{"class":3005}," implements",[2999,7869,4711],{"class":3012},[2999,7871,3023],{"class":3022},[2999,7873,3694],{"class":3012},[2999,7875,3034],{"class":3022},[2999,7877,7878],{"class":3001,"line":3417},[2999,7879,3351],{"emptyLinePlaceholder":3350},[2999,7881,7882,7884,7886,7889,7892,7895],{"class":3001,"line":3423},[2999,7883,5388],{"class":3005},[2999,7885,5391],{"class":3005},[2999,7887,7888],{"class":3012}," Integer",[2999,7890,7891],{"class":3060}," minDuration",[2999,7893,7894],{"class":3022},"; ",[2999,7896,7897],{"class":3222},"// у секундах\n",[2999,7899,7900,7902,7904,7906,7909,7911],{"class":3001,"line":3428},[2999,7901,5388],{"class":3005},[2999,7903,5391],{"class":3005},[2999,7905,7888],{"class":3012},[2999,7907,7908],{"class":3060}," maxDuration",[2999,7910,7894],{"class":3022},[2999,7912,7897],{"class":3222},[2999,7914,7915],{"class":3001,"line":3433},[2999,7916,3351],{"emptyLinePlaceholder":3350},[2999,7918,7919],{"class":3001,"line":3439},[2999,7920,4727],{"class":3222},[2999,7922,7923,7925,7927,7929],{"class":3001,"line":3445},[2999,7924,4757],{"class":3222},[2999,7926,4691],{"class":3005},[2999,7928,7891],{"class":3060},[2999,7930,7931],{"class":3222}," мінімальна тривалість (включно), може бути {@code null} — без нижньої межі\n",[2999,7933,7934,7936,7938,7940],{"class":3001,"line":3450},[2999,7935,4757],{"class":3222},[2999,7937,4691],{"class":3005},[2999,7939,7908],{"class":3060},[2999,7941,7942],{"class":3222}," максимальна тривалість (включно), може бути {@code null} — без верхньої межі\n",[2999,7944,7945],{"class":3001,"line":3455},[2999,7946,4780],{"class":3222},[2999,7948,7949,7951,7953,7955,7957,7959,7961,7963,7965],{"class":3001,"line":3460},[2999,7950,5432],{"class":3005},[2999,7952,7864],{"class":3050},[2999,7954,3054],{"class":3022},[2999,7956,3168],{"class":3012},[2999,7958,7891],{"class":3060},[2999,7960,2975],{"class":3022},[2999,7962,3168],{"class":3012},[2999,7964,7908],{"class":3060},[2999,7966,4047],{"class":3022},[2999,7968,7969,7971,7973,7976],{"class":3001,"line":3466},[2999,7970,5465],{"class":3005},[2999,7972,3803],{"class":3022},[2999,7974,7975],{"class":3060},"minDuration",[2999,7977,7978],{"class":3022}," = minDuration;\n",[2999,7980,7981,7983,7985,7988],{"class":3001,"line":3472},[2999,7982,5465],{"class":3005},[2999,7984,3803],{"class":3022},[2999,7986,7987],{"class":3060},"maxDuration",[2999,7989,7990],{"class":3022}," = maxDuration;\n",[2999,7992,7993],{"class":3001,"line":3477},[2999,7994,3391],{"class":3022},[2999,7996,7997],{"class":3001,"line":3482},[2999,7998,3351],{"emptyLinePlaceholder":3350},[2999,8000,8001,8003],{"class":3001,"line":3487},[2999,8002,5496],{"class":3022},[2999,8004,5499],{"class":3012},[2999,8006,8007,8009,8011,8013,8015,8017,8019],{"class":3001,"line":3493},[2999,8008,5432],{"class":3005},[2999,8010,5506],{"class":3012},[2999,8012,4788],{"class":3050},[2999,8014,3054],{"class":3022},[2999,8016,3694],{"class":3012},[2999,8018,4762],{"class":3060},[2999,8020,4047],{"class":3022},[2999,8022,8023,8026,8029,8031,8034,8036,8039],{"class":3001,"line":3499},[2999,8024,8025],{"class":3012},"        Integer",[2999,8027,8028],{"class":3060}," duration",[2999,8030,3702],{"class":3022},[2999,8032,8033],{"class":3060},"candidate",[2999,8035,3803],{"class":3022},[2999,8037,8038],{"class":3050},"getDuration",[2999,8040,4859],{"class":3022},[2999,8042,8043,8046,8049,8052,8054,8057,8060],{"class":3001,"line":3505},[2999,8044,8045],{"class":3705},"        if",[2999,8047,8048],{"class":3022}," (duration == ",[2999,8050,8051],{"class":3005},"null",[2999,8053,6486],{"class":3022},[2999,8055,8056],{"class":3705},"return",[2999,8058,8059],{"class":3005}," false",[2999,8061,4102],{"class":3022},[2999,8063,8064],{"class":3001,"line":3510},[2999,8065,3351],{"emptyLinePlaceholder":3350},[2999,8067,8068,8071,8074,8077,8079],{"class":3001,"line":3515},[2999,8069,8070],{"class":3012},"        boolean",[2999,8072,8073],{"class":3060}," satisfiesMin",[2999,8075,8076],{"class":3022}," = (minDuration == ",[2999,8078,8051],{"class":3005},[2999,8080,8081],{"class":3022},") || duration >= minDuration;\n",[2999,8083,8084,8086,8089,8092,8094],{"class":3001,"line":3520},[2999,8085,8070],{"class":3012},[2999,8087,8088],{"class":3060}," satisfiesMax",[2999,8090,8091],{"class":3022}," = (maxDuration == ",[2999,8093,8051],{"class":3005},[2999,8095,8096],{"class":3022},") || duration \u003C= maxDuration;\n",[2999,8098,8099],{"class":3001,"line":3526},[2999,8100,3351],{"emptyLinePlaceholder":3350},[2999,8102,8103,8105],{"class":3001,"line":3531},[2999,8104,5027],{"class":3705},[2999,8106,8107],{"class":3022}," satisfiesMin && satisfiesMax;\n",[2999,8109,8110],{"class":3001,"line":3536},[2999,8111,3391],{"class":3022},[2999,8113,8114],{"class":3001,"line":3541},[2999,8115,3351],{"emptyLinePlaceholder":3350},[2999,8117,8118,8120],{"class":3001,"line":3546},[2999,8119,5496],{"class":3022},[2999,8121,5499],{"class":3012},[2999,8123,8124,8126,8128,8130],{"class":3001,"line":3551},[2999,8125,5432],{"class":3005},[2999,8127,5565],{"class":3012},[2999,8129,4856],{"class":3050},[2999,8131,5199],{"class":3022},[2999,8133,8134,8136,8139,8141,8144,8146],{"class":3001,"line":3557},[2999,8135,8045],{"class":3705},[2999,8137,8138],{"class":3022}," (minDuration != ",[2999,8140,8051],{"class":3005},[2999,8142,8143],{"class":3022}," && maxDuration != ",[2999,8145,8051],{"class":3005},[2999,8147,4047],{"class":3022},[2999,8149,8150,8153,8156],{"class":3001,"line":3563},[2999,8151,8152],{"class":3705},"            return",[2999,8154,8155],{"class":3714}," \"ab.duration BETWEEN ? AND ?\"",[2999,8157,4102],{"class":3022},[2999,8159,8160,8162,8165,8168,8170,8172],{"class":3001,"line":3568},[2999,8161,7428],{"class":3022},[2999,8163,8164],{"class":3705},"else",[2999,8166,8167],{"class":3705}," if",[2999,8169,8138],{"class":3022},[2999,8171,8051],{"class":3005},[2999,8173,4047],{"class":3022},[2999,8175,8176,8178,8181],{"class":3001,"line":3573},[2999,8177,8152],{"class":3705},[2999,8179,8180],{"class":3714}," \"ab.duration >= ?\"",[2999,8182,4102],{"class":3022},[2999,8184,8185,8187,8189,8191,8194,8196],{"class":3001,"line":3578},[2999,8186,7428],{"class":3022},[2999,8188,8164],{"class":3705},[2999,8190,8167],{"class":3705},[2999,8192,8193],{"class":3022}," (maxDuration != ",[2999,8195,8051],{"class":3005},[2999,8197,4047],{"class":3022},[2999,8199,8200,8202,8205],{"class":3001,"line":3584},[2999,8201,8152],{"class":3705},[2999,8203,8204],{"class":3714}," \"ab.duration \u003C= ?\"",[2999,8206,4102],{"class":3022},[2999,8208,8209,8211,8213],{"class":3001,"line":3590},[2999,8210,7428],{"class":3022},[2999,8212,8164],{"class":3705},[2999,8214,8215],{"class":3022}," {\n",[2999,8217,8218,8220,8223,8225],{"class":3001,"line":3596},[2999,8219,8152],{"class":3705},[2999,8221,8222],{"class":3714}," \"1=1\"",[2999,8224,7894],{"class":3022},[2999,8226,8227],{"class":3222},"// завжди true — немає обмежень\n",[2999,8229,8230],{"class":3001,"line":3602},[2999,8231,7471],{"class":3022},[2999,8233,8234],{"class":3001,"line":3608},[2999,8235,3391],{"class":3022},[2999,8237,8238],{"class":3001,"line":3614},[2999,8239,3351],{"emptyLinePlaceholder":3350},[2999,8241,8242,8244],{"class":3001,"line":3619},[2999,8243,5496],{"class":3022},[2999,8245,5499],{"class":3012},[2999,8247,8248,8250,8252,8254,8256,8258,8260],{"class":3001,"line":3625},[2999,8249,5432],{"class":3005},[2999,8251,5631],{"class":3012},[2999,8253,3023],{"class":3022},[2999,8255,4934],{"class":3012},[2999,8257,3047],{"class":3022},[2999,8259,4939],{"class":3050},[2999,8261,5199],{"class":3022},[2999,8263,8264,8266,8268,8270,8272,8274,8276,8278,8280],{"class":3001,"line":3631},[2999,8265,5651],{"class":3012},[2999,8267,3023],{"class":3022},[2999,8269,4934],{"class":3012},[2999,8271,3047],{"class":3022},[2999,8273,5660],{"class":3060},[2999,8275,3702],{"class":3022},[2999,8277,3706],{"class":3705},[2999,8279,5667],{"class":3012},[2999,8281,5670],{"class":3022},[2999,8283,8284,8286,8288,8290,8292,8294,8296,8298],{"class":3001,"line":3637},[2999,8285,8045],{"class":3705},[2999,8287,8138],{"class":3022},[2999,8289,8051],{"class":3005},[2999,8291,6486],{"class":3022},[2999,8293,5660],{"class":3060},[2999,8295,3803],{"class":3022},[2999,8297,7402],{"class":3050},[2999,8299,8300],{"class":3022},"(minDuration);\n",[2999,8302,8303,8305,8307,8309,8311,8313,8315,8317],{"class":3001,"line":3643},[2999,8304,8045],{"class":3705},[2999,8306,8193],{"class":3022},[2999,8308,8051],{"class":3005},[2999,8310,6486],{"class":3022},[2999,8312,5660],{"class":3060},[2999,8314,3803],{"class":3022},[2999,8316,7402],{"class":3050},[2999,8318,8319],{"class":3022},"(maxDuration);\n",[2999,8321,8322,8324],{"class":3001,"line":4891},[2999,8323,5027],{"class":3705},[2999,8325,5716],{"class":3022},[2999,8327,8328],{"class":3001,"line":4897},[2999,8329,3391],{"class":3022},[2999,8331,8332],{"class":3001,"line":4902},[2999,8333,3101],{"class":3022},[3285,8335,8337],{"id":8336},"yearrangespecification","YearRangeSpecification",[2990,8339,8341],{"className":2992,"code":8340,"language":2994,"meta":4540,"style":2995},"package com.example.audiobook.specification.audiobook;\n\nimport com.example.audiobook.domain.Audiobook;\nimport com.example.audiobook.specification.Specification;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Специфікація: аудіокнига опублікована у заданому діапазоні років.\n * \u003Cp>\n * SQL: {@code release_year BETWEEN ? AND ?}.\n */\npublic class YearRangeSpecification implements Specification\u003CAudiobook> {\n\n    private final Integer minYear;\n    private final Integer maxYear;\n\n    /**\n     * @param minYear мінімальний рік (включно), може бути {@code null}\n     * @param maxYear максимальний рік (включно), може бути {@code null}\n     */\n    public YearRangeSpecification(Integer minYear, Integer maxYear) {\n        this.minYear = minYear;\n        this.maxYear = maxYear;\n    }\n\n    @Override\n    public boolean isSatisfiedBy(Audiobook candidate) {\n        Integer year = candidate.getReleaseYear();\n        if (year == null) return false;\n\n        boolean satisfiesMin = (minYear == null) || year >= minYear;\n        boolean satisfiesMax = (maxYear == null) || year \u003C= maxYear;\n\n        return satisfiesMin && satisfiesMax;\n    }\n\n    @Override\n    public String toSql() {\n        if (minYear != null && maxYear != null) {\n            return \"ab.release_year BETWEEN ? AND ?\";\n        } else if (minYear != null) {\n            return \"ab.release_year >= ?\";\n        } else if (maxYear != null) {\n            return \"ab.release_year \u003C= ?\";\n        } else {\n            return \"1=1\";\n        }\n    }\n\n    @Override\n    public List\u003CObject> getParameters() {\n        List\u003CObject> params = new ArrayList\u003C>();\n        if (minYear != null) params.add(minYear);\n        if (maxYear != null) params.add(maxYear);\n        return params;\n    }\n}\n",[2968,8342,8343,8349,8353,8359,8365,8369,8375,8381,8385,8389,8394,8398,8403,8407,8426,8430,8443,8456,8460,8464,8475,8486,8490,8510,8522,8534,8538,8542,8548,8564,8581,8598,8602,8616,8630,8634,8640,8644,8648,8654,8664,8680,8689,8703,8712,8727,8736,8744,8752,8756,8760,8764,8770,8786,8806,8825,8844,8850,8854],{"__ignoreMap":2995},[2999,8344,8345,8347],{"class":3001,"line":3002},[2999,8346,4547],{"class":3005},[2999,8348,7796],{"class":3022},[2999,8350,8351],{"class":3001,"line":3037},[2999,8352,3351],{"emptyLinePlaceholder":3350},[2999,8354,8355,8357],{"class":3001,"line":3067},[2999,8356,5312],{"class":3005},[2999,8358,7807],{"class":3022},[2999,8360,8361,8363],{"class":3001,"line":3098},[2999,8362,5312],{"class":3005},[2999,8364,6580],{"class":3022},[2999,8366,8367],{"class":3001,"line":3219},[2999,8368,3351],{"emptyLinePlaceholder":3350},[2999,8370,8371,8373],{"class":3001,"line":3364},[2999,8372,5312],{"class":3005},[2999,8374,5315],{"class":3022},[2999,8376,8377,8379],{"class":3001,"line":3370},[2999,8378,5312],{"class":3005},[2999,8380,5322],{"class":3022},[2999,8382,8383],{"class":3001,"line":3376},[2999,8384,3351],{"emptyLinePlaceholder":3350},[2999,8386,8387],{"class":3001,"line":3382},[2999,8388,4559],{"class":3222},[2999,8390,8391],{"class":3001,"line":3388},[2999,8392,8393],{"class":3222}," * Специфікація: аудіокнига опублікована у заданому діапазоні років.\n",[2999,8395,8396],{"class":3001,"line":3394},[2999,8397,4569],{"class":3222},[2999,8399,8400],{"class":3001,"line":3399},[2999,8401,8402],{"class":3222}," * SQL: {@code release_year BETWEEN ? AND ?}.\n",[2999,8404,8405],{"class":3001,"line":3405},[2999,8406,4702],{"class":3222},[2999,8408,8409,8411,8413,8416,8418,8420,8422,8424],{"class":3001,"line":3411},[2999,8410,3006],{"class":3005},[2999,8412,5360],{"class":3005},[2999,8414,8415],{"class":3012}," YearRangeSpecification",[2999,8417,7867],{"class":3005},[2999,8419,4711],{"class":3012},[2999,8421,3023],{"class":3022},[2999,8423,3694],{"class":3012},[2999,8425,3034],{"class":3022},[2999,8427,8428],{"class":3001,"line":3417},[2999,8429,3351],{"emptyLinePlaceholder":3350},[2999,8431,8432,8434,8436,8438,8441],{"class":3001,"line":3423},[2999,8433,5388],{"class":3005},[2999,8435,5391],{"class":3005},[2999,8437,7888],{"class":3012},[2999,8439,8440],{"class":3060}," minYear",[2999,8442,4102],{"class":3022},[2999,8444,8445,8447,8449,8451,8454],{"class":3001,"line":3428},[2999,8446,5388],{"class":3005},[2999,8448,5391],{"class":3005},[2999,8450,7888],{"class":3012},[2999,8452,8453],{"class":3060}," maxYear",[2999,8455,4102],{"class":3022},[2999,8457,8458],{"class":3001,"line":3433},[2999,8459,3351],{"emptyLinePlaceholder":3350},[2999,8461,8462],{"class":3001,"line":3439},[2999,8463,4727],{"class":3222},[2999,8465,8466,8468,8470,8472],{"class":3001,"line":3445},[2999,8467,4757],{"class":3222},[2999,8469,4691],{"class":3005},[2999,8471,8440],{"class":3060},[2999,8473,8474],{"class":3222}," мінімальний рік (включно), може бути {@code null}\n",[2999,8476,8477,8479,8481,8483],{"class":3001,"line":3450},[2999,8478,4757],{"class":3222},[2999,8480,4691],{"class":3005},[2999,8482,8453],{"class":3060},[2999,8484,8485],{"class":3222}," максимальний рік (включно), може бути {@code null}\n",[2999,8487,8488],{"class":3001,"line":3455},[2999,8489,4780],{"class":3222},[2999,8491,8492,8494,8496,8498,8500,8502,8504,8506,8508],{"class":3001,"line":3460},[2999,8493,5432],{"class":3005},[2999,8495,8415],{"class":3050},[2999,8497,3054],{"class":3022},[2999,8499,3168],{"class":3012},[2999,8501,8440],{"class":3060},[2999,8503,2975],{"class":3022},[2999,8505,3168],{"class":3012},[2999,8507,8453],{"class":3060},[2999,8509,4047],{"class":3022},[2999,8511,8512,8514,8516,8519],{"class":3001,"line":3466},[2999,8513,5465],{"class":3005},[2999,8515,3803],{"class":3022},[2999,8517,8518],{"class":3060},"minYear",[2999,8520,8521],{"class":3022}," = minYear;\n",[2999,8523,8524,8526,8528,8531],{"class":3001,"line":3472},[2999,8525,5465],{"class":3005},[2999,8527,3803],{"class":3022},[2999,8529,8530],{"class":3060},"maxYear",[2999,8532,8533],{"class":3022}," = maxYear;\n",[2999,8535,8536],{"class":3001,"line":3477},[2999,8537,3391],{"class":3022},[2999,8539,8540],{"class":3001,"line":3482},[2999,8541,3351],{"emptyLinePlaceholder":3350},[2999,8543,8544,8546],{"class":3001,"line":3487},[2999,8545,5496],{"class":3022},[2999,8547,5499],{"class":3012},[2999,8549,8550,8552,8554,8556,8558,8560,8562],{"class":3001,"line":3493},[2999,8551,5432],{"class":3005},[2999,8553,5506],{"class":3012},[2999,8555,4788],{"class":3050},[2999,8557,3054],{"class":3022},[2999,8559,3694],{"class":3012},[2999,8561,4762],{"class":3060},[2999,8563,4047],{"class":3022},[2999,8565,8566,8568,8570,8572,8574,8576,8579],{"class":3001,"line":3499},[2999,8567,8025],{"class":3012},[2999,8569,6453],{"class":3060},[2999,8571,3702],{"class":3022},[2999,8573,8033],{"class":3060},[2999,8575,3803],{"class":3022},[2999,8577,8578],{"class":3050},"getReleaseYear",[2999,8580,4859],{"class":3022},[2999,8582,8583,8585,8588,8590,8592,8594,8596],{"class":3001,"line":3505},[2999,8584,8045],{"class":3705},[2999,8586,8587],{"class":3022}," (year == ",[2999,8589,8051],{"class":3005},[2999,8591,6486],{"class":3022},[2999,8593,8056],{"class":3705},[2999,8595,8059],{"class":3005},[2999,8597,4102],{"class":3022},[2999,8599,8600],{"class":3001,"line":3510},[2999,8601,3351],{"emptyLinePlaceholder":3350},[2999,8603,8604,8606,8608,8611,8613],{"class":3001,"line":3515},[2999,8605,8070],{"class":3012},[2999,8607,8073],{"class":3060},[2999,8609,8610],{"class":3022}," = (minYear == ",[2999,8612,8051],{"class":3005},[2999,8614,8615],{"class":3022},") || year >= minYear;\n",[2999,8617,8618,8620,8622,8625,8627],{"class":3001,"line":3520},[2999,8619,8070],{"class":3012},[2999,8621,8088],{"class":3060},[2999,8623,8624],{"class":3022}," = (maxYear == ",[2999,8626,8051],{"class":3005},[2999,8628,8629],{"class":3022},") || year \u003C= maxYear;\n",[2999,8631,8632],{"class":3001,"line":3526},[2999,8633,3351],{"emptyLinePlaceholder":3350},[2999,8635,8636,8638],{"class":3001,"line":3531},[2999,8637,5027],{"class":3705},[2999,8639,8107],{"class":3022},[2999,8641,8642],{"class":3001,"line":3536},[2999,8643,3391],{"class":3022},[2999,8645,8646],{"class":3001,"line":3541},[2999,8647,3351],{"emptyLinePlaceholder":3350},[2999,8649,8650,8652],{"class":3001,"line":3546},[2999,8651,5496],{"class":3022},[2999,8653,5499],{"class":3012},[2999,8655,8656,8658,8660,8662],{"class":3001,"line":3551},[2999,8657,5432],{"class":3005},[2999,8659,5565],{"class":3012},[2999,8661,4856],{"class":3050},[2999,8663,5199],{"class":3022},[2999,8665,8666,8668,8671,8673,8676,8678],{"class":3001,"line":3557},[2999,8667,8045],{"class":3705},[2999,8669,8670],{"class":3022}," (minYear != ",[2999,8672,8051],{"class":3005},[2999,8674,8675],{"class":3022}," && maxYear != ",[2999,8677,8051],{"class":3005},[2999,8679,4047],{"class":3022},[2999,8681,8682,8684,8687],{"class":3001,"line":3563},[2999,8683,8152],{"class":3705},[2999,8685,8686],{"class":3714}," \"ab.release_year BETWEEN ? AND ?\"",[2999,8688,4102],{"class":3022},[2999,8690,8691,8693,8695,8697,8699,8701],{"class":3001,"line":3568},[2999,8692,7428],{"class":3022},[2999,8694,8164],{"class":3705},[2999,8696,8167],{"class":3705},[2999,8698,8670],{"class":3022},[2999,8700,8051],{"class":3005},[2999,8702,4047],{"class":3022},[2999,8704,8705,8707,8710],{"class":3001,"line":3573},[2999,8706,8152],{"class":3705},[2999,8708,8709],{"class":3714}," \"ab.release_year >= ?\"",[2999,8711,4102],{"class":3022},[2999,8713,8714,8716,8718,8720,8723,8725],{"class":3001,"line":3578},[2999,8715,7428],{"class":3022},[2999,8717,8164],{"class":3705},[2999,8719,8167],{"class":3705},[2999,8721,8722],{"class":3022}," (maxYear != ",[2999,8724,8051],{"class":3005},[2999,8726,4047],{"class":3022},[2999,8728,8729,8731,8734],{"class":3001,"line":3584},[2999,8730,8152],{"class":3705},[2999,8732,8733],{"class":3714}," \"ab.release_year \u003C= ?\"",[2999,8735,4102],{"class":3022},[2999,8737,8738,8740,8742],{"class":3001,"line":3590},[2999,8739,7428],{"class":3022},[2999,8741,8164],{"class":3705},[2999,8743,8215],{"class":3022},[2999,8745,8746,8748,8750],{"class":3001,"line":3596},[2999,8747,8152],{"class":3705},[2999,8749,8222],{"class":3714},[2999,8751,4102],{"class":3022},[2999,8753,8754],{"class":3001,"line":3602},[2999,8755,7471],{"class":3022},[2999,8757,8758],{"class":3001,"line":3608},[2999,8759,3391],{"class":3022},[2999,8761,8762],{"class":3001,"line":3614},[2999,8763,3351],{"emptyLinePlaceholder":3350},[2999,8765,8766,8768],{"class":3001,"line":3619},[2999,8767,5496],{"class":3022},[2999,8769,5499],{"class":3012},[2999,8771,8772,8774,8776,8778,8780,8782,8784],{"class":3001,"line":3625},[2999,8773,5432],{"class":3005},[2999,8775,5631],{"class":3012},[2999,8777,3023],{"class":3022},[2999,8779,4934],{"class":3012},[2999,8781,3047],{"class":3022},[2999,8783,4939],{"class":3050},[2999,8785,5199],{"class":3022},[2999,8787,8788,8790,8792,8794,8796,8798,8800,8802,8804],{"class":3001,"line":3631},[2999,8789,5651],{"class":3012},[2999,8791,3023],{"class":3022},[2999,8793,4934],{"class":3012},[2999,8795,3047],{"class":3022},[2999,8797,5660],{"class":3060},[2999,8799,3702],{"class":3022},[2999,8801,3706],{"class":3705},[2999,8803,5667],{"class":3012},[2999,8805,5670],{"class":3022},[2999,8807,8808,8810,8812,8814,8816,8818,8820,8822],{"class":3001,"line":3637},[2999,8809,8045],{"class":3705},[2999,8811,8670],{"class":3022},[2999,8813,8051],{"class":3005},[2999,8815,6486],{"class":3022},[2999,8817,5660],{"class":3060},[2999,8819,3803],{"class":3022},[2999,8821,7402],{"class":3050},[2999,8823,8824],{"class":3022},"(minYear);\n",[2999,8826,8827,8829,8831,8833,8835,8837,8839,8841],{"class":3001,"line":3643},[2999,8828,8045],{"class":3705},[2999,8830,8722],{"class":3022},[2999,8832,8051],{"class":3005},[2999,8834,6486],{"class":3022},[2999,8836,5660],{"class":3060},[2999,8838,3803],{"class":3022},[2999,8840,7402],{"class":3050},[2999,8842,8843],{"class":3022},"(maxYear);\n",[2999,8845,8846,8848],{"class":3001,"line":4891},[2999,8847,5027],{"class":3705},[2999,8849,5716],{"class":3022},[2999,8851,8852],{"class":3001,"line":4897},[2999,8853,3391],{"class":3022},[2999,8855,8856],{"class":3001,"line":4902},[2999,8857,3101],{"class":3022},[3285,8859,8861],{"id":8860},"genrespecification","GenreSpecification",[2990,8863,8865],{"className":2992,"code":8864,"language":2994,"meta":4540,"style":2995},"package com.example.audiobook.specification.audiobook;\n\nimport com.example.audiobook.domain.Audiobook;\nimport com.example.audiobook.specification.Specification;\n\nimport java.util.List;\n\n/**\n * Специфікація: аудіокнига належить до заданого жанру.\n * \u003Cp>\n * SQL: {@code g.name = ?} (припускаємо, що у SELECT є JOIN до genres).\n */\npublic class GenreSpecification implements Specification\u003CAudiobook> {\n\n    private final String genreName;\n\n    public GenreSpecification(String genreName) {\n        this.genreName = genreName;\n    }\n\n    @Override\n    public boolean isSatisfiedBy(Audiobook candidate) {\n        return candidate.getGenre() != null \n            && genreName.equals(candidate.getGenre().getName());\n    }\n\n    @Override\n    public String toSql() {\n        // Припускаємо, що у SELECT є JOIN genres g\n        return \"g.name = ?\";\n    }\n\n    @Override\n    public List\u003CObject> getParameters() {\n        return List.of(genreName);\n    }\n}\n",[2968,8866,8867,8873,8877,8883,8889,8893,8899,8903,8907,8912,8916,8921,8925,8944,8948,8961,8965,8979,8991,8995,8999,9005,9021,9040,9068,9072,9076,9082,9092,9097,9106,9110,9114,9120,9136,9149,9153],{"__ignoreMap":2995},[2999,8868,8869,8871],{"class":3001,"line":3002},[2999,8870,4547],{"class":3005},[2999,8872,7796],{"class":3022},[2999,8874,8875],{"class":3001,"line":3037},[2999,8876,3351],{"emptyLinePlaceholder":3350},[2999,8878,8879,8881],{"class":3001,"line":3067},[2999,8880,5312],{"class":3005},[2999,8882,7807],{"class":3022},[2999,8884,8885,8887],{"class":3001,"line":3098},[2999,8886,5312],{"class":3005},[2999,8888,6580],{"class":3022},[2999,8890,8891],{"class":3001,"line":3219},[2999,8892,3351],{"emptyLinePlaceholder":3350},[2999,8894,8895,8897],{"class":3001,"line":3364},[2999,8896,5312],{"class":3005},[2999,8898,5322],{"class":3022},[2999,8900,8901],{"class":3001,"line":3370},[2999,8902,3351],{"emptyLinePlaceholder":3350},[2999,8904,8905],{"class":3001,"line":3376},[2999,8906,4559],{"class":3222},[2999,8908,8909],{"class":3001,"line":3382},[2999,8910,8911],{"class":3222}," * Специфікація: аудіокнига належить до заданого жанру.\n",[2999,8913,8914],{"class":3001,"line":3388},[2999,8915,4569],{"class":3222},[2999,8917,8918],{"class":3001,"line":3394},[2999,8919,8920],{"class":3222}," * SQL: {@code g.name = ?} (припускаємо, що у SELECT є JOIN до genres).\n",[2999,8922,8923],{"class":3001,"line":3399},[2999,8924,4702],{"class":3222},[2999,8926,8927,8929,8931,8934,8936,8938,8940,8942],{"class":3001,"line":3405},[2999,8928,3006],{"class":3005},[2999,8930,5360],{"class":3005},[2999,8932,8933],{"class":3012}," GenreSpecification",[2999,8935,7867],{"class":3005},[2999,8937,4711],{"class":3012},[2999,8939,3023],{"class":3022},[2999,8941,3694],{"class":3012},[2999,8943,3034],{"class":3022},[2999,8945,8946],{"class":3001,"line":3411},[2999,8947,3351],{"emptyLinePlaceholder":3350},[2999,8949,8950,8952,8954,8956,8959],{"class":3001,"line":3417},[2999,8951,5388],{"class":3005},[2999,8953,5391],{"class":3005},[2999,8955,5565],{"class":3012},[2999,8957,8958],{"class":3060}," genreName",[2999,8960,4102],{"class":3022},[2999,8962,8963],{"class":3001,"line":3423},[2999,8964,3351],{"emptyLinePlaceholder":3350},[2999,8966,8967,8969,8971,8973,8975,8977],{"class":3001,"line":3428},[2999,8968,5432],{"class":3005},[2999,8970,8933],{"class":3050},[2999,8972,3054],{"class":3022},[2999,8974,3057],{"class":3012},[2999,8976,8958],{"class":3060},[2999,8978,4047],{"class":3022},[2999,8980,8981,8983,8985,8988],{"class":3001,"line":3433},[2999,8982,5465],{"class":3005},[2999,8984,3803],{"class":3022},[2999,8986,8987],{"class":3060},"genreName",[2999,8989,8990],{"class":3022}," = genreName;\n",[2999,8992,8993],{"class":3001,"line":3439},[2999,8994,3391],{"class":3022},[2999,8996,8997],{"class":3001,"line":3445},[2999,8998,3351],{"emptyLinePlaceholder":3350},[2999,9000,9001,9003],{"class":3001,"line":3450},[2999,9002,5496],{"class":3022},[2999,9004,5499],{"class":3012},[2999,9006,9007,9009,9011,9013,9015,9017,9019],{"class":3001,"line":3455},[2999,9008,5432],{"class":3005},[2999,9010,5506],{"class":3012},[2999,9012,4788],{"class":3050},[2999,9014,3054],{"class":3022},[2999,9016,3694],{"class":3012},[2999,9018,4762],{"class":3060},[2999,9020,4047],{"class":3022},[2999,9022,9023,9025,9027,9029,9032,9035,9037],{"class":3001,"line":3460},[2999,9024,5027],{"class":3705},[2999,9026,4762],{"class":3060},[2999,9028,3803],{"class":3022},[2999,9030,9031],{"class":3050},"getGenre",[2999,9033,9034],{"class":3022},"() != ",[2999,9036,8051],{"class":3005},[2999,9038,9039],{"class":3022}," \n",[2999,9041,9042,9045,9047,9049,9052,9054,9056,9058,9060,9063,9066],{"class":3001,"line":3466},[2999,9043,9044],{"class":3022},"            && ",[2999,9046,8987],{"class":3060},[2999,9048,3803],{"class":3022},[2999,9050,9051],{"class":3050},"equals",[2999,9053,3054],{"class":3022},[2999,9055,8033],{"class":3060},[2999,9057,3803],{"class":3022},[2999,9059,9031],{"class":3050},[2999,9061,9062],{"class":3022},"().",[2999,9064,9065],{"class":3050},"getName",[2999,9067,5691],{"class":3022},[2999,9069,9070],{"class":3001,"line":3472},[2999,9071,3391],{"class":3022},[2999,9073,9074],{"class":3001,"line":3477},[2999,9075,3351],{"emptyLinePlaceholder":3350},[2999,9077,9078,9080],{"class":3001,"line":3482},[2999,9079,5496],{"class":3022},[2999,9081,5499],{"class":3012},[2999,9083,9084,9086,9088,9090],{"class":3001,"line":3487},[2999,9085,5432],{"class":3005},[2999,9087,5565],{"class":3012},[2999,9089,4856],{"class":3050},[2999,9091,5199],{"class":3022},[2999,9093,9094],{"class":3001,"line":3493},[2999,9095,9096],{"class":3222},"        // Припускаємо, що у SELECT є JOIN genres g\n",[2999,9098,9099,9101,9104],{"class":3001,"line":3499},[2999,9100,5027],{"class":3705},[2999,9102,9103],{"class":3714}," \"g.name = ?\"",[2999,9105,4102],{"class":3022},[2999,9107,9108],{"class":3001,"line":3505},[2999,9109,3391],{"class":3022},[2999,9111,9112],{"class":3001,"line":3510},[2999,9113,3351],{"emptyLinePlaceholder":3350},[2999,9115,9116,9118],{"class":3001,"line":3515},[2999,9117,5496],{"class":3022},[2999,9119,5499],{"class":3012},[2999,9121,9122,9124,9126,9128,9130,9132,9134],{"class":3001,"line":3520},[2999,9123,5432],{"class":3005},[2999,9125,5631],{"class":3012},[2999,9127,3023],{"class":3022},[2999,9129,4934],{"class":3012},[2999,9131,3047],{"class":3022},[2999,9133,4939],{"class":3050},[2999,9135,5199],{"class":3022},[2999,9137,9138,9140,9142,9144,9146],{"class":3001,"line":3526},[2999,9139,5027],{"class":3705},[2999,9141,5631],{"class":3060},[2999,9143,3803],{"class":3022},[2999,9145,4152],{"class":3050},[2999,9147,9148],{"class":3022},"(genreName);\n",[2999,9150,9151],{"class":3001,"line":3531},[2999,9152,3391],{"class":3022},[2999,9154,9155],{"class":3001,"line":3536},[2999,9156,3101],{"class":3022},[3285,9158,9160],{"id":9159},"titlecontainsspecification","TitleContainsSpecification",[2990,9162,9164],{"className":2992,"code":9163,"language":2994,"meta":4540,"style":2995},"package com.example.audiobook.specification.audiobook;\n\nimport com.example.audiobook.domain.Audiobook;\nimport com.example.audiobook.specification.Specification;\n\nimport java.util.List;\n\n/**\n * Специфікація: назва аудіокниги містить заданий підрядок (регістр-незалежний пошук).\n * \u003Cp>\n * SQL: {@code LOWER(ab.title) LIKE LOWER(?)}.\n */\npublic class TitleContainsSpecification implements Specification\u003CAudiobook> {\n\n    private final String titlePart;\n\n    public TitleContainsSpecification(String titlePart) {\n        this.titlePart = titlePart;\n    }\n\n    @Override\n    public boolean isSatisfiedBy(Audiobook candidate) {\n        String title = candidate.getTitle();\n        return title != null && title.toLowerCase().contains(titlePart.toLowerCase());\n    }\n\n    @Override\n    public String toSql() {\n        return \"LOWER(ab.title) LIKE LOWER(?)\";\n    }\n\n    @Override\n    public List\u003CObject> getParameters() {\n        return List.of(\"%\" + titlePart + \"%\");\n    }\n}\n",[2968,9165,9166,9172,9176,9182,9188,9192,9198,9202,9206,9211,9215,9220,9224,9243,9247,9260,9264,9278,9290,9294,9298,9304,9320,9338,9372,9376,9380,9386,9396,9405,9409,9413,9419,9435,9457,9461],{"__ignoreMap":2995},[2999,9167,9168,9170],{"class":3001,"line":3002},[2999,9169,4547],{"class":3005},[2999,9171,7796],{"class":3022},[2999,9173,9174],{"class":3001,"line":3037},[2999,9175,3351],{"emptyLinePlaceholder":3350},[2999,9177,9178,9180],{"class":3001,"line":3067},[2999,9179,5312],{"class":3005},[2999,9181,7807],{"class":3022},[2999,9183,9184,9186],{"class":3001,"line":3098},[2999,9185,5312],{"class":3005},[2999,9187,6580],{"class":3022},[2999,9189,9190],{"class":3001,"line":3219},[2999,9191,3351],{"emptyLinePlaceholder":3350},[2999,9193,9194,9196],{"class":3001,"line":3364},[2999,9195,5312],{"class":3005},[2999,9197,5322],{"class":3022},[2999,9199,9200],{"class":3001,"line":3370},[2999,9201,3351],{"emptyLinePlaceholder":3350},[2999,9203,9204],{"class":3001,"line":3376},[2999,9205,4559],{"class":3222},[2999,9207,9208],{"class":3001,"line":3382},[2999,9209,9210],{"class":3222}," * Специфікація: назва аудіокниги містить заданий підрядок (регістр-незалежний пошук).\n",[2999,9212,9213],{"class":3001,"line":3388},[2999,9214,4569],{"class":3222},[2999,9216,9217],{"class":3001,"line":3394},[2999,9218,9219],{"class":3222}," * SQL: {@code LOWER(ab.title) LIKE LOWER(?)}.\n",[2999,9221,9222],{"class":3001,"line":3399},[2999,9223,4702],{"class":3222},[2999,9225,9226,9228,9230,9233,9235,9237,9239,9241],{"class":3001,"line":3405},[2999,9227,3006],{"class":3005},[2999,9229,5360],{"class":3005},[2999,9231,9232],{"class":3012}," TitleContainsSpecification",[2999,9234,7867],{"class":3005},[2999,9236,4711],{"class":3012},[2999,9238,3023],{"class":3022},[2999,9240,3694],{"class":3012},[2999,9242,3034],{"class":3022},[2999,9244,9245],{"class":3001,"line":3411},[2999,9246,3351],{"emptyLinePlaceholder":3350},[2999,9248,9249,9251,9253,9255,9258],{"class":3001,"line":3417},[2999,9250,5388],{"class":3005},[2999,9252,5391],{"class":3005},[2999,9254,5565],{"class":3012},[2999,9256,9257],{"class":3060}," titlePart",[2999,9259,4102],{"class":3022},[2999,9261,9262],{"class":3001,"line":3423},[2999,9263,3351],{"emptyLinePlaceholder":3350},[2999,9265,9266,9268,9270,9272,9274,9276],{"class":3001,"line":3428},[2999,9267,5432],{"class":3005},[2999,9269,9232],{"class":3050},[2999,9271,3054],{"class":3022},[2999,9273,3057],{"class":3012},[2999,9275,9257],{"class":3060},[2999,9277,4047],{"class":3022},[2999,9279,9280,9282,9284,9287],{"class":3001,"line":3433},[2999,9281,5465],{"class":3005},[2999,9283,3803],{"class":3022},[2999,9285,9286],{"class":3060},"titlePart",[2999,9288,9289],{"class":3022}," = titlePart;\n",[2999,9291,9292],{"class":3001,"line":3439},[2999,9293,3391],{"class":3022},[2999,9295,9296],{"class":3001,"line":3445},[2999,9297,3351],{"emptyLinePlaceholder":3350},[2999,9299,9300,9302],{"class":3001,"line":3450},[2999,9301,5496],{"class":3022},[2999,9303,5499],{"class":3012},[2999,9305,9306,9308,9310,9312,9314,9316,9318],{"class":3001,"line":3455},[2999,9307,5432],{"class":3005},[2999,9309,5506],{"class":3012},[2999,9311,4788],{"class":3050},[2999,9313,3054],{"class":3022},[2999,9315,3694],{"class":3012},[2999,9317,4762],{"class":3060},[2999,9319,4047],{"class":3022},[2999,9321,9322,9324,9327,9329,9331,9333,9336],{"class":3001,"line":3460},[2999,9323,7134],{"class":3012},[2999,9325,9326],{"class":3060}," title",[2999,9328,3702],{"class":3022},[2999,9330,8033],{"class":3060},[2999,9332,3803],{"class":3022},[2999,9334,9335],{"class":3050},"getTitle",[2999,9337,4859],{"class":3022},[2999,9339,9340,9342,9345,9347,9350,9352,9354,9357,9359,9362,9364,9366,9368,9370],{"class":3001,"line":3466},[2999,9341,5027],{"class":3705},[2999,9343,9344],{"class":3022}," title != ",[2999,9346,8051],{"class":3005},[2999,9348,9349],{"class":3022}," && ",[2999,9351,7780],{"class":3060},[2999,9353,3803],{"class":3022},[2999,9355,9356],{"class":3050},"toLowerCase",[2999,9358,9062],{"class":3022},[2999,9360,9361],{"class":3050},"contains",[2999,9363,3054],{"class":3022},[2999,9365,9286],{"class":3060},[2999,9367,3803],{"class":3022},[2999,9369,9356],{"class":3050},[2999,9371,5691],{"class":3022},[2999,9373,9374],{"class":3001,"line":3472},[2999,9375,3391],{"class":3022},[2999,9377,9378],{"class":3001,"line":3477},[2999,9379,3351],{"emptyLinePlaceholder":3350},[2999,9381,9382,9384],{"class":3001,"line":3482},[2999,9383,5496],{"class":3022},[2999,9385,5499],{"class":3012},[2999,9387,9388,9390,9392,9394],{"class":3001,"line":3487},[2999,9389,5432],{"class":3005},[2999,9391,5565],{"class":3012},[2999,9393,4856],{"class":3050},[2999,9395,5199],{"class":3022},[2999,9397,9398,9400,9403],{"class":3001,"line":3493},[2999,9399,5027],{"class":3705},[2999,9401,9402],{"class":3714}," \"LOWER(ab.title) LIKE LOWER(?)\"",[2999,9404,4102],{"class":3022},[2999,9406,9407],{"class":3001,"line":3499},[2999,9408,3391],{"class":3022},[2999,9410,9411],{"class":3001,"line":3505},[2999,9412,3351],{"emptyLinePlaceholder":3350},[2999,9414,9415,9417],{"class":3001,"line":3510},[2999,9416,5496],{"class":3022},[2999,9418,5499],{"class":3012},[2999,9420,9421,9423,9425,9427,9429,9431,9433],{"class":3001,"line":3515},[2999,9422,5432],{"class":3005},[2999,9424,5631],{"class":3012},[2999,9426,3023],{"class":3022},[2999,9428,4934],{"class":3012},[2999,9430,3047],{"class":3022},[2999,9432,4939],{"class":3050},[2999,9434,5199],{"class":3022},[2999,9436,9437,9439,9441,9443,9445,9447,9450,9453,9455],{"class":3001,"line":3520},[2999,9438,5027],{"class":3705},[2999,9440,5631],{"class":3060},[2999,9442,3803],{"class":3022},[2999,9444,4152],{"class":3050},[2999,9446,3054],{"class":3022},[2999,9448,9449],{"class":3714},"\"%\"",[2999,9451,9452],{"class":3022}," + titlePart + ",[2999,9454,9449],{"class":3714},[2999,9456,3064],{"class":3022},[2999,9458,9459],{"class":3001,"line":3526},[2999,9460,3391],{"class":3022},[2999,9462,9463],{"class":3001,"line":3531},[2999,9464,3101],{"class":3022},[3278,9466],{},[2959,9468,9470],{"id":9469},"реалізація-jdbcaudiobookrepository-з-specification","Реалізація JdbcAudiobookRepository з Specification",[2964,9472,9473,9474,9477,9478,9481,9482,9485],{},"Тепер адаптуємо ",[2968,9475,9476],{},"JdbcAudiobookRepository"," для роботи зі специфікаціями. ",[3228,9479,9480],{},"Ключовий принцип:"," специфікація є ",[3228,9483,9484],{},"додатковим"," методом пошуку, а не заміною існуючих специфічних методів.",[2990,9487,9489],{"className":2992,"code":9488,"language":2994,"meta":4540,"style":2995},"package com.example.audiobook.repository.jdbc;\n\nimport com.example.audiobook.db.ConnectionManager;\nimport com.example.audiobook.db.DatabaseException;\nimport com.example.audiobook.domain.Author;\nimport com.example.audiobook.domain.Audiobook;\nimport com.example.audiobook.domain.Genre;\nimport com.example.audiobook.repository.AudiobookRepository;\n\nimport java.sql.*;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\n/**\n * JDBC-реалізація {@link AudiobookRepository} з підтримкою Specification Pattern.\n * \u003Cp>\n * Клас зберігає всі специфічні методи пошуку (findByAuthorId, findByGenreName)\n * і додає універсальний метод findAll(Specification) для складних запитів.\n */\npublic class JdbcAudiobookRepository \n    extends AbstractJdbcRepository\u003CAudiobook, UUID>\n    implements AudiobookRepository {\n\n    /**\n     * Базовий SELECT з усіма JOIN для специфікацій.\n     * \u003Cp>\n     * Важливо: псевдоніми таблиць (ab, a, g) мають відповідати тим,\n     * що використовуються у специфікаціях (ab.duration, g.name тощо).\n     */\n    private static final String SQL_SELECT_BASE = \"\"\"\n        SELECT ab.id,\n               ab.title, ab.duration, ab.release_year,\n               ab.description, ab.cover_image_path,\n               a.id         AS author_id,\n               a.first_name, a.last_name, a.bio, a.image_path,\n               g.id         AS genre_id,\n               g.name       AS genre_name,\n               g.description AS genre_description\n        FROM audiobooks ab\n        JOIN authors a ON ab.author_id = a.id\n        JOIN genres  g ON ab.genre_id  = g.id\n        \"\"\";\n\n    private static final String SQL_SELECT_BY_ID =\n        SQL_SELECT_BASE + \"WHERE ab.id = ?\";\n\n    private static final String SQL_INSERT = \"\"\"\n        INSERT INTO audiobooks\n          (id, title, author_id, genre_id, duration, release_year, description, cover_image_path)\n        VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n        \"\"\";\n\n    private static final String SQL_UPDATE = \"\"\"\n        UPDATE audiobooks\n        SET title            = ?,\n            author_id        = ?,\n            genre_id         = ?,\n            duration         = ?,\n            release_year     = ?,\n            description      = ?,\n            cover_image_path = ?\n        WHERE id = ?\n        \"\"\";\n\n    // Специфічні SQL для методів AudiobookRepository\n    private static final String SQL_FIND_BY_AUTHOR_ID =\n        SQL_SELECT_BASE + \"WHERE ab.author_id = ? ORDER BY ab.release_year DESC\";\n\n    private static final String SQL_FIND_BY_GENRE_NAME =\n        SQL_SELECT_BASE + \"WHERE g.name = ? ORDER BY ab.release_year DESC\";\n\n    public JdbcAudiobookRepository(ConnectionManager connectionManager) {\n        super(connectionManager);\n    }\n\n    // === Реалізація абстрактних методів AbstractJdbcRepository ===\n\n    @Override\n    protected String getTableName() {\n        return \"audiobooks\";\n    }\n\n    @Override\n    protected String getSelectByIdSql() {\n        return SQL_SELECT_BY_ID;\n    }\n\n    @Override\n    protected String getSelectAllSql() {\n        // Повертаємо базовий SELECT без WHERE — WHERE додається у findAll(Specification)\n        return SQL_SELECT_BASE;\n    }\n\n    @Override\n    protected String getInsertSql() {\n        return SQL_INSERT;\n    }\n\n    @Override\n    protected String getUpdateSql() {\n        return SQL_UPDATE;\n    }\n\n    /**\n     * Data Mapper: ResultSet → Audiobook з вкладеними Author та Genre.\n     */\n    @Override\n    protected Audiobook mapRow(ResultSet rs) throws SQLException {\n        // Відновлюємо вкладені об'єкти з JOIN-рядка\n        Author author = new Author(\n            rs.getObject(\"author_id\", UUID.class),\n            rs.getString(\"first_name\"),\n            rs.getString(\"last_name\"),\n            rs.getString(\"bio\"),\n            rs.getString(\"image_path\")\n        );\n\n        Genre genre = new Genre(\n            rs.getObject(\"genre_id\", UUID.class),\n            rs.getString(\"genre_name\"),\n            rs.getString(\"genre_description\")\n        );\n\n        return new Audiobook(\n            rs.getObject(\"id\", UUID.class),\n            rs.getString(\"title\"),\n            author,\n            genre,\n            rs.getInt(\"duration\"),\n            rs.getInt(\"release_year\"),\n            rs.getString(\"description\"),\n            rs.getString(\"cover_image_path\")\n        );\n    }\n\n    @Override\n    protected void setInsertParams(PreparedStatement stmt, Audiobook audiobook) throws SQLException {\n        stmt.setObject(1, audiobook.getId());\n        stmt.setString(2, audiobook.getTitle());\n        stmt.setObject(3, audiobook.getAuthor().getId());\n        stmt.setObject(4, audiobook.getGenre().getId());\n        stmt.setInt(5, audiobook.getDuration());\n        stmt.setInt(6, audiobook.getReleaseYear());\n        stmt.setString(7, audiobook.getDescription());\n        stmt.setString(8, audiobook.getCoverImagePath());\n    }\n\n    @Override\n    protected void setUpdateParams(PreparedStatement stmt, Audiobook audiobook) throws SQLException {\n        stmt.setString(1, audiobook.getTitle());\n        stmt.setObject(2, audiobook.getAuthor().getId());\n        stmt.setObject(3, audiobook.getGenre().getId());\n        stmt.setInt(4, audiobook.getDuration());\n        stmt.setInt(5, audiobook.getReleaseYear());\n        stmt.setString(6, audiobook.getDescription());\n        stmt.setString(7, audiobook.getCoverImagePath());\n        stmt.setObject(8, audiobook.getId()); // WHERE id = ?\n    }\n\n    @Override\n    protected UUID getId(Audiobook audiobook) {\n        return audiobook.getId();\n    }\n\n    // === Специфічні методи AudiobookRepository — ЗБЕРІГАЄМО ЇХ ===\n\n    /**\n     * Знаходить всі аудіокниги заданого автора.\n     * Цей метод залишається у репозиторії — Specification не замінює його.\n     */\n    @Override\n    public List\u003CAudiobook> findByAuthorId(UUID authorId) {\n        List\u003CAudiobook> books = new ArrayList\u003C>();\n\n        try (Connection conn = connectionManager.getConnection();\n             PreparedStatement stmt = conn.prepareStatement(SQL_FIND_BY_AUTHOR_ID)) {\n\n            stmt.setObject(1, authorId);\n            try (ResultSet rs = stmt.executeQuery()) {\n                while (rs.next()) {\n                    books.add(mapRow(rs));\n                }\n            }\n\n        } catch (SQLException e) {\n            throw new DatabaseException(\"Помилка findByAuthorId для: \" + authorId, e);\n        }\n        return books;\n    }\n\n    /**\n     * Знаходить всі аудіокниги заданого жанру за назвою.\n     * Цей метод також залишається — він є зручним для простих запитів.\n     */\n    @Override\n    public List\u003CAudiobook> findByGenreName(String genreName) {\n        List\u003CAudiobook> books = new ArrayList\u003C>();\n\n        try (Connection conn = connectionManager.getConnection();\n             PreparedStatement stmt = conn.prepareStatement(SQL_FIND_BY_GENRE_NAME)) {\n\n            stmt.setString(1, genreName);\n            try (ResultSet rs = stmt.executeQuery()) {\n                while (rs.next()) {\n                    books.add(mapRow(rs));\n                }\n            }\n\n        } catch (SQLException e) {\n            throw new DatabaseException(\"Помилка findByGenreName для: \" + genreName, e);\n        }\n        return books;\n    }\n\n    // Метод findAll(Specification) успадкований від AbstractJdbcRepository\n    // і використовує getSelectAllSql() + spec.toSql()\n}\n",[2968,9490,9491,9497,9501,9507,9513,9520,9526,9533,9540,9544,9550,9556,9562,9569,9573,9577,9582,9586,9591,9596,9600,9611,9629,9639,9643,9647,9652,9656,9661,9666,9670,9688,9693,9698,9703,9708,9713,9718,9723,9728,9733,9738,9743,9749,9753,9769,9779,9783,9800,9805,9810,9815,9821,9825,9842,9847,9852,9857,9862,9867,9872,9877,9882,9887,9893,9897,9902,9917,9926,9930,9945,9954,9958,9972,9980,9984,9988,9993,9997,10003,10013,10022,10026,10030,10036,10047,10054,10058,10062,10068,10078,10083,10091,10096,10101,10108,10120,10128,10133,10138,10145,10157,10165,10170,10175,10180,10186,10191,10198,10222,10228,10246,10273,10290,10306,10322,10338,10344,10349,10367,10391,10407,10423,10428,10433,10444,10468,10484,10490,10496,10513,10529,10545,10561,10566,10571,10576,10583,10615,10641,10666,10695,10723,10748,10772,10797,10822,10827,10832,10839,10869,10892,10919,10946,10969,10992,11015,11038,11065,11070,11075,11082,11101,11114,11119,11124,11130,11135,11140,11146,11152,11157,11164,11189,11211,11216,11237,11255,11260,11277,11298,11313,11329,11334,11339,11344,11359,11376,11381,11389,11394,11399,11404,11410,11416,11421,11428,11452,11473,11478,11499,11517,11522,11538,11559,11574,11589,11594,11599,11604,11619,11636,11641,11648,11653,11658,11664,11670],{"__ignoreMap":2995},[2999,9492,9493,9495],{"class":3001,"line":3002},[2999,9494,4547],{"class":3005},[2999,9496,6848],{"class":3022},[2999,9498,9499],{"class":3001,"line":3037},[2999,9500,3351],{"emptyLinePlaceholder":3350},[2999,9502,9503,9505],{"class":3001,"line":3067},[2999,9504,5312],{"class":3005},[2999,9506,6859],{"class":3022},[2999,9508,9509,9511],{"class":3001,"line":3098},[2999,9510,5312],{"class":3005},[2999,9512,6866],{"class":3022},[2999,9514,9515,9517],{"class":3001,"line":3219},[2999,9516,5312],{"class":3005},[2999,9518,9519],{"class":3022}," com.example.audiobook.domain.Author;\n",[2999,9521,9522,9524],{"class":3001,"line":3364},[2999,9523,5312],{"class":3005},[2999,9525,7807],{"class":3022},[2999,9527,9528,9530],{"class":3001,"line":3370},[2999,9529,5312],{"class":3005},[2999,9531,9532],{"class":3022}," com.example.audiobook.domain.Genre;\n",[2999,9534,9535,9537],{"class":3001,"line":3376},[2999,9536,5312],{"class":3005},[2999,9538,9539],{"class":3022}," com.example.audiobook.repository.AudiobookRepository;\n",[2999,9541,9542],{"class":3001,"line":3382},[2999,9543,3351],{"emptyLinePlaceholder":3350},[2999,9545,9546,9548],{"class":3001,"line":3388},[2999,9547,5312],{"class":3005},[2999,9549,6890],{"class":3022},[2999,9551,9552,9554],{"class":3001,"line":3394},[2999,9553,5312],{"class":3005},[2999,9555,5315],{"class":3022},[2999,9557,9558,9560],{"class":3001,"line":3399},[2999,9559,5312],{"class":3005},[2999,9561,5322],{"class":3022},[2999,9563,9564,9566],{"class":3001,"line":3405},[2999,9565,5312],{"class":3005},[2999,9567,9568],{"class":3022}," java.util.UUID;\n",[2999,9570,9571],{"class":3001,"line":3411},[2999,9572,3351],{"emptyLinePlaceholder":3350},[2999,9574,9575],{"class":3001,"line":3417},[2999,9576,4559],{"class":3222},[2999,9578,9579],{"class":3001,"line":3423},[2999,9580,9581],{"class":3222}," * JDBC-реалізація {@link AudiobookRepository} з підтримкою Specification Pattern.\n",[2999,9583,9584],{"class":3001,"line":3428},[2999,9585,4569],{"class":3222},[2999,9587,9588],{"class":3001,"line":3433},[2999,9589,9590],{"class":3222}," * Клас зберігає всі специфічні методи пошуку (findByAuthorId, findByGenreName)\n",[2999,9592,9593],{"class":3001,"line":3439},[2999,9594,9595],{"class":3222}," * і додає універсальний метод findAll(Specification) для складних запитів.\n",[2999,9597,9598],{"class":3001,"line":3445},[2999,9599,4702],{"class":3222},[2999,9601,9602,9604,9606,9609],{"class":3001,"line":3450},[2999,9603,3006],{"class":3005},[2999,9605,5360],{"class":3005},[2999,9607,9608],{"class":3012}," JdbcAudiobookRepository",[2999,9610,9039],{"class":3022},[2999,9612,9613,9616,9618,9620,9622,9624,9626],{"class":3001,"line":3455},[2999,9614,9615],{"class":3005},"    extends",[2999,9617,6937],{"class":3012},[2999,9619,3023],{"class":3022},[2999,9621,3694],{"class":3012},[2999,9623,2975],{"class":3022},[2999,9625,3031],{"class":3012},[2999,9627,9628],{"class":3022},">\n",[2999,9630,9631,9634,9637],{"class":3001,"line":3460},[2999,9632,9633],{"class":3005},"    implements",[2999,9635,9636],{"class":3012}," AudiobookRepository",[2999,9638,8215],{"class":3022},[2999,9640,9641],{"class":3001,"line":3466},[2999,9642,3351],{"emptyLinePlaceholder":3350},[2999,9644,9645],{"class":3001,"line":3472},[2999,9646,4727],{"class":3222},[2999,9648,9649],{"class":3001,"line":3477},[2999,9650,9651],{"class":3222},"     * Базовий SELECT з усіма JOIN для специфікацій.\n",[2999,9653,9654],{"class":3001,"line":3482},[2999,9655,4737],{"class":3222},[2999,9657,9658],{"class":3001,"line":3487},[2999,9659,9660],{"class":3222},"     * Важливо: псевдоніми таблиць (ab, a, g) мають відповідати тим,\n",[2999,9662,9663],{"class":3001,"line":3493},[2999,9664,9665],{"class":3222},"     * що використовуються у специфікаціях (ab.duration, g.name тощо).\n",[2999,9667,9668],{"class":3001,"line":3499},[2999,9669,4780],{"class":3222},[2999,9671,9672,9674,9677,9679,9681,9684,9686],{"class":3001,"line":3505},[2999,9673,5388],{"class":3005},[2999,9675,9676],{"class":3005}," static",[2999,9678,5391],{"class":3005},[2999,9680,5565],{"class":3012},[2999,9682,9683],{"class":3060}," SQL_SELECT_BASE",[2999,9685,3702],{"class":3022},[2999,9687,4064],{"class":3714},[2999,9689,9690],{"class":3001,"line":3510},[2999,9691,9692],{"class":3714},"        SELECT ab.id,\n",[2999,9694,9695],{"class":3001,"line":3515},[2999,9696,9697],{"class":3714},"               ab.title, ab.duration, ab.release_year,\n",[2999,9699,9700],{"class":3001,"line":3520},[2999,9701,9702],{"class":3714},"               ab.description, ab.cover_image_path,\n",[2999,9704,9705],{"class":3001,"line":3526},[2999,9706,9707],{"class":3714},"               a.id         AS author_id,\n",[2999,9709,9710],{"class":3001,"line":3531},[2999,9711,9712],{"class":3714},"               a.first_name, a.last_name, a.bio, a.image_path,\n",[2999,9714,9715],{"class":3001,"line":3536},[2999,9716,9717],{"class":3714},"               g.id         AS genre_id,\n",[2999,9719,9720],{"class":3001,"line":3541},[2999,9721,9722],{"class":3714},"               g.name       AS genre_name,\n",[2999,9724,9725],{"class":3001,"line":3546},[2999,9726,9727],{"class":3714},"               g.description AS genre_description\n",[2999,9729,9730],{"class":3001,"line":3551},[2999,9731,9732],{"class":3714},"        FROM audiobooks ab\n",[2999,9734,9735],{"class":3001,"line":3557},[2999,9736,9737],{"class":3714},"        JOIN authors a ON ab.author_id = a.id\n",[2999,9739,9740],{"class":3001,"line":3563},[2999,9741,9742],{"class":3714},"        JOIN genres  g ON ab.genre_id  = g.id\n",[2999,9744,9745,9747],{"class":3001,"line":3568},[2999,9746,4099],{"class":3714},[2999,9748,4102],{"class":3022},[2999,9750,9751],{"class":3001,"line":3573},[2999,9752,3351],{"emptyLinePlaceholder":3350},[2999,9754,9755,9757,9759,9761,9763,9766],{"class":3001,"line":3578},[2999,9756,5388],{"class":3005},[2999,9758,9676],{"class":3005},[2999,9760,5391],{"class":3005},[2999,9762,5565],{"class":3012},[2999,9764,9765],{"class":3060}," SQL_SELECT_BY_ID",[2999,9767,9768],{"class":3022}," =\n",[2999,9770,9771,9774,9777],{"class":3001,"line":3584},[2999,9772,9773],{"class":3022},"        SQL_SELECT_BASE + ",[2999,9775,9776],{"class":3714},"\"WHERE ab.id = ?\"",[2999,9778,4102],{"class":3022},[2999,9780,9781],{"class":3001,"line":3590},[2999,9782,3351],{"emptyLinePlaceholder":3350},[2999,9784,9785,9787,9789,9791,9793,9796,9798],{"class":3001,"line":3596},[2999,9786,5388],{"class":3005},[2999,9788,9676],{"class":3005},[2999,9790,5391],{"class":3005},[2999,9792,5565],{"class":3012},[2999,9794,9795],{"class":3060}," SQL_INSERT",[2999,9797,3702],{"class":3022},[2999,9799,4064],{"class":3714},[2999,9801,9802],{"class":3001,"line":3602},[2999,9803,9804],{"class":3714},"        INSERT INTO audiobooks\n",[2999,9806,9807],{"class":3001,"line":3608},[2999,9808,9809],{"class":3714},"          (id, title, author_id, genre_id, duration, release_year, description, cover_image_path)\n",[2999,9811,9812],{"class":3001,"line":3614},[2999,9813,9814],{"class":3714},"        VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n",[2999,9816,9817,9819],{"class":3001,"line":3619},[2999,9818,4099],{"class":3714},[2999,9820,4102],{"class":3022},[2999,9822,9823],{"class":3001,"line":3625},[2999,9824,3351],{"emptyLinePlaceholder":3350},[2999,9826,9827,9829,9831,9833,9835,9838,9840],{"class":3001,"line":3631},[2999,9828,5388],{"class":3005},[2999,9830,9676],{"class":3005},[2999,9832,5391],{"class":3005},[2999,9834,5565],{"class":3012},[2999,9836,9837],{"class":3060}," SQL_UPDATE",[2999,9839,3702],{"class":3022},[2999,9841,4064],{"class":3714},[2999,9843,9844],{"class":3001,"line":3637},[2999,9845,9846],{"class":3714},"        UPDATE audiobooks\n",[2999,9848,9849],{"class":3001,"line":3643},[2999,9850,9851],{"class":3714},"        SET title            = ?,\n",[2999,9853,9854],{"class":3001,"line":4891},[2999,9855,9856],{"class":3714},"            author_id        = ?,\n",[2999,9858,9859],{"class":3001,"line":4897},[2999,9860,9861],{"class":3714},"            genre_id         = ?,\n",[2999,9863,9864],{"class":3001,"line":4902},[2999,9865,9866],{"class":3714},"            duration         = ?,\n",[2999,9868,9869],{"class":3001,"line":4912},[2999,9870,9871],{"class":3714},"            release_year     = ?,\n",[2999,9873,9874],{"class":3001,"line":4917},[2999,9875,9876],{"class":3714},"            description      = ?,\n",[2999,9878,9879],{"class":3001,"line":4944},[2999,9880,9881],{"class":3714},"            cover_image_path = ?\n",[2999,9883,9884],{"class":3001,"line":4949},[2999,9885,9886],{"class":3714},"        WHERE id = ?\n",[2999,9888,9889,9891],{"class":3001,"line":4954},[2999,9890,4099],{"class":3714},[2999,9892,4102],{"class":3022},[2999,9894,9895],{"class":3001,"line":4960},[2999,9896,3351],{"emptyLinePlaceholder":3350},[2999,9898,9899],{"class":3001,"line":4965},[2999,9900,9901],{"class":3222},"    // Специфічні SQL для методів AudiobookRepository\n",[2999,9903,9904,9906,9908,9910,9912,9915],{"class":3001,"line":4978},[2999,9905,5388],{"class":3005},[2999,9907,9676],{"class":3005},[2999,9909,5391],{"class":3005},[2999,9911,5565],{"class":3012},[2999,9913,9914],{"class":3060}," SQL_FIND_BY_AUTHOR_ID",[2999,9916,9768],{"class":3022},[2999,9918,9919,9921,9924],{"class":3001,"line":4988},[2999,9920,9773],{"class":3022},[2999,9922,9923],{"class":3714},"\"WHERE ab.author_id = ? ORDER BY ab.release_year DESC\"",[2999,9925,4102],{"class":3022},[2999,9927,9928],{"class":3001,"line":4993},[2999,9929,3351],{"emptyLinePlaceholder":3350},[2999,9931,9932,9934,9936,9938,9940,9943],{"class":3001,"line":5024},[2999,9933,5388],{"class":3005},[2999,9935,9676],{"class":3005},[2999,9937,5391],{"class":3005},[2999,9939,5565],{"class":3012},[2999,9941,9942],{"class":3060}," SQL_FIND_BY_GENRE_NAME",[2999,9944,9768],{"class":3022},[2999,9946,9947,9949,9952],{"class":3001,"line":5045},[2999,9948,9773],{"class":3022},[2999,9950,9951],{"class":3714},"\"WHERE g.name = ? ORDER BY ab.release_year DESC\"",[2999,9953,4102],{"class":3022},[2999,9955,9956],{"class":3001,"line":5050},[2999,9957,3351],{"emptyLinePlaceholder":3350},[2999,9959,9960,9962,9964,9966,9968,9970],{"class":3001,"line":5055},[2999,9961,5432],{"class":3005},[2999,9963,9608],{"class":3050},[2999,9965,3054],{"class":3022},[2999,9967,6995],{"class":3012},[2999,9969,6978],{"class":3060},[2999,9971,4047],{"class":3022},[2999,9973,9974,9977],{"class":3001,"line":5060},[2999,9975,9976],{"class":3005},"        super",[2999,9978,9979],{"class":3022},"(connectionManager);\n",[2999,9981,9982],{"class":3001,"line":5066},[2999,9983,3391],{"class":3022},[2999,9985,9986],{"class":3001,"line":5071},[2999,9987,3351],{"emptyLinePlaceholder":3350},[2999,9989,9990],{"class":3001,"line":5082},[2999,9991,9992],{"class":3222},"    // === Реалізація абстрактних методів AbstractJdbcRepository ===\n",[2999,9994,9995],{"class":3001,"line":5092},[2999,9996,3351],{"emptyLinePlaceholder":3350},[2999,9998,9999,10001],{"class":3001,"line":5097},[2999,10000,5496],{"class":3022},[2999,10002,5499],{"class":3012},[2999,10004,10005,10007,10009,10011],{"class":3001,"line":5126},[2999,10006,6970],{"class":3005},[2999,10008,5565],{"class":3012},[2999,10010,7534],{"class":3050},[2999,10012,5199],{"class":3022},[2999,10014,10015,10017,10020],{"class":3001,"line":5142},[2999,10016,5027],{"class":3705},[2999,10018,10019],{"class":3714}," \"audiobooks\"",[2999,10021,4102],{"class":3022},[2999,10023,10024],{"class":3001,"line":5147},[2999,10025,3391],{"class":3022},[2999,10027,10028],{"class":3001,"line":5152},[2999,10029,3351],{"emptyLinePlaceholder":3350},[2999,10031,10032,10034],{"class":3001,"line":5157},[2999,10033,5496],{"class":3022},[2999,10035,5499],{"class":3012},[2999,10037,10038,10040,10042,10045],{"class":3001,"line":5163},[2999,10039,6970],{"class":3005},[2999,10041,5565],{"class":3012},[2999,10043,10044],{"class":3050}," getSelectByIdSql",[2999,10046,5199],{"class":3022},[2999,10048,10049,10051],{"class":3001,"line":5168},[2999,10050,5027],{"class":3705},[2999,10052,10053],{"class":3022}," SQL_SELECT_BY_ID;\n",[2999,10055,10056],{"class":3001,"line":5178},[2999,10057,3391],{"class":3022},[2999,10059,10060],{"class":3001,"line":5183},[2999,10061,3351],{"emptyLinePlaceholder":3350},[2999,10063,10064,10066],{"class":3001,"line":5202},[2999,10065,5496],{"class":3022},[2999,10067,5499],{"class":3012},[2999,10069,10070,10072,10074,10076],{"class":3001,"line":5218},[2999,10071,6970],{"class":3005},[2999,10073,5565],{"class":3012},[2999,10075,7547],{"class":3050},[2999,10077,5199],{"class":3022},[2999,10079,10080],{"class":3001,"line":5223},[2999,10081,10082],{"class":3222},"        // Повертаємо базовий SELECT без WHERE — WHERE додається у findAll(Specification)\n",[2999,10084,10086,10088],{"class":3001,"line":10085},92,[2999,10087,5027],{"class":3705},[2999,10089,10090],{"class":3022}," SQL_SELECT_BASE;\n",[2999,10092,10094],{"class":3001,"line":10093},93,[2999,10095,3391],{"class":3022},[2999,10097,10099],{"class":3001,"line":10098},94,[2999,10100,3351],{"emptyLinePlaceholder":3350},[2999,10102,10104,10106],{"class":3001,"line":10103},95,[2999,10105,5496],{"class":3022},[2999,10107,5499],{"class":3012},[2999,10109,10111,10113,10115,10118],{"class":3001,"line":10110},96,[2999,10112,6970],{"class":3005},[2999,10114,5565],{"class":3012},[2999,10116,10117],{"class":3050}," getInsertSql",[2999,10119,5199],{"class":3022},[2999,10121,10123,10125],{"class":3001,"line":10122},97,[2999,10124,5027],{"class":3705},[2999,10126,10127],{"class":3022}," SQL_INSERT;\n",[2999,10129,10131],{"class":3001,"line":10130},98,[2999,10132,3391],{"class":3022},[2999,10134,10136],{"class":3001,"line":10135},99,[2999,10137,3351],{"emptyLinePlaceholder":3350},[2999,10139,10141,10143],{"class":3001,"line":10140},100,[2999,10142,5496],{"class":3022},[2999,10144,5499],{"class":3012},[2999,10146,10148,10150,10152,10155],{"class":3001,"line":10147},101,[2999,10149,6970],{"class":3005},[2999,10151,5565],{"class":3012},[2999,10153,10154],{"class":3050}," getUpdateSql",[2999,10156,5199],{"class":3022},[2999,10158,10160,10162],{"class":3001,"line":10159},102,[2999,10161,5027],{"class":3705},[2999,10163,10164],{"class":3022}," SQL_UPDATE;\n",[2999,10166,10168],{"class":3001,"line":10167},103,[2999,10169,3391],{"class":3022},[2999,10171,10173],{"class":3001,"line":10172},104,[2999,10174,3351],{"emptyLinePlaceholder":3350},[2999,10176,10178],{"class":3001,"line":10177},105,[2999,10179,4727],{"class":3222},[2999,10181,10183],{"class":3001,"line":10182},106,[2999,10184,10185],{"class":3222},"     * Data Mapper: ResultSet → Audiobook з вкладеними Author та Genre.\n",[2999,10187,10189],{"class":3001,"line":10188},107,[2999,10190,4780],{"class":3222},[2999,10192,10194,10196],{"class":3001,"line":10193},108,[2999,10195,5496],{"class":3022},[2999,10197,5499],{"class":3012},[2999,10199,10201,10203,10206,10208,10210,10212,10214,10216,10218,10220],{"class":3001,"line":10200},109,[2999,10202,6970],{"class":3005},[2999,10204,10205],{"class":3012}," Audiobook",[2999,10207,7507],{"class":3050},[2999,10209,3054],{"class":3022},[2999,10211,7359],{"class":3012},[2999,10213,7362],{"class":3060},[2999,10215,6486],{"class":3022},[2999,10217,7518],{"class":3005},[2999,10219,7521],{"class":3012},[2999,10221,8215],{"class":3022},[2999,10223,10225],{"class":3001,"line":10224},110,[2999,10226,10227],{"class":3222},"        // Відновлюємо вкладені об'єкти з JOIN-рядка\n",[2999,10229,10231,10234,10237,10239,10241,10244],{"class":3001,"line":10230},111,[2999,10232,10233],{"class":3012},"        Author",[2999,10235,10236],{"class":3060}," author",[2999,10238,3702],{"class":3022},[2999,10240,3706],{"class":3705},[2999,10242,10243],{"class":3050}," Author",[2999,10245,3795],{"class":3022},[2999,10247,10249,10252,10254,10257,10259,10262,10264,10266,10268,10271],{"class":3001,"line":10248},112,[2999,10250,10251],{"class":3060},"            rs",[2999,10253,3803],{"class":3022},[2999,10255,10256],{"class":3050},"getObject",[2999,10258,3054],{"class":3022},[2999,10260,10261],{"class":3714},"\"author_id\"",[2999,10263,2975],{"class":3022},[2999,10265,3031],{"class":3060},[2999,10267,3803],{"class":3022},[2999,10269,10270],{"class":3060},"class",[2999,10272,7718],{"class":3022},[2999,10274,10276,10278,10280,10283,10285,10288],{"class":3001,"line":10275},113,[2999,10277,10251],{"class":3060},[2999,10279,3803],{"class":3022},[2999,10281,10282],{"class":3050},"getString",[2999,10284,3054],{"class":3022},[2999,10286,10287],{"class":3714},"\"first_name\"",[2999,10289,7718],{"class":3022},[2999,10291,10293,10295,10297,10299,10301,10304],{"class":3001,"line":10292},114,[2999,10294,10251],{"class":3060},[2999,10296,3803],{"class":3022},[2999,10298,10282],{"class":3050},[2999,10300,3054],{"class":3022},[2999,10302,10303],{"class":3714},"\"last_name\"",[2999,10305,7718],{"class":3022},[2999,10307,10309,10311,10313,10315,10317,10320],{"class":3001,"line":10308},115,[2999,10310,10251],{"class":3060},[2999,10312,3803],{"class":3022},[2999,10314,10282],{"class":3050},[2999,10316,3054],{"class":3022},[2999,10318,10319],{"class":3714},"\"bio\"",[2999,10321,7718],{"class":3022},[2999,10323,10325,10327,10329,10331,10333,10336],{"class":3001,"line":10324},116,[2999,10326,10251],{"class":3060},[2999,10328,3803],{"class":3022},[2999,10330,10282],{"class":3050},[2999,10332,3054],{"class":3022},[2999,10334,10335],{"class":3714},"\"image_path\"",[2999,10337,3830],{"class":3022},[2999,10339,10341],{"class":3001,"line":10340},117,[2999,10342,10343],{"class":3022},"        );\n",[2999,10345,10347],{"class":3001,"line":10346},118,[2999,10348,3351],{"emptyLinePlaceholder":3350},[2999,10350,10352,10355,10358,10360,10362,10365],{"class":3001,"line":10351},119,[2999,10353,10354],{"class":3012},"        Genre",[2999,10356,10357],{"class":3060}," genre",[2999,10359,3702],{"class":3022},[2999,10361,3706],{"class":3705},[2999,10363,10364],{"class":3050}," Genre",[2999,10366,3795],{"class":3022},[2999,10368,10370,10372,10374,10376,10378,10381,10383,10385,10387,10389],{"class":3001,"line":10369},120,[2999,10371,10251],{"class":3060},[2999,10373,3803],{"class":3022},[2999,10375,10256],{"class":3050},[2999,10377,3054],{"class":3022},[2999,10379,10380],{"class":3714},"\"genre_id\"",[2999,10382,2975],{"class":3022},[2999,10384,3031],{"class":3060},[2999,10386,3803],{"class":3022},[2999,10388,10270],{"class":3060},[2999,10390,7718],{"class":3022},[2999,10392,10394,10396,10398,10400,10402,10405],{"class":3001,"line":10393},121,[2999,10395,10251],{"class":3060},[2999,10397,3803],{"class":3022},[2999,10399,10282],{"class":3050},[2999,10401,3054],{"class":3022},[2999,10403,10404],{"class":3714},"\"genre_name\"",[2999,10406,7718],{"class":3022},[2999,10408,10410,10412,10414,10416,10418,10421],{"class":3001,"line":10409},122,[2999,10411,10251],{"class":3060},[2999,10413,3803],{"class":3022},[2999,10415,10282],{"class":3050},[2999,10417,3054],{"class":3022},[2999,10419,10420],{"class":3714},"\"genre_description\"",[2999,10422,3830],{"class":3022},[2999,10424,10426],{"class":3001,"line":10425},123,[2999,10427,10343],{"class":3022},[2999,10429,10431],{"class":3001,"line":10430},124,[2999,10432,3351],{"emptyLinePlaceholder":3350},[2999,10434,10436,10438,10440,10442],{"class":3001,"line":10435},125,[2999,10437,5027],{"class":3705},[2999,10439,5030],{"class":3705},[2999,10441,10205],{"class":3050},[2999,10443,3795],{"class":3022},[2999,10445,10447,10449,10451,10453,10455,10458,10460,10462,10464,10466],{"class":3001,"line":10446},126,[2999,10448,10251],{"class":3060},[2999,10450,3803],{"class":3022},[2999,10452,10256],{"class":3050},[2999,10454,3054],{"class":3022},[2999,10456,10457],{"class":3714},"\"id\"",[2999,10459,2975],{"class":3022},[2999,10461,3031],{"class":3060},[2999,10463,3803],{"class":3022},[2999,10465,10270],{"class":3060},[2999,10467,7718],{"class":3022},[2999,10469,10471,10473,10475,10477,10479,10482],{"class":3001,"line":10470},127,[2999,10472,10251],{"class":3060},[2999,10474,3803],{"class":3022},[2999,10476,10282],{"class":3050},[2999,10478,3054],{"class":3022},[2999,10480,10481],{"class":3714},"\"title\"",[2999,10483,7718],{"class":3022},[2999,10485,10487],{"class":3001,"line":10486},128,[2999,10488,10489],{"class":3022},"            author,\n",[2999,10491,10493],{"class":3001,"line":10492},129,[2999,10494,10495],{"class":3022},"            genre,\n",[2999,10497,10499,10501,10503,10506,10508,10511],{"class":3001,"line":10498},130,[2999,10500,10251],{"class":3060},[2999,10502,3803],{"class":3022},[2999,10504,10505],{"class":3050},"getInt",[2999,10507,3054],{"class":3022},[2999,10509,10510],{"class":3714},"\"duration\"",[2999,10512,7718],{"class":3022},[2999,10514,10516,10518,10520,10522,10524,10527],{"class":3001,"line":10515},131,[2999,10517,10251],{"class":3060},[2999,10519,3803],{"class":3022},[2999,10521,10505],{"class":3050},[2999,10523,3054],{"class":3022},[2999,10525,10526],{"class":3714},"\"release_year\"",[2999,10528,7718],{"class":3022},[2999,10530,10532,10534,10536,10538,10540,10543],{"class":3001,"line":10531},132,[2999,10533,10251],{"class":3060},[2999,10535,3803],{"class":3022},[2999,10537,10282],{"class":3050},[2999,10539,3054],{"class":3022},[2999,10541,10542],{"class":3714},"\"description\"",[2999,10544,7718],{"class":3022},[2999,10546,10548,10550,10552,10554,10556,10559],{"class":3001,"line":10547},133,[2999,10549,10251],{"class":3060},[2999,10551,3803],{"class":3022},[2999,10553,10282],{"class":3050},[2999,10555,3054],{"class":3022},[2999,10557,10558],{"class":3714},"\"cover_image_path\"",[2999,10560,3830],{"class":3022},[2999,10562,10564],{"class":3001,"line":10563},134,[2999,10565,10343],{"class":3022},[2999,10567,10569],{"class":3001,"line":10568},135,[2999,10570,3391],{"class":3022},[2999,10572,10574],{"class":3001,"line":10573},136,[2999,10575,3351],{"emptyLinePlaceholder":3350},[2999,10577,10579,10581],{"class":3001,"line":10578},137,[2999,10580,5496],{"class":3022},[2999,10582,5499],{"class":3012},[2999,10584,10586,10588,10591,10594,10596,10598,10600,10602,10604,10607,10609,10611,10613],{"class":3001,"line":10585},138,[2999,10587,6970],{"class":3005},[2999,10589,10590],{"class":3012}," void",[2999,10592,10593],{"class":3050}," setInsertParams",[2999,10595,3054],{"class":3022},[2999,10597,7592],{"class":3012},[2999,10599,7236],{"class":3060},[2999,10601,2975],{"class":3022},[2999,10603,3694],{"class":3012},[2999,10605,10606],{"class":3060}," audiobook",[2999,10608,6486],{"class":3022},[2999,10610,7518],{"class":3005},[2999,10612,7521],{"class":3012},[2999,10614,8215],{"class":3022},[2999,10616,10618,10621,10623,10625,10627,10629,10631,10634,10636,10639],{"class":3001,"line":10617},139,[2999,10619,10620],{"class":3060},"        stmt",[2999,10622,3803],{"class":3022},[2999,10624,7322],{"class":3050},[2999,10626,3054],{"class":3022},[2999,10628,7328],{"class":3769},[2999,10630,2975],{"class":3022},[2999,10632,10633],{"class":3060},"audiobook",[2999,10635,3803],{"class":3022},[2999,10637,10638],{"class":3050},"getId",[2999,10640,5691],{"class":3022},[2999,10642,10644,10646,10648,10651,10653,10656,10658,10660,10662,10664],{"class":3001,"line":10643},140,[2999,10645,10620],{"class":3060},[2999,10647,3803],{"class":3022},[2999,10649,10650],{"class":3050},"setString",[2999,10652,3054],{"class":3022},[2999,10654,10655],{"class":3769},"2",[2999,10657,2975],{"class":3022},[2999,10659,10633],{"class":3060},[2999,10661,3803],{"class":3022},[2999,10663,9335],{"class":3050},[2999,10665,5691],{"class":3022},[2999,10667,10669,10671,10673,10675,10677,10680,10682,10684,10686,10689,10691,10693],{"class":3001,"line":10668},141,[2999,10670,10620],{"class":3060},[2999,10672,3803],{"class":3022},[2999,10674,7322],{"class":3050},[2999,10676,3054],{"class":3022},[2999,10678,10679],{"class":3769},"3",[2999,10681,2975],{"class":3022},[2999,10683,10633],{"class":3060},[2999,10685,3803],{"class":3022},[2999,10687,10688],{"class":3050},"getAuthor",[2999,10690,9062],{"class":3022},[2999,10692,10638],{"class":3050},[2999,10694,5691],{"class":3022},[2999,10696,10698,10700,10702,10704,10706,10709,10711,10713,10715,10717,10719,10721],{"class":3001,"line":10697},142,[2999,10699,10620],{"class":3060},[2999,10701,3803],{"class":3022},[2999,10703,7322],{"class":3050},[2999,10705,3054],{"class":3022},[2999,10707,10708],{"class":3769},"4",[2999,10710,2975],{"class":3022},[2999,10712,10633],{"class":3060},[2999,10714,3803],{"class":3022},[2999,10716,9031],{"class":3050},[2999,10718,9062],{"class":3022},[2999,10720,10638],{"class":3050},[2999,10722,5691],{"class":3022},[2999,10724,10726,10728,10730,10733,10735,10738,10740,10742,10744,10746],{"class":3001,"line":10725},143,[2999,10727,10620],{"class":3060},[2999,10729,3803],{"class":3022},[2999,10731,10732],{"class":3050},"setInt",[2999,10734,3054],{"class":3022},[2999,10736,10737],{"class":3769},"5",[2999,10739,2975],{"class":3022},[2999,10741,10633],{"class":3060},[2999,10743,3803],{"class":3022},[2999,10745,8038],{"class":3050},[2999,10747,5691],{"class":3022},[2999,10749,10751,10753,10755,10757,10759,10762,10764,10766,10768,10770],{"class":3001,"line":10750},144,[2999,10752,10620],{"class":3060},[2999,10754,3803],{"class":3022},[2999,10756,10732],{"class":3050},[2999,10758,3054],{"class":3022},[2999,10760,10761],{"class":3769},"6",[2999,10763,2975],{"class":3022},[2999,10765,10633],{"class":3060},[2999,10767,3803],{"class":3022},[2999,10769,8578],{"class":3050},[2999,10771,5691],{"class":3022},[2999,10773,10775,10777,10779,10781,10783,10786,10788,10790,10792,10795],{"class":3001,"line":10774},145,[2999,10776,10620],{"class":3060},[2999,10778,3803],{"class":3022},[2999,10780,10650],{"class":3050},[2999,10782,3054],{"class":3022},[2999,10784,10785],{"class":3769},"7",[2999,10787,2975],{"class":3022},[2999,10789,10633],{"class":3060},[2999,10791,3803],{"class":3022},[2999,10793,10794],{"class":3050},"getDescription",[2999,10796,5691],{"class":3022},[2999,10798,10800,10802,10804,10806,10808,10811,10813,10815,10817,10820],{"class":3001,"line":10799},146,[2999,10801,10620],{"class":3060},[2999,10803,3803],{"class":3022},[2999,10805,10650],{"class":3050},[2999,10807,3054],{"class":3022},[2999,10809,10810],{"class":3769},"8",[2999,10812,2975],{"class":3022},[2999,10814,10633],{"class":3060},[2999,10816,3803],{"class":3022},[2999,10818,10819],{"class":3050},"getCoverImagePath",[2999,10821,5691],{"class":3022},[2999,10823,10825],{"class":3001,"line":10824},147,[2999,10826,3391],{"class":3022},[2999,10828,10830],{"class":3001,"line":10829},148,[2999,10831,3351],{"emptyLinePlaceholder":3350},[2999,10833,10835,10837],{"class":3001,"line":10834},149,[2999,10836,5496],{"class":3022},[2999,10838,5499],{"class":3012},[2999,10840,10842,10844,10846,10849,10851,10853,10855,10857,10859,10861,10863,10865,10867],{"class":3001,"line":10841},150,[2999,10843,6970],{"class":3005},[2999,10845,10590],{"class":3012},[2999,10847,10848],{"class":3050}," setUpdateParams",[2999,10850,3054],{"class":3022},[2999,10852,7592],{"class":3012},[2999,10854,7236],{"class":3060},[2999,10856,2975],{"class":3022},[2999,10858,3694],{"class":3012},[2999,10860,10606],{"class":3060},[2999,10862,6486],{"class":3022},[2999,10864,7518],{"class":3005},[2999,10866,7521],{"class":3012},[2999,10868,8215],{"class":3022},[2999,10870,10872,10874,10876,10878,10880,10882,10884,10886,10888,10890],{"class":3001,"line":10871},151,[2999,10873,10620],{"class":3060},[2999,10875,3803],{"class":3022},[2999,10877,10650],{"class":3050},[2999,10879,3054],{"class":3022},[2999,10881,7328],{"class":3769},[2999,10883,2975],{"class":3022},[2999,10885,10633],{"class":3060},[2999,10887,3803],{"class":3022},[2999,10889,9335],{"class":3050},[2999,10891,5691],{"class":3022},[2999,10893,10895,10897,10899,10901,10903,10905,10907,10909,10911,10913,10915,10917],{"class":3001,"line":10894},152,[2999,10896,10620],{"class":3060},[2999,10898,3803],{"class":3022},[2999,10900,7322],{"class":3050},[2999,10902,3054],{"class":3022},[2999,10904,10655],{"class":3769},[2999,10906,2975],{"class":3022},[2999,10908,10633],{"class":3060},[2999,10910,3803],{"class":3022},[2999,10912,10688],{"class":3050},[2999,10914,9062],{"class":3022},[2999,10916,10638],{"class":3050},[2999,10918,5691],{"class":3022},[2999,10920,10922,10924,10926,10928,10930,10932,10934,10936,10938,10940,10942,10944],{"class":3001,"line":10921},153,[2999,10923,10620],{"class":3060},[2999,10925,3803],{"class":3022},[2999,10927,7322],{"class":3050},[2999,10929,3054],{"class":3022},[2999,10931,10679],{"class":3769},[2999,10933,2975],{"class":3022},[2999,10935,10633],{"class":3060},[2999,10937,3803],{"class":3022},[2999,10939,9031],{"class":3050},[2999,10941,9062],{"class":3022},[2999,10943,10638],{"class":3050},[2999,10945,5691],{"class":3022},[2999,10947,10949,10951,10953,10955,10957,10959,10961,10963,10965,10967],{"class":3001,"line":10948},154,[2999,10950,10620],{"class":3060},[2999,10952,3803],{"class":3022},[2999,10954,10732],{"class":3050},[2999,10956,3054],{"class":3022},[2999,10958,10708],{"class":3769},[2999,10960,2975],{"class":3022},[2999,10962,10633],{"class":3060},[2999,10964,3803],{"class":3022},[2999,10966,8038],{"class":3050},[2999,10968,5691],{"class":3022},[2999,10970,10972,10974,10976,10978,10980,10982,10984,10986,10988,10990],{"class":3001,"line":10971},155,[2999,10973,10620],{"class":3060},[2999,10975,3803],{"class":3022},[2999,10977,10732],{"class":3050},[2999,10979,3054],{"class":3022},[2999,10981,10737],{"class":3769},[2999,10983,2975],{"class":3022},[2999,10985,10633],{"class":3060},[2999,10987,3803],{"class":3022},[2999,10989,8578],{"class":3050},[2999,10991,5691],{"class":3022},[2999,10993,10995,10997,10999,11001,11003,11005,11007,11009,11011,11013],{"class":3001,"line":10994},156,[2999,10996,10620],{"class":3060},[2999,10998,3803],{"class":3022},[2999,11000,10650],{"class":3050},[2999,11002,3054],{"class":3022},[2999,11004,10761],{"class":3769},[2999,11006,2975],{"class":3022},[2999,11008,10633],{"class":3060},[2999,11010,3803],{"class":3022},[2999,11012,10794],{"class":3050},[2999,11014,5691],{"class":3022},[2999,11016,11018,11020,11022,11024,11026,11028,11030,11032,11034,11036],{"class":3001,"line":11017},157,[2999,11019,10620],{"class":3060},[2999,11021,3803],{"class":3022},[2999,11023,10650],{"class":3050},[2999,11025,3054],{"class":3022},[2999,11027,10785],{"class":3769},[2999,11029,2975],{"class":3022},[2999,11031,10633],{"class":3060},[2999,11033,3803],{"class":3022},[2999,11035,10819],{"class":3050},[2999,11037,5691],{"class":3022},[2999,11039,11041,11043,11045,11047,11049,11051,11053,11055,11057,11059,11062],{"class":3001,"line":11040},158,[2999,11042,10620],{"class":3060},[2999,11044,3803],{"class":3022},[2999,11046,7322],{"class":3050},[2999,11048,3054],{"class":3022},[2999,11050,10810],{"class":3769},[2999,11052,2975],{"class":3022},[2999,11054,10633],{"class":3060},[2999,11056,3803],{"class":3022},[2999,11058,10638],{"class":3050},[2999,11060,11061],{"class":3022},"()); ",[2999,11063,11064],{"class":3222},"// WHERE id = ?\n",[2999,11066,11068],{"class":3001,"line":11067},159,[2999,11069,3391],{"class":3022},[2999,11071,11073],{"class":3001,"line":11072},160,[2999,11074,3351],{"emptyLinePlaceholder":3350},[2999,11076,11078,11080],{"class":3001,"line":11077},161,[2999,11079,5496],{"class":3022},[2999,11081,5499],{"class":3012},[2999,11083,11085,11087,11090,11093,11095,11097,11099],{"class":3001,"line":11084},162,[2999,11086,6970],{"class":3005},[2999,11088,11089],{"class":3012}," UUID",[2999,11091,11092],{"class":3050}," getId",[2999,11094,3054],{"class":3022},[2999,11096,3694],{"class":3012},[2999,11098,10606],{"class":3060},[2999,11100,4047],{"class":3022},[2999,11102,11104,11106,11108,11110,11112],{"class":3001,"line":11103},163,[2999,11105,5027],{"class":3705},[2999,11107,10606],{"class":3060},[2999,11109,3803],{"class":3022},[2999,11111,10638],{"class":3050},[2999,11113,4859],{"class":3022},[2999,11115,11117],{"class":3001,"line":11116},164,[2999,11118,3391],{"class":3022},[2999,11120,11122],{"class":3001,"line":11121},165,[2999,11123,3351],{"emptyLinePlaceholder":3350},[2999,11125,11127],{"class":3001,"line":11126},166,[2999,11128,11129],{"class":3222},"    // === Специфічні методи AudiobookRepository — ЗБЕРІГАЄМО ЇХ ===\n",[2999,11131,11133],{"class":3001,"line":11132},167,[2999,11134,3351],{"emptyLinePlaceholder":3350},[2999,11136,11138],{"class":3001,"line":11137},168,[2999,11139,4727],{"class":3222},[2999,11141,11143],{"class":3001,"line":11142},169,[2999,11144,11145],{"class":3222},"     * Знаходить всі аудіокниги заданого автора.\n",[2999,11147,11149],{"class":3001,"line":11148},170,[2999,11150,11151],{"class":3222},"     * Цей метод залишається у репозиторії — Specification не замінює його.\n",[2999,11153,11155],{"class":3001,"line":11154},171,[2999,11156,4780],{"class":3222},[2999,11158,11160,11162],{"class":3001,"line":11159},172,[2999,11161,5496],{"class":3022},[2999,11163,5499],{"class":3012},[2999,11165,11167,11169,11171,11173,11175,11177,11180,11182,11184,11187],{"class":3001,"line":11166},173,[2999,11168,5432],{"class":3005},[2999,11170,5631],{"class":3012},[2999,11172,3023],{"class":3022},[2999,11174,3694],{"class":3012},[2999,11176,3047],{"class":3022},[2999,11178,11179],{"class":3050},"findByAuthorId",[2999,11181,3054],{"class":3022},[2999,11183,3031],{"class":3012},[2999,11185,11186],{"class":3060}," authorId",[2999,11188,4047],{"class":3022},[2999,11190,11192,11194,11196,11198,11200,11203,11205,11207,11209],{"class":3001,"line":11191},174,[2999,11193,5651],{"class":3012},[2999,11195,3023],{"class":3022},[2999,11197,3694],{"class":3012},[2999,11199,3047],{"class":3022},[2999,11201,11202],{"class":3060},"books",[2999,11204,3702],{"class":3022},[2999,11206,3706],{"class":3705},[2999,11208,5667],{"class":3012},[2999,11210,5670],{"class":3022},[2999,11212,11214],{"class":3001,"line":11213},175,[2999,11215,3351],{"emptyLinePlaceholder":3350},[2999,11217,11219,11221,11223,11225,11227,11229,11231,11233,11235],{"class":3001,"line":11218},176,[2999,11220,7209],{"class":3705},[2999,11222,5238],{"class":3022},[2999,11224,7214],{"class":3012},[2999,11226,7217],{"class":3060},[2999,11228,3702],{"class":3022},[2999,11230,7008],{"class":3060},[2999,11232,3803],{"class":3022},[2999,11234,7226],{"class":3050},[2999,11236,4859],{"class":3022},[2999,11238,11240,11242,11244,11246,11248,11250,11252],{"class":3001,"line":11239},177,[2999,11241,7233],{"class":3012},[2999,11243,7236],{"class":3060},[2999,11245,3702],{"class":3022},[2999,11247,7241],{"class":3060},[2999,11249,3803],{"class":3022},[2999,11251,7246],{"class":3050},[2999,11253,11254],{"class":3022},"(SQL_FIND_BY_AUTHOR_ID)) {\n",[2999,11256,11258],{"class":3001,"line":11257},178,[2999,11259,3351],{"emptyLinePlaceholder":3350},[2999,11261,11263,11266,11268,11270,11272,11274],{"class":3001,"line":11262},179,[2999,11264,11265],{"class":3060},"            stmt",[2999,11267,3803],{"class":3022},[2999,11269,7322],{"class":3050},[2999,11271,3054],{"class":3022},[2999,11273,7328],{"class":3769},[2999,11275,11276],{"class":3022},", authorId);\n",[2999,11278,11280,11282,11284,11286,11288,11290,11292,11294,11296],{"class":3001,"line":11279},180,[2999,11281,7354],{"class":3705},[2999,11283,5238],{"class":3022},[2999,11285,7359],{"class":3012},[2999,11287,7362],{"class":3060},[2999,11289,3702],{"class":3022},[2999,11291,7367],{"class":3060},[2999,11293,3803],{"class":3022},[2999,11295,7372],{"class":3050},[2999,11297,7375],{"class":3022},[2999,11299,11301,11303,11305,11307,11309,11311],{"class":3001,"line":11300},181,[2999,11302,7380],{"class":3705},[2999,11304,5238],{"class":3022},[2999,11306,7385],{"class":3060},[2999,11308,3803],{"class":3022},[2999,11310,7390],{"class":3050},[2999,11312,7375],{"class":3022},[2999,11314,11316,11319,11321,11323,11325,11327],{"class":3001,"line":11315},182,[2999,11317,11318],{"class":3060},"                    books",[2999,11320,3803],{"class":3022},[2999,11322,7402],{"class":3050},[2999,11324,3054],{"class":3022},[2999,11326,7407],{"class":3050},[2999,11328,7410],{"class":3022},[2999,11330,11332],{"class":3001,"line":11331},183,[2999,11333,7415],{"class":3022},[2999,11335,11337],{"class":3001,"line":11336},184,[2999,11338,7345],{"class":3022},[2999,11340,11342],{"class":3001,"line":11341},185,[2999,11343,3351],{"emptyLinePlaceholder":3350},[2999,11345,11347,11349,11351,11353,11355,11357],{"class":3001,"line":11346},186,[2999,11348,7428],{"class":3022},[2999,11350,7431],{"class":3705},[2999,11352,5238],{"class":3022},[2999,11354,7436],{"class":3012},[2999,11356,7439],{"class":3060},[2999,11358,4047],{"class":3022},[2999,11360,11362,11364,11366,11368,11370,11373],{"class":3001,"line":11361},187,[2999,11363,7446],{"class":3705},[2999,11365,5030],{"class":3705},[2999,11367,7451],{"class":3050},[2999,11369,3054],{"class":3022},[2999,11371,11372],{"class":3714},"\"Помилка findByAuthorId для: \"",[2999,11374,11375],{"class":3022}," + authorId, e);\n",[2999,11377,11379],{"class":3001,"line":11378},188,[2999,11380,7471],{"class":3022},[2999,11382,11384,11386],{"class":3001,"line":11383},189,[2999,11385,5027],{"class":3705},[2999,11387,11388],{"class":3022}," books;\n",[2999,11390,11392],{"class":3001,"line":11391},190,[2999,11393,3391],{"class":3022},[2999,11395,11397],{"class":3001,"line":11396},191,[2999,11398,3351],{"emptyLinePlaceholder":3350},[2999,11400,11402],{"class":3001,"line":11401},192,[2999,11403,4727],{"class":3222},[2999,11405,11407],{"class":3001,"line":11406},193,[2999,11408,11409],{"class":3222},"     * Знаходить всі аудіокниги заданого жанру за назвою.\n",[2999,11411,11413],{"class":3001,"line":11412},194,[2999,11414,11415],{"class":3222},"     * Цей метод також залишається — він є зручним для простих запитів.\n",[2999,11417,11419],{"class":3001,"line":11418},195,[2999,11420,4780],{"class":3222},[2999,11422,11424,11426],{"class":3001,"line":11423},196,[2999,11425,5496],{"class":3022},[2999,11427,5499],{"class":3012},[2999,11429,11431,11433,11435,11437,11439,11441,11444,11446,11448,11450],{"class":3001,"line":11430},197,[2999,11432,5432],{"class":3005},[2999,11434,5631],{"class":3012},[2999,11436,3023],{"class":3022},[2999,11438,3694],{"class":3012},[2999,11440,3047],{"class":3022},[2999,11442,11443],{"class":3050},"findByGenreName",[2999,11445,3054],{"class":3022},[2999,11447,3057],{"class":3012},[2999,11449,8958],{"class":3060},[2999,11451,4047],{"class":3022},[2999,11453,11455,11457,11459,11461,11463,11465,11467,11469,11471],{"class":3001,"line":11454},198,[2999,11456,5651],{"class":3012},[2999,11458,3023],{"class":3022},[2999,11460,3694],{"class":3012},[2999,11462,3047],{"class":3022},[2999,11464,11202],{"class":3060},[2999,11466,3702],{"class":3022},[2999,11468,3706],{"class":3705},[2999,11470,5667],{"class":3012},[2999,11472,5670],{"class":3022},[2999,11474,11476],{"class":3001,"line":11475},199,[2999,11477,3351],{"emptyLinePlaceholder":3350},[2999,11479,11481,11483,11485,11487,11489,11491,11493,11495,11497],{"class":3001,"line":11480},200,[2999,11482,7209],{"class":3705},[2999,11484,5238],{"class":3022},[2999,11486,7214],{"class":3012},[2999,11488,7217],{"class":3060},[2999,11490,3702],{"class":3022},[2999,11492,7008],{"class":3060},[2999,11494,3803],{"class":3022},[2999,11496,7226],{"class":3050},[2999,11498,4859],{"class":3022},[2999,11500,11502,11504,11506,11508,11510,11512,11514],{"class":3001,"line":11501},201,[2999,11503,7233],{"class":3012},[2999,11505,7236],{"class":3060},[2999,11507,3702],{"class":3022},[2999,11509,7241],{"class":3060},[2999,11511,3803],{"class":3022},[2999,11513,7246],{"class":3050},[2999,11515,11516],{"class":3022},"(SQL_FIND_BY_GENRE_NAME)) {\n",[2999,11518,11520],{"class":3001,"line":11519},202,[2999,11521,3351],{"emptyLinePlaceholder":3350},[2999,11523,11525,11527,11529,11531,11533,11535],{"class":3001,"line":11524},203,[2999,11526,11265],{"class":3060},[2999,11528,3803],{"class":3022},[2999,11530,10650],{"class":3050},[2999,11532,3054],{"class":3022},[2999,11534,7328],{"class":3769},[2999,11536,11537],{"class":3022},", genreName);\n",[2999,11539,11541,11543,11545,11547,11549,11551,11553,11555,11557],{"class":3001,"line":11540},204,[2999,11542,7354],{"class":3705},[2999,11544,5238],{"class":3022},[2999,11546,7359],{"class":3012},[2999,11548,7362],{"class":3060},[2999,11550,3702],{"class":3022},[2999,11552,7367],{"class":3060},[2999,11554,3803],{"class":3022},[2999,11556,7372],{"class":3050},[2999,11558,7375],{"class":3022},[2999,11560,11562,11564,11566,11568,11570,11572],{"class":3001,"line":11561},205,[2999,11563,7380],{"class":3705},[2999,11565,5238],{"class":3022},[2999,11567,7385],{"class":3060},[2999,11569,3803],{"class":3022},[2999,11571,7390],{"class":3050},[2999,11573,7375],{"class":3022},[2999,11575,11577,11579,11581,11583,11585,11587],{"class":3001,"line":11576},206,[2999,11578,11318],{"class":3060},[2999,11580,3803],{"class":3022},[2999,11582,7402],{"class":3050},[2999,11584,3054],{"class":3022},[2999,11586,7407],{"class":3050},[2999,11588,7410],{"class":3022},[2999,11590,11592],{"class":3001,"line":11591},207,[2999,11593,7415],{"class":3022},[2999,11595,11597],{"class":3001,"line":11596},208,[2999,11598,7345],{"class":3022},[2999,11600,11602],{"class":3001,"line":11601},209,[2999,11603,3351],{"emptyLinePlaceholder":3350},[2999,11605,11607,11609,11611,11613,11615,11617],{"class":3001,"line":11606},210,[2999,11608,7428],{"class":3022},[2999,11610,7431],{"class":3705},[2999,11612,5238],{"class":3022},[2999,11614,7436],{"class":3012},[2999,11616,7439],{"class":3060},[2999,11618,4047],{"class":3022},[2999,11620,11622,11624,11626,11628,11630,11633],{"class":3001,"line":11621},211,[2999,11623,7446],{"class":3705},[2999,11625,5030],{"class":3705},[2999,11627,7451],{"class":3050},[2999,11629,3054],{"class":3022},[2999,11631,11632],{"class":3714},"\"Помилка findByGenreName для: \"",[2999,11634,11635],{"class":3022}," + genreName, e);\n",[2999,11637,11639],{"class":3001,"line":11638},212,[2999,11640,7471],{"class":3022},[2999,11642,11644,11646],{"class":3001,"line":11643},213,[2999,11645,5027],{"class":3705},[2999,11647,11388],{"class":3022},[2999,11649,11651],{"class":3001,"line":11650},214,[2999,11652,3391],{"class":3022},[2999,11654,11656],{"class":3001,"line":11655},215,[2999,11657,3351],{"emptyLinePlaceholder":3350},[2999,11659,11661],{"class":3001,"line":11660},216,[2999,11662,11663],{"class":3222},"    // Метод findAll(Specification) успадкований від AbstractJdbcRepository\n",[2999,11665,11667],{"class":3001,"line":11666},217,[2999,11668,11669],{"class":3222},"    // і використовує getSelectAllSql() + spec.toSql()\n",[2999,11671,11673],{"class":3001,"line":11672},218,[2999,11674,3101],{"class":3022},[2964,11676,11677],{},[3228,11678,5230],{},[4397,11680,11681,11708,11720,11731],{},[4400,11682,11683,5238,11686,11689,11690,2975,11692,2975,11694,11697,11698,2975,11701,2975,11704,11707],{},[3228,11684,11685],{},"Рядки 31–42",[2968,11687,11688],{},"SQL_SELECT_BASE","): базовий SELECT містить усі поля зі схеми БД — ",[2968,11691,7771],{},[2968,11693,7774],{},[2968,11695,11696],{},"cover_image_path",". Псевдоніми таблиць (",[2968,11699,11700],{},"ab",[2968,11702,11703],{},"a",[2968,11705,11706],{},"g",") відповідають тим, що використовуються у специфікаціях.",[4400,11709,11710,5238,11713,11715,11716,11719],{},[3228,11711,11712],{},"Рядки 175–197",[2968,11714,11179],{},"): специфічний метод ",[3228,11717,11718],{},"залишається у репозиторії",". Specification Pattern не замінює його — він додає гнучкість для складних запитів, але прості запити залишаються простими.",[4400,11721,11722,5238,11725,11727,11728,3803],{},[3228,11723,11724],{},"Рядки 202–220",[2968,11726,11443],{},"): аналогічно — зручний метод для частого сценарію. Якщо потрібен складний запит (жанр + рік + тривалість), використовується ",[2968,11729,11730],{},"findAll(Specification)",[4400,11732,11733,11736,11737,11740,11741,11743,11744,3803],{},[3228,11734,11735],{},"Рядок 223"," (коментар): метод ",[2968,11738,11739],{},"findAll(Specification\u003CAudiobook> spec)"," успадкований від ",[2968,11742,6835],{}," і автоматично працює завдяки ",[2968,11745,7063],{},[2964,11747,11748,11751,11752,11754,11755,11758,11759,5238,11762,11765],{},[3228,11749,11750],{},"Критична деталь:"," рядок 31 (",[2968,11753,11688],{},") містить псевдонім ",[2968,11756,11757],{},"g.name AS genre_name",". Саме цей псевдонім використовується у ",[2968,11760,11761],{},"GenreSpecification.toSql()",[2968,11763,11764],{},"\"g.name = ?\"","). Якби псевдоніми не збігалися — SQL-запит був би некоректним.",[3278,11767],{},[2959,11769,11771],{"id":11770},"демонстрація-складні-запити-через-композицію","Демонстрація: Складні запити через композицію",[2964,11773,11774,11775,3803],{},"Тепер продемонструємо, як Specification Pattern вирішує проблему комбінаторного вибуху методів пошуку, використовуючи ",[3228,11776,7617],{},[2990,11778,11780],{"className":2992,"code":11779,"language":2994,"meta":4540,"style":2995},"package com.example.audiobook;\n\nimport com.example.audiobook.db.ConnectionManager;\nimport com.example.audiobook.domain.Audiobook;\nimport com.example.audiobook.repository.AudiobookRepository;\nimport com.example.audiobook.repository.jdbc.JdbcAudiobookRepository;\nimport com.example.audiobook.specification.Specification;\nimport com.example.audiobook.specification.audiobook.*;\n\nimport java.util.List;\n\npublic class Main {\n\n    public static void main(String[] args) {\n\n        ConnectionManager cm = ConnectionManager.forH2(\"./data/audiobook_db\");\n        AudiobookRepository repo = new JdbcAudiobookRepository(cm);\n\n        // === Сценарій 1: Простий запит ===\n        System.out.println(\"=== Аудіокниги жанру 'Поезія' ===\");\n        Specification\u003CAudiobook> poetry = new GenreSpecification(\"Поезія\");\n        List\u003CAudiobook> result1 = repo.findAll(poetry);\n        System.out.println(\"Знайдено: \" + result1.size());\n\n        // === Сценарій 2: Композиція через AND ===\n        System.out.println(\"\\n=== Поезія, опублікована після 2020 року ===\");\n        Specification\u003CAudiobook> after2020 = new YearRangeSpecification(2020, null);\n        \n        Specification\u003CAudiobook> query2 = poetry.and(after2020);\n        List\u003CAudiobook> result2 = repo.findAll(query2);\n        System.out.println(\"Знайдено: \" + result2.size());\n\n        // === Сценарій 3: Композиція через OR ===\n        System.out.println(\"\\n=== Поезія АБО проза ===\");\n        Specification\u003CAudiobook> prose = new GenreSpecification(\"Проза\");\n        Specification\u003CAudiobook> poetryOrProse = poetry.or(prose);\n        List\u003CAudiobook> result3 = repo.findAll(poetryOrProse);\n        System.out.println(\"Знайдено: \" + result3.size());\n\n        // === Сценарій 4: Складна композиція ===\n        System.out.println(\"\\n=== (Поезія АБО проза) І (2020-2023) І (тривалість 3600-7200 сек) ===\");\n        Specification\u003CAudiobook> yearRange = new YearRangeSpecification(2020, 2023);\n        Specification\u003CAudiobook> durationRange = new DurationRangeSpecification(3600, 7200); // 1-2 години\n\n        Specification\u003CAudiobook> complexQuery = poetryOrProse\n            .and(yearRange)\n            .and(durationRange);\n\n        List\u003CAudiobook> result4 = repo.findAll(complexQuery);\n        System.out.println(\"Знайдено: \" + result4.size());\n\n        // Виведемо згенерований SQL для діагностики\n        System.out.println(\"\\nЗгенерований SQL WHERE:\");\n        System.out.println(complexQuery.toSql());\n        System.out.println(\"\\nПараметри:\");\n        System.out.println(complexQuery.getParameters());\n\n        // === Сценарій 5: Динамічна побудова запиту ===\n        System.out.println(\"\\n=== Динамічний фільтр (користувацький ввід) ===\");\n        \n        // Імітація користувацького вводу\n        String userGenre = \"Фантастика\";  // може бути null\n        Integer userMinYear = 2015;       // може бути null\n        Integer userMaxDuration = 10800;  // може бути null (3 години у секундах)\n\n        Specification\u003CAudiobook> dynamicQuery = buildDynamicQuery(\n            userGenre, userMinYear, userMaxDuration\n        );\n\n        List\u003CAudiobook> result5 = repo.findAll(dynamicQuery);\n        System.out.println(\"Знайдено: \" + result5.size());\n\n        // === Сценарій 6: Пошук за назвою + жанр ===\n        System.out.println(\"\\n=== Назва містить 'Кобзар' І жанр 'Поезія' ===\");\n        Specification\u003CAudiobook> titleContains = new TitleContainsSpecification(\"Кобзар\");\n        Specification\u003CAudiobook> query6 = titleContains.and(poetry);\n        List\u003CAudiobook> result6 = repo.findAll(query6);\n        System.out.println(\"Знайдено: \" + result6.size());\n\n        cm.close();\n    }\n\n    /**\n     * Будує специфікацію динамічно на основі користувацького вводу.\n     * Якщо параметр null — він не включається у запит.\n     */\n    private static Specification\u003CAudiobook> buildDynamicQuery(\n            String genre, Integer minYear, Integer maxDuration) {\n\n        // Початкова специфікація — завжди true (1=1)\n        Specification\u003CAudiobook> spec = new AlwaysTrueSpecification\u003C>();\n\n        if (genre != null) {\n            spec = spec.and(new GenreSpecification(genre));\n        }\n        if (minYear != null) {\n            spec = spec.and(new YearRangeSpecification(minYear, null));\n        }\n        if (maxDuration != null) {\n            spec = spec.and(new DurationRangeSpecification(null, maxDuration));\n        }\n\n        return spec;\n    }\n}\n",[2968,11781,11782,11789,11793,11799,11805,11811,11818,11824,11831,11835,11841,11845,11856,11860,11883,11887,11911,11928,11932,11937,11959,11984,12008,12035,12039,12044,12070,12098,12102,12126,12150,12176,12180,12185,12208,12232,12254,12278,12304,12308,12313,12336,12366,12401,12405,12419,12429,12438,12442,12465,12491,12495,12500,12523,12545,12568,12590,12594,12599,12622,12626,12631,12649,12666,12683,12687,12707,12712,12716,12720,12744,12770,12774,12779,12802,12828,12851,12875,12901,12905,12917,12921,12925,12929,12934,12939,12943,12961,12982,12986,12991,13012,13016,13027,13047,13051,13061,13084,13088,13098,13121,13125,13129,13136,13140],{"__ignoreMap":2995},[2999,11783,11784,11786],{"class":3001,"line":3002},[2999,11785,4547],{"class":3005},[2999,11787,11788],{"class":3022}," com.example.audiobook;\n",[2999,11790,11791],{"class":3001,"line":3037},[2999,11792,3351],{"emptyLinePlaceholder":3350},[2999,11794,11795,11797],{"class":3001,"line":3067},[2999,11796,5312],{"class":3005},[2999,11798,6859],{"class":3022},[2999,11800,11801,11803],{"class":3001,"line":3098},[2999,11802,5312],{"class":3005},[2999,11804,7807],{"class":3022},[2999,11806,11807,11809],{"class":3001,"line":3219},[2999,11808,5312],{"class":3005},[2999,11810,9539],{"class":3022},[2999,11812,11813,11815],{"class":3001,"line":3364},[2999,11814,5312],{"class":3005},[2999,11816,11817],{"class":3022}," com.example.audiobook.repository.jdbc.JdbcAudiobookRepository;\n",[2999,11819,11820,11822],{"class":3001,"line":3370},[2999,11821,5312],{"class":3005},[2999,11823,6580],{"class":3022},[2999,11825,11826,11828],{"class":3001,"line":3376},[2999,11827,5312],{"class":3005},[2999,11829,11830],{"class":3022}," com.example.audiobook.specification.audiobook.*;\n",[2999,11832,11833],{"class":3001,"line":3382},[2999,11834,3351],{"emptyLinePlaceholder":3350},[2999,11836,11837,11839],{"class":3001,"line":3388},[2999,11838,5312],{"class":3005},[2999,11840,5322],{"class":3022},[2999,11842,11843],{"class":3001,"line":3394},[2999,11844,3351],{"emptyLinePlaceholder":3350},[2999,11846,11847,11849,11851,11854],{"class":3001,"line":3399},[2999,11848,3006],{"class":3005},[2999,11850,5360],{"class":3005},[2999,11852,11853],{"class":3012}," Main",[2999,11855,8215],{"class":3022},[2999,11857,11858],{"class":3001,"line":3405},[2999,11859,3351],{"emptyLinePlaceholder":3350},[2999,11861,11862,11864,11866,11868,11871,11873,11875,11878,11881],{"class":3001,"line":3411},[2999,11863,5432],{"class":3005},[2999,11865,9676],{"class":3005},[2999,11867,10590],{"class":3012},[2999,11869,11870],{"class":3050}," main",[2999,11872,3054],{"class":3022},[2999,11874,3057],{"class":3012},[2999,11876,11877],{"class":3022},"[] ",[2999,11879,11880],{"class":3060},"args",[2999,11882,4047],{"class":3022},[2999,11884,11885],{"class":3001,"line":3417},[2999,11886,3351],{"emptyLinePlaceholder":3350},[2999,11888,11889,11892,11895,11897,11899,11901,11904,11906,11909],{"class":3001,"line":3423},[2999,11890,11891],{"class":3012},"        ConnectionManager",[2999,11893,11894],{"class":3060}," cm",[2999,11896,3702],{"class":3022},[2999,11898,6995],{"class":3060},[2999,11900,3803],{"class":3022},[2999,11902,11903],{"class":3050},"forH2",[2999,11905,3054],{"class":3022},[2999,11907,11908],{"class":3714},"\"./data/audiobook_db\"",[2999,11910,3064],{"class":3022},[2999,11912,11913,11916,11919,11921,11923,11925],{"class":3001,"line":3428},[2999,11914,11915],{"class":3012},"        AudiobookRepository",[2999,11917,11918],{"class":3060}," repo",[2999,11920,3702],{"class":3022},[2999,11922,3706],{"class":3705},[2999,11924,9608],{"class":3050},[2999,11926,11927],{"class":3022},"(cm);\n",[2999,11929,11930],{"class":3001,"line":3433},[2999,11931,3351],{"emptyLinePlaceholder":3350},[2999,11933,11934],{"class":3001,"line":3439},[2999,11935,11936],{"class":3222},"        // === Сценарій 1: Простий запит ===\n",[2999,11938,11939,11942,11944,11947,11949,11952,11954,11957],{"class":3001,"line":3445},[2999,11940,11941],{"class":3060},"        System",[2999,11943,3803],{"class":3022},[2999,11945,11946],{"class":3060},"out",[2999,11948,3803],{"class":3022},[2999,11950,11951],{"class":3050},"println",[2999,11953,3054],{"class":3022},[2999,11955,11956],{"class":3714},"\"=== Аудіокниги жанру 'Поезія' ===\"",[2999,11958,3064],{"class":3022},[2999,11960,11961,11964,11966,11968,11970,11972,11974,11976,11978,11980,11982],{"class":3001,"line":3450},[2999,11962,11963],{"class":3012},"        Specification",[2999,11965,3023],{"class":3022},[2999,11967,3694],{"class":3012},[2999,11969,3047],{"class":3022},[2999,11971,3699],{"class":3060},[2999,11973,3702],{"class":3022},[2999,11975,3706],{"class":3705},[2999,11977,8933],{"class":3050},[2999,11979,3054],{"class":3022},[2999,11981,3715],{"class":3714},[2999,11983,3064],{"class":3022},[2999,11985,11986,11988,11990,11992,11994,11997,11999,12001,12003,12005],{"class":3001,"line":3455},[2999,11987,5651],{"class":3012},[2999,11989,3023],{"class":3022},[2999,11991,3694],{"class":3012},[2999,11993,3047],{"class":3022},[2999,11995,11996],{"class":3060},"result1",[2999,11998,3702],{"class":3022},[2999,12000,4137],{"class":3060},[2999,12002,3803],{"class":3022},[2999,12004,2978],{"class":3050},[2999,12006,12007],{"class":3022},"(poetry);\n",[2999,12009,12010,12012,12014,12016,12018,12020,12022,12025,12027,12029,12031,12033],{"class":3001,"line":3460},[2999,12011,11941],{"class":3060},[2999,12013,3803],{"class":3022},[2999,12015,11946],{"class":3060},[2999,12017,3803],{"class":3022},[2999,12019,11951],{"class":3050},[2999,12021,3054],{"class":3022},[2999,12023,12024],{"class":3714},"\"Знайдено: \"",[2999,12026,5241],{"class":3022},[2999,12028,11996],{"class":3060},[2999,12030,3803],{"class":3022},[2999,12032,7309],{"class":3050},[2999,12034,5691],{"class":3022},[2999,12036,12037],{"class":3001,"line":3466},[2999,12038,3351],{"emptyLinePlaceholder":3350},[2999,12040,12041],{"class":3001,"line":3472},[2999,12042,12043],{"class":3222},"        // === Сценарій 2: Композиція через AND ===\n",[2999,12045,12046,12048,12050,12052,12054,12056,12058,12061,12065,12068],{"class":3001,"line":3477},[2999,12047,11941],{"class":3060},[2999,12049,3803],{"class":3022},[2999,12051,11946],{"class":3060},[2999,12053,3803],{"class":3022},[2999,12055,11951],{"class":3050},[2999,12057,3054],{"class":3022},[2999,12059,12060],{"class":3714},"\"",[2999,12062,12064],{"class":12063},"sjcCO","\\n",[2999,12066,12067],{"class":3714},"=== Поезія, опублікована після 2020 року ===\"",[2999,12069,3064],{"class":3022},[2999,12071,12072,12074,12076,12078,12080,12082,12084,12086,12088,12090,12092,12094,12096],{"class":3001,"line":3482},[2999,12073,11963],{"class":3012},[2999,12075,3023],{"class":3022},[2999,12077,3694],{"class":3012},[2999,12079,3047],{"class":3022},[2999,12081,3757],{"class":3060},[2999,12083,3702],{"class":3022},[2999,12085,3706],{"class":3705},[2999,12087,8415],{"class":3050},[2999,12089,3054],{"class":3022},[2999,12091,3770],{"class":3769},[2999,12093,2975],{"class":3022},[2999,12095,8051],{"class":3005},[2999,12097,3064],{"class":3022},[2999,12099,12100],{"class":3001,"line":3487},[2999,12101,7149],{"class":3022},[2999,12103,12104,12106,12108,12110,12112,12115,12117,12119,12121,12123],{"class":3001,"line":3493},[2999,12105,11963],{"class":3012},[2999,12107,3023],{"class":3022},[2999,12109,3694],{"class":3012},[2999,12111,3047],{"class":3022},[2999,12113,12114],{"class":3060},"query2",[2999,12116,3702],{"class":3022},[2999,12118,3699],{"class":3060},[2999,12120,3803],{"class":3022},[2999,12122,3919],{"class":3050},[2999,12124,12125],{"class":3022},"(after2020);\n",[2999,12127,12128,12130,12132,12134,12136,12139,12141,12143,12145,12147],{"class":3001,"line":3499},[2999,12129,5651],{"class":3012},[2999,12131,3023],{"class":3022},[2999,12133,3694],{"class":3012},[2999,12135,3047],{"class":3022},[2999,12137,12138],{"class":3060},"result2",[2999,12140,3702],{"class":3022},[2999,12142,4137],{"class":3060},[2999,12144,3803],{"class":3022},[2999,12146,2978],{"class":3050},[2999,12148,12149],{"class":3022},"(query2);\n",[2999,12151,12152,12154,12156,12158,12160,12162,12164,12166,12168,12170,12172,12174],{"class":3001,"line":3505},[2999,12153,11941],{"class":3060},[2999,12155,3803],{"class":3022},[2999,12157,11946],{"class":3060},[2999,12159,3803],{"class":3022},[2999,12161,11951],{"class":3050},[2999,12163,3054],{"class":3022},[2999,12165,12024],{"class":3714},[2999,12167,5241],{"class":3022},[2999,12169,12138],{"class":3060},[2999,12171,3803],{"class":3022},[2999,12173,7309],{"class":3050},[2999,12175,5691],{"class":3022},[2999,12177,12178],{"class":3001,"line":3510},[2999,12179,3351],{"emptyLinePlaceholder":3350},[2999,12181,12182],{"class":3001,"line":3515},[2999,12183,12184],{"class":3222},"        // === Сценарій 3: Композиція через OR ===\n",[2999,12186,12187,12189,12191,12193,12195,12197,12199,12201,12203,12206],{"class":3001,"line":3520},[2999,12188,11941],{"class":3060},[2999,12190,3803],{"class":3022},[2999,12192,11946],{"class":3060},[2999,12194,3803],{"class":3022},[2999,12196,11951],{"class":3050},[2999,12198,3054],{"class":3022},[2999,12200,12060],{"class":3714},[2999,12202,12064],{"class":12063},[2999,12204,12205],{"class":3714},"=== Поезія АБО проза ===\"",[2999,12207,3064],{"class":3022},[2999,12209,12210,12212,12214,12216,12218,12220,12222,12224,12226,12228,12230],{"class":3001,"line":3526},[2999,12211,11963],{"class":3012},[2999,12213,3023],{"class":3022},[2999,12215,3694],{"class":3012},[2999,12217,3047],{"class":3022},[2999,12219,3730],{"class":3060},[2999,12221,3702],{"class":3022},[2999,12223,3706],{"class":3705},[2999,12225,8933],{"class":3050},[2999,12227,3054],{"class":3022},[2999,12229,3742],{"class":3714},[2999,12231,3064],{"class":3022},[2999,12233,12234,12236,12238,12240,12242,12244,12246,12248,12250,12252],{"class":3001,"line":3531},[2999,12235,11963],{"class":3012},[2999,12237,3023],{"class":3022},[2999,12239,3694],{"class":3012},[2999,12241,3047],{"class":3022},[2999,12243,3883],{"class":3060},[2999,12245,3702],{"class":3022},[2999,12247,3699],{"class":3060},[2999,12249,3803],{"class":3022},[2999,12251,3892],{"class":3050},[2999,12253,3895],{"class":3022},[2999,12255,12256,12258,12260,12262,12264,12267,12269,12271,12273,12275],{"class":3001,"line":3536},[2999,12257,5651],{"class":3012},[2999,12259,3023],{"class":3022},[2999,12261,3694],{"class":3012},[2999,12263,3047],{"class":3022},[2999,12265,12266],{"class":3060},"result3",[2999,12268,3702],{"class":3022},[2999,12270,4137],{"class":3060},[2999,12272,3803],{"class":3022},[2999,12274,2978],{"class":3050},[2999,12276,12277],{"class":3022},"(poetryOrProse);\n",[2999,12279,12280,12282,12284,12286,12288,12290,12292,12294,12296,12298,12300,12302],{"class":3001,"line":3541},[2999,12281,11941],{"class":3060},[2999,12283,3803],{"class":3022},[2999,12285,11946],{"class":3060},[2999,12287,3803],{"class":3022},[2999,12289,11951],{"class":3050},[2999,12291,3054],{"class":3022},[2999,12293,12024],{"class":3714},[2999,12295,5241],{"class":3022},[2999,12297,12266],{"class":3060},[2999,12299,3803],{"class":3022},[2999,12301,7309],{"class":3050},[2999,12303,5691],{"class":3022},[2999,12305,12306],{"class":3001,"line":3546},[2999,12307,3351],{"emptyLinePlaceholder":3350},[2999,12309,12310],{"class":3001,"line":3551},[2999,12311,12312],{"class":3222},"        // === Сценарій 4: Складна композиція ===\n",[2999,12314,12315,12317,12319,12321,12323,12325,12327,12329,12331,12334],{"class":3001,"line":3557},[2999,12316,11941],{"class":3060},[2999,12318,3803],{"class":3022},[2999,12320,11946],{"class":3060},[2999,12322,3803],{"class":3022},[2999,12324,11951],{"class":3050},[2999,12326,3054],{"class":3022},[2999,12328,12060],{"class":3714},[2999,12330,12064],{"class":12063},[2999,12332,12333],{"class":3714},"=== (Поезія АБО проза) І (2020-2023) І (тривалість 3600-7200 сек) ===\"",[2999,12335,3064],{"class":3022},[2999,12337,12338,12340,12342,12344,12346,12349,12351,12353,12355,12357,12359,12361,12364],{"class":3001,"line":3563},[2999,12339,11963],{"class":3012},[2999,12341,3023],{"class":3022},[2999,12343,3694],{"class":3012},[2999,12345,3047],{"class":3022},[2999,12347,12348],{"class":3060},"yearRange",[2999,12350,3702],{"class":3022},[2999,12352,3706],{"class":3705},[2999,12354,8415],{"class":3050},[2999,12356,3054],{"class":3022},[2999,12358,3770],{"class":3769},[2999,12360,2975],{"class":3022},[2999,12362,12363],{"class":3769},"2023",[2999,12365,3064],{"class":3022},[2999,12367,12368,12370,12372,12374,12376,12379,12381,12383,12385,12387,12390,12392,12395,12398],{"class":3001,"line":3568},[2999,12369,11963],{"class":3012},[2999,12371,3023],{"class":3022},[2999,12373,3694],{"class":3012},[2999,12375,3047],{"class":3022},[2999,12377,12378],{"class":3060},"durationRange",[2999,12380,3702],{"class":3022},[2999,12382,3706],{"class":3705},[2999,12384,7864],{"class":3050},[2999,12386,3054],{"class":3022},[2999,12388,12389],{"class":3769},"3600",[2999,12391,2975],{"class":3022},[2999,12393,12394],{"class":3769},"7200",[2999,12396,12397],{"class":3022},"); ",[2999,12399,12400],{"class":3222},"// 1-2 години\n",[2999,12402,12403],{"class":3001,"line":3573},[2999,12404,3351],{"emptyLinePlaceholder":3350},[2999,12406,12407,12409,12411,12413,12415,12417],{"class":3001,"line":3578},[2999,12408,11963],{"class":3012},[2999,12410,3023],{"class":3022},[2999,12412,3694],{"class":3012},[2999,12414,3047],{"class":3022},[2999,12416,3908],{"class":3060},[2999,12418,3911],{"class":3022},[2999,12420,12421,12424,12426],{"class":3001,"line":3584},[2999,12422,12423],{"class":3022},"            .",[2999,12425,3919],{"class":3050},[2999,12427,12428],{"class":3022},"(yearRange)\n",[2999,12430,12431,12433,12435],{"class":3001,"line":3590},[2999,12432,12423],{"class":3022},[2999,12434,3919],{"class":3050},[2999,12436,12437],{"class":3022},"(durationRange);\n",[2999,12439,12440],{"class":3001,"line":3596},[2999,12441,3351],{"emptyLinePlaceholder":3350},[2999,12443,12444,12446,12448,12450,12452,12455,12457,12459,12461,12463],{"class":3001,"line":3602},[2999,12445,5651],{"class":3012},[2999,12447,3023],{"class":3022},[2999,12449,3694],{"class":3012},[2999,12451,3047],{"class":3022},[2999,12453,12454],{"class":3060},"result4",[2999,12456,3702],{"class":3022},[2999,12458,4137],{"class":3060},[2999,12460,3803],{"class":3022},[2999,12462,2978],{"class":3050},[2999,12464,3974],{"class":3022},[2999,12466,12467,12469,12471,12473,12475,12477,12479,12481,12483,12485,12487,12489],{"class":3001,"line":3608},[2999,12468,11941],{"class":3060},[2999,12470,3803],{"class":3022},[2999,12472,11946],{"class":3060},[2999,12474,3803],{"class":3022},[2999,12476,11951],{"class":3050},[2999,12478,3054],{"class":3022},[2999,12480,12024],{"class":3714},[2999,12482,5241],{"class":3022},[2999,12484,12454],{"class":3060},[2999,12486,3803],{"class":3022},[2999,12488,7309],{"class":3050},[2999,12490,5691],{"class":3022},[2999,12492,12493],{"class":3001,"line":3614},[2999,12494,3351],{"emptyLinePlaceholder":3350},[2999,12496,12497],{"class":3001,"line":3619},[2999,12498,12499],{"class":3222},"        // Виведемо згенерований SQL для діагностики\n",[2999,12501,12502,12504,12506,12508,12510,12512,12514,12516,12518,12521],{"class":3001,"line":3625},[2999,12503,11941],{"class":3060},[2999,12505,3803],{"class":3022},[2999,12507,11946],{"class":3060},[2999,12509,3803],{"class":3022},[2999,12511,11951],{"class":3050},[2999,12513,3054],{"class":3022},[2999,12515,12060],{"class":3714},[2999,12517,12064],{"class":12063},[2999,12519,12520],{"class":3714},"Згенерований SQL WHERE:\"",[2999,12522,3064],{"class":3022},[2999,12524,12525,12527,12529,12531,12533,12535,12537,12539,12541,12543],{"class":3001,"line":3631},[2999,12526,11941],{"class":3060},[2999,12528,3803],{"class":3022},[2999,12530,11946],{"class":3060},[2999,12532,3803],{"class":3022},[2999,12534,11951],{"class":3050},[2999,12536,3054],{"class":3022},[2999,12538,3908],{"class":3060},[2999,12540,3803],{"class":3022},[2999,12542,5244],{"class":3050},[2999,12544,5691],{"class":3022},[2999,12546,12547,12549,12551,12553,12555,12557,12559,12561,12563,12566],{"class":3001,"line":3637},[2999,12548,11941],{"class":3060},[2999,12550,3803],{"class":3022},[2999,12552,11946],{"class":3060},[2999,12554,3803],{"class":3022},[2999,12556,11951],{"class":3050},[2999,12558,3054],{"class":3022},[2999,12560,12060],{"class":3714},[2999,12562,12064],{"class":12063},[2999,12564,12565],{"class":3714},"Параметри:\"",[2999,12567,3064],{"class":3022},[2999,12569,12570,12572,12574,12576,12578,12580,12582,12584,12586,12588],{"class":3001,"line":3643},[2999,12571,11941],{"class":3060},[2999,12573,3803],{"class":3022},[2999,12575,11946],{"class":3060},[2999,12577,3803],{"class":3022},[2999,12579,11951],{"class":3050},[2999,12581,3054],{"class":3022},[2999,12583,3908],{"class":3060},[2999,12585,3803],{"class":3022},[2999,12587,4939],{"class":3050},[2999,12589,5691],{"class":3022},[2999,12591,12592],{"class":3001,"line":4891},[2999,12593,3351],{"emptyLinePlaceholder":3350},[2999,12595,12596],{"class":3001,"line":4897},[2999,12597,12598],{"class":3222},"        // === Сценарій 5: Динамічна побудова запиту ===\n",[2999,12600,12601,12603,12605,12607,12609,12611,12613,12615,12617,12620],{"class":3001,"line":4902},[2999,12602,11941],{"class":3060},[2999,12604,3803],{"class":3022},[2999,12606,11946],{"class":3060},[2999,12608,3803],{"class":3022},[2999,12610,11951],{"class":3050},[2999,12612,3054],{"class":3022},[2999,12614,12060],{"class":3714},[2999,12616,12064],{"class":12063},[2999,12618,12619],{"class":3714},"=== Динамічний фільтр (користувацький ввід) ===\"",[2999,12621,3064],{"class":3022},[2999,12623,12624],{"class":3001,"line":4912},[2999,12625,7149],{"class":3022},[2999,12627,12628],{"class":3001,"line":4917},[2999,12629,12630],{"class":3222},"        // Імітація користувацького вводу\n",[2999,12632,12633,12635,12638,12640,12643,12646],{"class":3001,"line":4944},[2999,12634,7134],{"class":3012},[2999,12636,12637],{"class":3060}," userGenre",[2999,12639,3702],{"class":3022},[2999,12641,12642],{"class":3714},"\"Фантастика\"",[2999,12644,12645],{"class":3022},";  ",[2999,12647,12648],{"class":3222},"// може бути null\n",[2999,12650,12651,12653,12656,12658,12661,12664],{"class":3001,"line":4949},[2999,12652,8025],{"class":3012},[2999,12654,12655],{"class":3060}," userMinYear",[2999,12657,3702],{"class":3022},[2999,12659,12660],{"class":3769},"2015",[2999,12662,12663],{"class":3022},";       ",[2999,12665,12648],{"class":3222},[2999,12667,12668,12670,12673,12675,12678,12680],{"class":3001,"line":4954},[2999,12669,8025],{"class":3012},[2999,12671,12672],{"class":3060}," userMaxDuration",[2999,12674,3702],{"class":3022},[2999,12676,12677],{"class":3769},"10800",[2999,12679,12645],{"class":3022},[2999,12681,12682],{"class":3222},"// може бути null (3 години у секундах)\n",[2999,12684,12685],{"class":3001,"line":4960},[2999,12686,3351],{"emptyLinePlaceholder":3350},[2999,12688,12689,12691,12693,12695,12697,12700,12702,12705],{"class":3001,"line":4965},[2999,12690,11963],{"class":3012},[2999,12692,3023],{"class":3022},[2999,12694,3694],{"class":3012},[2999,12696,3047],{"class":3022},[2999,12698,12699],{"class":3060},"dynamicQuery",[2999,12701,3702],{"class":3022},[2999,12703,12704],{"class":3050},"buildDynamicQuery",[2999,12706,3795],{"class":3022},[2999,12708,12709],{"class":3001,"line":4978},[2999,12710,12711],{"class":3022},"            userGenre, userMinYear, userMaxDuration\n",[2999,12713,12714],{"class":3001,"line":4988},[2999,12715,10343],{"class":3022},[2999,12717,12718],{"class":3001,"line":4993},[2999,12719,3351],{"emptyLinePlaceholder":3350},[2999,12721,12722,12724,12726,12728,12730,12733,12735,12737,12739,12741],{"class":3001,"line":5024},[2999,12723,5651],{"class":3012},[2999,12725,3023],{"class":3022},[2999,12727,3694],{"class":3012},[2999,12729,3047],{"class":3022},[2999,12731,12732],{"class":3060},"result5",[2999,12734,3702],{"class":3022},[2999,12736,4137],{"class":3060},[2999,12738,3803],{"class":3022},[2999,12740,2978],{"class":3050},[2999,12742,12743],{"class":3022},"(dynamicQuery);\n",[2999,12745,12746,12748,12750,12752,12754,12756,12758,12760,12762,12764,12766,12768],{"class":3001,"line":5045},[2999,12747,11941],{"class":3060},[2999,12749,3803],{"class":3022},[2999,12751,11946],{"class":3060},[2999,12753,3803],{"class":3022},[2999,12755,11951],{"class":3050},[2999,12757,3054],{"class":3022},[2999,12759,12024],{"class":3714},[2999,12761,5241],{"class":3022},[2999,12763,12732],{"class":3060},[2999,12765,3803],{"class":3022},[2999,12767,7309],{"class":3050},[2999,12769,5691],{"class":3022},[2999,12771,12772],{"class":3001,"line":5050},[2999,12773,3351],{"emptyLinePlaceholder":3350},[2999,12775,12776],{"class":3001,"line":5055},[2999,12777,12778],{"class":3222},"        // === Сценарій 6: Пошук за назвою + жанр ===\n",[2999,12780,12781,12783,12785,12787,12789,12791,12793,12795,12797,12800],{"class":3001,"line":5060},[2999,12782,11941],{"class":3060},[2999,12784,3803],{"class":3022},[2999,12786,11946],{"class":3060},[2999,12788,3803],{"class":3022},[2999,12790,11951],{"class":3050},[2999,12792,3054],{"class":3022},[2999,12794,12060],{"class":3714},[2999,12796,12064],{"class":12063},[2999,12798,12799],{"class":3714},"=== Назва містить 'Кобзар' І жанр 'Поезія' ===\"",[2999,12801,3064],{"class":3022},[2999,12803,12804,12806,12808,12810,12812,12815,12817,12819,12821,12823,12826],{"class":3001,"line":5066},[2999,12805,11963],{"class":3012},[2999,12807,3023],{"class":3022},[2999,12809,3694],{"class":3012},[2999,12811,3047],{"class":3022},[2999,12813,12814],{"class":3060},"titleContains",[2999,12816,3702],{"class":3022},[2999,12818,3706],{"class":3705},[2999,12820,9232],{"class":3050},[2999,12822,3054],{"class":3022},[2999,12824,12825],{"class":3714},"\"Кобзар\"",[2999,12827,3064],{"class":3022},[2999,12829,12830,12832,12834,12836,12838,12841,12843,12845,12847,12849],{"class":3001,"line":5071},[2999,12831,11963],{"class":3012},[2999,12833,3023],{"class":3022},[2999,12835,3694],{"class":3012},[2999,12837,3047],{"class":3022},[2999,12839,12840],{"class":3060},"query6",[2999,12842,3702],{"class":3022},[2999,12844,12814],{"class":3060},[2999,12846,3803],{"class":3022},[2999,12848,3919],{"class":3050},[2999,12850,12007],{"class":3022},[2999,12852,12853,12855,12857,12859,12861,12864,12866,12868,12870,12872],{"class":3001,"line":5082},[2999,12854,5651],{"class":3012},[2999,12856,3023],{"class":3022},[2999,12858,3694],{"class":3012},[2999,12860,3047],{"class":3022},[2999,12862,12863],{"class":3060},"result6",[2999,12865,3702],{"class":3022},[2999,12867,4137],{"class":3060},[2999,12869,3803],{"class":3022},[2999,12871,2978],{"class":3050},[2999,12873,12874],{"class":3022},"(query6);\n",[2999,12876,12877,12879,12881,12883,12885,12887,12889,12891,12893,12895,12897,12899],{"class":3001,"line":5092},[2999,12878,11941],{"class":3060},[2999,12880,3803],{"class":3022},[2999,12882,11946],{"class":3060},[2999,12884,3803],{"class":3022},[2999,12886,11951],{"class":3050},[2999,12888,3054],{"class":3022},[2999,12890,12024],{"class":3714},[2999,12892,5241],{"class":3022},[2999,12894,12863],{"class":3060},[2999,12896,3803],{"class":3022},[2999,12898,7309],{"class":3050},[2999,12900,5691],{"class":3022},[2999,12902,12903],{"class":3001,"line":5097},[2999,12904,3351],{"emptyLinePlaceholder":3350},[2999,12906,12907,12910,12912,12915],{"class":3001,"line":5126},[2999,12908,12909],{"class":3060},"        cm",[2999,12911,3803],{"class":3022},[2999,12913,12914],{"class":3050},"close",[2999,12916,4859],{"class":3022},[2999,12918,12919],{"class":3001,"line":5142},[2999,12920,3391],{"class":3022},[2999,12922,12923],{"class":3001,"line":5147},[2999,12924,3351],{"emptyLinePlaceholder":3350},[2999,12926,12927],{"class":3001,"line":5152},[2999,12928,4727],{"class":3222},[2999,12930,12931],{"class":3001,"line":5157},[2999,12932,12933],{"class":3222},"     * Будує специфікацію динамічно на основі користувацького вводу.\n",[2999,12935,12936],{"class":3001,"line":5163},[2999,12937,12938],{"class":3222},"     * Якщо параметр null — він не включається у запит.\n",[2999,12940,12941],{"class":3001,"line":5168},[2999,12942,4780],{"class":3222},[2999,12944,12945,12947,12949,12951,12953,12955,12957,12959],{"class":3001,"line":5178},[2999,12946,5388],{"class":3005},[2999,12948,9676],{"class":3005},[2999,12950,4711],{"class":3012},[2999,12952,3023],{"class":3022},[2999,12954,3694],{"class":3012},[2999,12956,3047],{"class":3022},[2999,12958,12704],{"class":3050},[2999,12960,3795],{"class":3022},[2999,12962,12963,12966,12968,12970,12972,12974,12976,12978,12980],{"class":3001,"line":5183},[2999,12964,12965],{"class":3012},"            String",[2999,12967,10357],{"class":3060},[2999,12969,2975],{"class":3022},[2999,12971,3168],{"class":3012},[2999,12973,8440],{"class":3060},[2999,12975,2975],{"class":3022},[2999,12977,3168],{"class":3012},[2999,12979,7908],{"class":3060},[2999,12981,4047],{"class":3022},[2999,12983,12984],{"class":3001,"line":5202},[2999,12985,3351],{"emptyLinePlaceholder":3350},[2999,12987,12988],{"class":3001,"line":5218},[2999,12989,12990],{"class":3222},"        // Початкова специфікація — завжди true (1=1)\n",[2999,12992,12993,12995,12997,12999,13001,13003,13005,13007,13010],{"class":3001,"line":5223},[2999,12994,11963],{"class":3012},[2999,12996,3023],{"class":3022},[2999,12998,3694],{"class":3012},[2999,13000,3047],{"class":3022},[2999,13002,4230],{"class":3060},[2999,13004,3702],{"class":3022},[2999,13006,3706],{"class":3705},[2999,13008,13009],{"class":3012}," AlwaysTrueSpecification",[2999,13011,5670],{"class":3022},[2999,13013,13014],{"class":3001,"line":10085},[2999,13015,3351],{"emptyLinePlaceholder":3350},[2999,13017,13018,13020,13023,13025],{"class":3001,"line":10093},[2999,13019,8045],{"class":3705},[2999,13021,13022],{"class":3022}," (genre != ",[2999,13024,8051],{"class":3005},[2999,13026,4047],{"class":3022},[2999,13028,13029,13032,13034,13036,13038,13040,13042,13044],{"class":3001,"line":10098},[2999,13030,13031],{"class":3022},"            spec = ",[2999,13033,4230],{"class":3060},[2999,13035,3803],{"class":3022},[2999,13037,3919],{"class":3050},[2999,13039,3054],{"class":3022},[2999,13041,3706],{"class":3705},[2999,13043,8933],{"class":3050},[2999,13045,13046],{"class":3022},"(genre));\n",[2999,13048,13049],{"class":3001,"line":10103},[2999,13050,7471],{"class":3022},[2999,13052,13053,13055,13057,13059],{"class":3001,"line":10110},[2999,13054,8045],{"class":3705},[2999,13056,8670],{"class":3022},[2999,13058,8051],{"class":3005},[2999,13060,4047],{"class":3022},[2999,13062,13063,13065,13067,13069,13071,13073,13075,13077,13080,13082],{"class":3001,"line":10122},[2999,13064,13031],{"class":3022},[2999,13066,4230],{"class":3060},[2999,13068,3803],{"class":3022},[2999,13070,3919],{"class":3050},[2999,13072,3054],{"class":3022},[2999,13074,3706],{"class":3705},[2999,13076,8415],{"class":3050},[2999,13078,13079],{"class":3022},"(minYear, ",[2999,13081,8051],{"class":3005},[2999,13083,4337],{"class":3022},[2999,13085,13086],{"class":3001,"line":10130},[2999,13087,7471],{"class":3022},[2999,13089,13090,13092,13094,13096],{"class":3001,"line":10135},[2999,13091,8045],{"class":3705},[2999,13093,8193],{"class":3022},[2999,13095,8051],{"class":3005},[2999,13097,4047],{"class":3022},[2999,13099,13100,13102,13104,13106,13108,13110,13112,13114,13116,13118],{"class":3001,"line":10140},[2999,13101,13031],{"class":3022},[2999,13103,4230],{"class":3060},[2999,13105,3803],{"class":3022},[2999,13107,3919],{"class":3050},[2999,13109,3054],{"class":3022},[2999,13111,3706],{"class":3705},[2999,13113,7864],{"class":3050},[2999,13115,3054],{"class":3022},[2999,13117,8051],{"class":3005},[2999,13119,13120],{"class":3022},", maxDuration));\n",[2999,13122,13123],{"class":3001,"line":10147},[2999,13124,7471],{"class":3022},[2999,13126,13127],{"class":3001,"line":10159},[2999,13128,3351],{"emptyLinePlaceholder":3350},[2999,13130,13131,13133],{"class":3001,"line":10167},[2999,13132,5027],{"class":3705},[2999,13134,13135],{"class":3022}," spec;\n",[2999,13137,13138],{"class":3001,"line":10172},[2999,13139,3391],{"class":3022},[2999,13141,13142],{"class":3001,"line":10177},[2999,13143,3101],{"class":3022},[2964,13145,13146],{},"Результат виконання:",[13148,13149,13152,13166,13174,13178,13181,13188,13192,13195,13202,13206,13209,13216,13220,13223,13231,13235,13238,13245,13249,13252,13259,13263,13266,13273],"terminal-preview",{":cursor":13150,"title":13151},"false","java Main",[13153,13154,13156,4371,13161],"div",{"className":13155},[3001],[2999,13157,13160],{"className":13158},[13159],"opacity-40","$",[3228,13162,13165],{"className":13163},[13164],"font-bold","java -cp . com.example.audiobook.Main",[13153,13167,13169],{"className":13168},[3001],[2999,13170,13173],{"className":13171},[13172,13164],"text-blue-400","=== Аудіокниги жанру 'Поезія' ===",[13153,13175,13177],{"className":13176},[3001],"Знайдено: 23",[13153,13179],{"className":13180},[3001],[13153,13182,13184],{"className":13183},[3001],[2999,13185,13187],{"className":13186},[13172,13164],"=== Поезія, опублікована після 2020 року ===",[13153,13189,13191],{"className":13190},[3001],"Знайдено: 8",[13153,13193],{"className":13194},[3001],[13153,13196,13198],{"className":13197},[3001],[2999,13199,13201],{"className":13200},[13172,13164],"=== Поезія АБО проза ===",[13153,13203,13205],{"className":13204},[3001],"Знайдено: 67",[13153,13207],{"className":13208},[3001],[13153,13210,13212],{"className":13211},[3001],[2999,13213,13215],{"className":13214},[13172,13164],"=== (Поезія АБО проза) І (2020-2023) І (тривалість 3600-7200 сек) ===",[13153,13217,13219],{"className":13218},[3001],"Знайдено: 12",[13153,13221],{"className":13222},[3001],[13153,13224,13226],{"className":13225},[3001],[2999,13227,13230],{"className":13228},[13229],"text-yellow-400","Згенерований SQL WHERE:",[13153,13232,13234],{"className":13233},[3001],"((g.name = ?) OR (g.name = ?)) AND (ab.release_year BETWEEN ? AND ?) AND (ab.duration BETWEEN ? AND ?)",[13153,13236],{"className":13237},[3001],[13153,13239,13241],{"className":13240},[3001],[2999,13242,13244],{"className":13243},[13229],"Параметри:",[13153,13246,13248],{"className":13247},[3001],"[Поезія, Проза, 2020, 2023, 3600, 7200]",[13153,13250],{"className":13251},[3001],[13153,13253,13255],{"className":13254},[3001],[2999,13256,13258],{"className":13257},[13172,13164],"=== Динамічний фільтр (користувацький ввід) ===",[13153,13260,13262],{"className":13261},[3001],"Знайдено: 15",[13153,13264],{"className":13265},[3001],[13153,13267,13269],{"className":13268},[3001],[2999,13270,13272],{"className":13271},[13172,13164],"=== Назва містить 'Кобзар' І жанр 'Поезія' ===",[13153,13274,13276],{"className":13275},[3001],"Знайдено: 3",[2964,13278,13279],{},[3228,13280,13281],{},"Ключові спостереження:",[4397,13283,13284,13293,13302,13322,13333],{},[4400,13285,13286,5238,13289,13292],{},[3228,13287,13288],{},"Рядок 29",[2968,13290,13291],{},"poetry.and(after2020)","): дві специфікації комбінуються через fluent API. Жодного нового методу у репозиторії не потрібно.",[4400,13294,13295,5238,13298,13301],{},[3228,13296,13297],{},"Рядок 36",[2968,13299,13300],{},"poetry.or(prose)","): логічне OR створює нову специфікацію, що може бути далі скомбінована через AND.",[4400,13303,13304,13307,13308,3656,13310,13312,13313,2975,13316,2975,13319,3803],{},[3228,13305,13306],{},"Рядок 57"," (згенерований SQL): дужки автоматично розставлені правильно завдяки ",[2968,13309,3652],{},[2968,13311,3655],{},". Використовуються реальні поля: ",[2968,13314,13315],{},"ab.release_year",[2968,13317,13318],{},"ab.duration",[2968,13320,13321],{},"g.name",[4400,13323,13324,5238,13327,13329,13330,13332],{},[3228,13325,13326],{},"Рядки 90–105",[2968,13328,12704],{},"): динамічна побудова запиту на основі користувацького вводу. Якщо параметр ",[2968,13331,8051],{}," — він не включається у WHERE-умову.",[4400,13334,13335,13338,13339,13341,13342,3803],{},[3228,13336,13337],{},"Рядок 78"," (пошук за назвою): ",[2968,13340,9160],{}," демонструє роботу з текстовими полями через ",[2968,13343,13344],{},"LIKE",[3278,13346],{},[2959,13348,13350],{"id":13349},"alwaystruespecification-нейтральний-елемент-композиції","AlwaysTrueSpecification: Нейтральний елемент композиції",[2964,13352,13353,13354,6557],{},"Для динамічних запитів корисна спеціальна специфікація, що завжди повертає ",[2968,13355,13356],{},"true",[2990,13358,13360],{"className":2992,"code":13359,"language":2994,"meta":4540,"style":2995},"package com.example.audiobook.specification;\n\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Специфікація, що завжди задоволена (нейтральний елемент для AND).\n * \u003Cp>\n * SQL: {@code 1=1} (завжди true).\n * \u003Cp>\n * Використовується як початкова точка для динамічної побудови запитів:\n * \u003Cpre>{@code\n * Specification\u003CT> spec = new AlwaysTrueSpecification\u003C>();\n * if (condition1) spec = spec.and(new SomeSpec());\n * if (condition2) spec = spec.and(new OtherSpec());\n * }\u003C/pre>\n */\npublic class AlwaysTrueSpecification\u003CT> implements Specification\u003CT> {\n\n    @Override\n    public boolean isSatisfiedBy(T candidate) {\n        return true; // завжди задоволена\n    }\n\n    @Override\n    public String toSql() {\n        return \"1=1\"; // SQL-константа, що завжди true\n    }\n\n    @Override\n    public List\u003CObject> getParameters() {\n        return Collections.emptyList(); // немає параметрів\n    }\n}\n",[2968,13361,13362,13368,13372,13379,13385,13389,13393,13398,13402,13407,13411,13416,13420,13425,13430,13435,13439,13443,13467,13471,13477,13493,13505,13509,13513,13519,13529,13540,13544,13548,13554,13570,13588,13592],{"__ignoreMap":2995},[2999,13363,13364,13366],{"class":3001,"line":3002},[2999,13365,4547],{"class":3005},[2999,13367,4550],{"class":3022},[2999,13369,13370],{"class":3001,"line":3037},[2999,13371,3351],{"emptyLinePlaceholder":3350},[2999,13373,13374,13376],{"class":3001,"line":3067},[2999,13375,5312],{"class":3005},[2999,13377,13378],{"class":3022}," java.util.Collections;\n",[2999,13380,13381,13383],{"class":3001,"line":3098},[2999,13382,5312],{"class":3005},[2999,13384,5322],{"class":3022},[2999,13386,13387],{"class":3001,"line":3219},[2999,13388,3351],{"emptyLinePlaceholder":3350},[2999,13390,13391],{"class":3001,"line":3364},[2999,13392,4559],{"class":3222},[2999,13394,13395],{"class":3001,"line":3370},[2999,13396,13397],{"class":3222}," * Специфікація, що завжди задоволена (нейтральний елемент для AND).\n",[2999,13399,13400],{"class":3001,"line":3376},[2999,13401,4569],{"class":3222},[2999,13403,13404],{"class":3001,"line":3382},[2999,13405,13406],{"class":3222}," * SQL: {@code 1=1} (завжди true).\n",[2999,13408,13409],{"class":3001,"line":3388},[2999,13410,4569],{"class":3222},[2999,13412,13413],{"class":3001,"line":3394},[2999,13414,13415],{"class":3222}," * Використовується як початкова точка для динамічної побудови запитів:\n",[2999,13417,13418],{"class":3001,"line":3399},[2999,13419,4613],{"class":3222},[2999,13421,13422],{"class":3001,"line":3405},[2999,13423,13424],{"class":3222}," * Specification\u003CT> spec = new AlwaysTrueSpecification\u003C>();\n",[2999,13426,13427],{"class":3001,"line":3411},[2999,13428,13429],{"class":3222}," * if (condition1) spec = spec.and(new SomeSpec());\n",[2999,13431,13432],{"class":3001,"line":3417},[2999,13433,13434],{"class":3222}," * if (condition2) spec = spec.and(new OtherSpec());\n",[2999,13436,13437],{"class":3001,"line":3423},[2999,13438,4678],{"class":3222},[2999,13440,13441],{"class":3001,"line":3428},[2999,13442,4702],{"class":3222},[2999,13444,13445,13447,13449,13451,13453,13455,13457,13459,13461,13463,13465],{"class":3001,"line":3433},[2999,13446,3006],{"class":3005},[2999,13448,5360],{"class":3005},[2999,13450,13009],{"class":3012},[2999,13452,3023],{"class":3022},[2999,13454,4716],{"class":3012},[2999,13456,3047],{"class":3022},[2999,13458,5371],{"class":3005},[2999,13460,4711],{"class":3012},[2999,13462,3023],{"class":3022},[2999,13464,4716],{"class":3012},[2999,13466,3034],{"class":3022},[2999,13468,13469],{"class":3001,"line":3439},[2999,13470,3351],{"emptyLinePlaceholder":3350},[2999,13472,13473,13475],{"class":3001,"line":3445},[2999,13474,5496],{"class":3022},[2999,13476,5499],{"class":3012},[2999,13478,13479,13481,13483,13485,13487,13489,13491],{"class":3001,"line":3450},[2999,13480,5432],{"class":3005},[2999,13482,5506],{"class":3012},[2999,13484,4788],{"class":3050},[2999,13486,3054],{"class":3022},[2999,13488,4716],{"class":3012},[2999,13490,4762],{"class":3060},[2999,13492,4047],{"class":3022},[2999,13494,13495,13497,13500,13502],{"class":3001,"line":3455},[2999,13496,5027],{"class":3705},[2999,13498,13499],{"class":3005}," true",[2999,13501,7894],{"class":3022},[2999,13503,13504],{"class":3222},"// завжди задоволена\n",[2999,13506,13507],{"class":3001,"line":3460},[2999,13508,3391],{"class":3022},[2999,13510,13511],{"class":3001,"line":3466},[2999,13512,3351],{"emptyLinePlaceholder":3350},[2999,13514,13515,13517],{"class":3001,"line":3472},[2999,13516,5496],{"class":3022},[2999,13518,5499],{"class":3012},[2999,13520,13521,13523,13525,13527],{"class":3001,"line":3477},[2999,13522,5432],{"class":3005},[2999,13524,5565],{"class":3012},[2999,13526,4856],{"class":3050},[2999,13528,5199],{"class":3022},[2999,13530,13531,13533,13535,13537],{"class":3001,"line":3482},[2999,13532,5027],{"class":3705},[2999,13534,8222],{"class":3714},[2999,13536,7894],{"class":3022},[2999,13538,13539],{"class":3222},"// SQL-константа, що завжди true\n",[2999,13541,13542],{"class":3001,"line":3487},[2999,13543,3391],{"class":3022},[2999,13545,13546],{"class":3001,"line":3493},[2999,13547,3351],{"emptyLinePlaceholder":3350},[2999,13549,13550,13552],{"class":3001,"line":3499},[2999,13551,5496],{"class":3022},[2999,13553,5499],{"class":3012},[2999,13555,13556,13558,13560,13562,13564,13566,13568],{"class":3001,"line":3505},[2999,13557,5432],{"class":3005},[2999,13559,5631],{"class":3012},[2999,13561,3023],{"class":3022},[2999,13563,4934],{"class":3012},[2999,13565,3047],{"class":3022},[2999,13567,4939],{"class":3050},[2999,13569,5199],{"class":3022},[2999,13571,13572,13574,13577,13579,13582,13585],{"class":3001,"line":3510},[2999,13573,5027],{"class":3705},[2999,13575,13576],{"class":3060}," Collections",[2999,13578,3803],{"class":3022},[2999,13580,13581],{"class":3050},"emptyList",[2999,13583,13584],{"class":3022},"(); ",[2999,13586,13587],{"class":3222},"// немає параметрів\n",[2999,13589,13590],{"class":3001,"line":3515},[2999,13591,3391],{"class":3022},[2999,13593,13594],{"class":3001,"line":3520},[2999,13595,3101],{"class":3022},[2964,13597,13598,13599,13602,13603,13606],{},"Ця специфікація є ",[3228,13600,13601],{},"нейтральним елементом"," для операції AND: ",[2968,13604,13605],{},"AlwaysTrue AND X = X",". Це дозволяє писати динамічні запити без спеціальної обробки першого елемента.",[3278,13608],{},[2959,13610,13612],{"id":13611},"порівняння-до-і-після-specification-pattern","Порівняння: До і після Specification Pattern",[2964,13614,13615],{},"Підсумуємо, що змінилося у архітектурі після впровадження Specification Pattern:",[3979,13617,13618,13914],{},[3982,13619,13621],{"label":13620},"До (стаття 14)",[2990,13622,13624],{"className":2992,"code":13623,"language":2994,"meta":2995,"style":2995},"// Репозиторій містить десятки спеціалізованих методів\npublic interface AudiobookRepository extends Repository\u003CAudiobook, UUID> {\n    List\u003CAudiobook> findByAuthorId(UUID authorId);\n    List\u003CAudiobook> findByGenreName(String genreName);\n    \n    // Для кожної комбінації критеріїв — окремий метод\n    List\u003CAudiobook> findByGenreAndYear(String genre, Integer year);\n    List\u003CAudiobook> findByGenreAndYearRange(String genre, Integer minYear, Integer maxYear);\n    List\u003CAudiobook> findByGenreAndMinDuration(String genre, Integer minDuration);\n    List\u003CAudiobook> findByYearAndDuration(Integer year, Integer minDuration);\n    List\u003CAudiobook> findByGenreAndYearAndDuration(String genre, Integer year, Integer duration);\n    // ... ще десятки методів для всіх комбінацій\n}\n\n// Клієнтський код жорстко прив'язаний до методів\nList\u003CAudiobook> books = repo.findByGenreAndYearAndDuration(\n    \"Поезія\", 2020, 3600\n);\n\n// Нова комбінація критеріїв = новий метод у репозиторії\n",[2968,13625,13626,13631,13653,13673,13693,13698,13703,13729,13761,13787,13814,13847,13852,13856,13860,13865,13887,13901,13905,13909],{"__ignoreMap":2995},[2999,13627,13628],{"class":3001,"line":3002},[2999,13629,13630],{"class":3222},"// Репозиторій містить десятки спеціалізованих методів\n",[2999,13632,13633,13635,13637,13639,13641,13643,13645,13647,13649,13651],{"class":3001,"line":3037},[2999,13634,3006],{"class":3005},[2999,13636,3009],{"class":3005},[2999,13638,9636],{"class":3012},[2999,13640,3016],{"class":3005},[2999,13642,3019],{"class":3012},[2999,13644,3023],{"class":3022},[2999,13646,3694],{"class":3012},[2999,13648,2975],{"class":3022},[2999,13650,3031],{"class":3012},[2999,13652,3034],{"class":3022},[2999,13654,13655,13657,13659,13661,13663,13665,13667,13669,13671],{"class":3001,"line":3067},[2999,13656,3040],{"class":3012},[2999,13658,3023],{"class":3022},[2999,13660,3694],{"class":3012},[2999,13662,3047],{"class":3022},[2999,13664,11179],{"class":3050},[2999,13666,3054],{"class":3022},[2999,13668,3031],{"class":3012},[2999,13670,11186],{"class":3060},[2999,13672,3064],{"class":3022},[2999,13674,13675,13677,13679,13681,13683,13685,13687,13689,13691],{"class":3001,"line":3098},[2999,13676,3040],{"class":3012},[2999,13678,3023],{"class":3022},[2999,13680,3694],{"class":3012},[2999,13682,3047],{"class":3022},[2999,13684,11443],{"class":3050},[2999,13686,3054],{"class":3022},[2999,13688,3057],{"class":3012},[2999,13690,8958],{"class":3060},[2999,13692,3064],{"class":3022},[2999,13694,13695],{"class":3001,"line":3219},[2999,13696,13697],{"class":3022},"    \n",[2999,13699,13700],{"class":3001,"line":3364},[2999,13701,13702],{"class":3222},"    // Для кожної комбінації критеріїв — окремий метод\n",[2999,13704,13705,13707,13709,13711,13713,13715,13717,13719,13721,13723,13725,13727],{"class":3001,"line":3370},[2999,13706,3040],{"class":3012},[2999,13708,3023],{"class":3022},[2999,13710,3694],{"class":3012},[2999,13712,3047],{"class":3022},[2999,13714,3158],{"class":3050},[2999,13716,3054],{"class":3022},[2999,13718,3057],{"class":3012},[2999,13720,10357],{"class":3060},[2999,13722,2975],{"class":3022},[2999,13724,3168],{"class":3012},[2999,13726,6453],{"class":3060},[2999,13728,3064],{"class":3022},[2999,13730,13731,13733,13735,13737,13739,13741,13743,13745,13747,13749,13751,13753,13755,13757,13759],{"class":3001,"line":3376},[2999,13732,3040],{"class":3012},[2999,13734,3023],{"class":3022},[2999,13736,3694],{"class":3012},[2999,13738,3047],{"class":3022},[2999,13740,3180],{"class":3050},[2999,13742,3054],{"class":3022},[2999,13744,3057],{"class":3012},[2999,13746,10357],{"class":3060},[2999,13748,2975],{"class":3022},[2999,13750,3168],{"class":3012},[2999,13752,8440],{"class":3060},[2999,13754,2975],{"class":3022},[2999,13756,3168],{"class":3012},[2999,13758,8453],{"class":3060},[2999,13760,3064],{"class":3022},[2999,13762,13763,13765,13767,13769,13771,13773,13775,13777,13779,13781,13783,13785],{"class":3001,"line":3382},[2999,13764,3040],{"class":3012},[2999,13766,3023],{"class":3022},[2999,13768,3694],{"class":3012},[2999,13770,3047],{"class":3022},[2999,13772,3205],{"class":3050},[2999,13774,3054],{"class":3022},[2999,13776,3057],{"class":3012},[2999,13778,10357],{"class":3060},[2999,13780,2975],{"class":3022},[2999,13782,3168],{"class":3012},[2999,13784,7891],{"class":3060},[2999,13786,3064],{"class":3022},[2999,13788,13789,13791,13793,13795,13797,13800,13802,13804,13806,13808,13810,13812],{"class":3001,"line":3388},[2999,13790,3040],{"class":3012},[2999,13792,3023],{"class":3022},[2999,13794,3694],{"class":3012},[2999,13796,3047],{"class":3022},[2999,13798,13799],{"class":3050},"findByYearAndDuration",[2999,13801,3054],{"class":3022},[2999,13803,3168],{"class":3012},[2999,13805,6453],{"class":3060},[2999,13807,2975],{"class":3022},[2999,13809,3168],{"class":3012},[2999,13811,7891],{"class":3060},[2999,13813,3064],{"class":3022},[2999,13815,13816,13818,13820,13822,13824,13827,13829,13831,13833,13835,13837,13839,13841,13843,13845],{"class":3001,"line":3394},[2999,13817,3040],{"class":3012},[2999,13819,3023],{"class":3022},[2999,13821,3694],{"class":3012},[2999,13823,3047],{"class":3022},[2999,13825,13826],{"class":3050},"findByGenreAndYearAndDuration",[2999,13828,3054],{"class":3022},[2999,13830,3057],{"class":3012},[2999,13832,10357],{"class":3060},[2999,13834,2975],{"class":3022},[2999,13836,3168],{"class":3012},[2999,13838,6453],{"class":3060},[2999,13840,2975],{"class":3022},[2999,13842,3168],{"class":3012},[2999,13844,8028],{"class":3060},[2999,13846,3064],{"class":3022},[2999,13848,13849],{"class":3001,"line":3399},[2999,13850,13851],{"class":3222},"    // ... ще десятки методів для всіх комбінацій\n",[2999,13853,13854],{"class":3001,"line":3405},[2999,13855,3101],{"class":3022},[2999,13857,13858],{"class":3001,"line":3411},[2999,13859,3351],{"emptyLinePlaceholder":3350},[2999,13861,13862],{"class":3001,"line":3417},[2999,13863,13864],{"class":3222},"// Клієнтський код жорстко прив'язаний до методів\n",[2999,13866,13867,13869,13871,13873,13875,13877,13879,13881,13883,13885],{"class":3001,"line":3423},[2999,13868,3136],{"class":3012},[2999,13870,3023],{"class":3022},[2999,13872,3694],{"class":3012},[2999,13874,3047],{"class":3022},[2999,13876,11202],{"class":3060},[2999,13878,3702],{"class":3022},[2999,13880,4137],{"class":3060},[2999,13882,3803],{"class":3022},[2999,13884,13826],{"class":3050},[2999,13886,3795],{"class":3022},[2999,13888,13889,13892,13894,13896,13898],{"class":3001,"line":3428},[2999,13890,13891],{"class":3714},"    \"Поезія\"",[2999,13893,2975],{"class":3022},[2999,13895,3770],{"class":3769},[2999,13897,2975],{"class":3022},[2999,13899,13900],{"class":3769},"3600\n",[2999,13902,13903],{"class":3001,"line":3433},[2999,13904,3064],{"class":3022},[2999,13906,13907],{"class":3001,"line":3439},[2999,13908,3351],{"emptyLinePlaceholder":3350},[2999,13910,13911],{"class":3001,"line":3445},[2999,13912,13913],{"class":3222},"// Нова комбінація критеріїв = новий метод у репозиторії\n",[3982,13915,13917],{"label":13916},"Після (стаття 20)",[2990,13918,13920],{"className":2992,"code":13919,"language":2994,"meta":2995,"style":2995},"// Репозиторій зберігає специфічні методи + додає універсальний\npublic interface AudiobookRepository extends Repository\u003CAudiobook, UUID> {\n    // Специфічні методи — ЗАЛИШАЮТЬСЯ для простих запитів\n    List\u003CAudiobook> findByAuthorId(UUID authorId);\n    List\u003CAudiobook> findByGenreName(String genreName);\n    \n    // Універсальний метод для складних запитів — ДОДАЄТЬСЯ\n    List\u003CAudiobook> findAll(Specification\u003CAudiobook> spec);\n}\n\n// Клієнтський код будує запит через композицію\nSpecification\u003CAudiobook> spec = new GenreSpecification(\"Поезія\")\n    .and(new YearRangeSpecification(2020, null))\n    .and(new DurationRangeSpecification(3600, null));\n\nList\u003CAudiobook> books = repo.findAll(spec);\n\n// Нова комбінація = нова композиція існуючих специфікацій\n// Репозиторій НЕ змінюється\n",[2968,13921,13922,13927,13949,13954,13974,13994,13998,14003,14029,14033,14037,14042,14066,14088,14110,14114,14136,14140,14145],{"__ignoreMap":2995},[2999,13923,13924],{"class":3001,"line":3002},[2999,13925,13926],{"class":3222},"// Репозиторій зберігає специфічні методи + додає універсальний\n",[2999,13928,13929,13931,13933,13935,13937,13939,13941,13943,13945,13947],{"class":3001,"line":3037},[2999,13930,3006],{"class":3005},[2999,13932,3009],{"class":3005},[2999,13934,9636],{"class":3012},[2999,13936,3016],{"class":3005},[2999,13938,3019],{"class":3012},[2999,13940,3023],{"class":3022},[2999,13942,3694],{"class":3012},[2999,13944,2975],{"class":3022},[2999,13946,3031],{"class":3012},[2999,13948,3034],{"class":3022},[2999,13950,13951],{"class":3001,"line":3067},[2999,13952,13953],{"class":3222},"    // Специфічні методи — ЗАЛИШАЮТЬСЯ для простих запитів\n",[2999,13955,13956,13958,13960,13962,13964,13966,13968,13970,13972],{"class":3001,"line":3098},[2999,13957,3040],{"class":3012},[2999,13959,3023],{"class":3022},[2999,13961,3694],{"class":3012},[2999,13963,3047],{"class":3022},[2999,13965,11179],{"class":3050},[2999,13967,3054],{"class":3022},[2999,13969,3031],{"class":3012},[2999,13971,11186],{"class":3060},[2999,13973,3064],{"class":3022},[2999,13975,13976,13978,13980,13982,13984,13986,13988,13990,13992],{"class":3001,"line":3219},[2999,13977,3040],{"class":3012},[2999,13979,3023],{"class":3022},[2999,13981,3694],{"class":3012},[2999,13983,3047],{"class":3022},[2999,13985,11443],{"class":3050},[2999,13987,3054],{"class":3022},[2999,13989,3057],{"class":3012},[2999,13991,8958],{"class":3060},[2999,13993,3064],{"class":3022},[2999,13995,13996],{"class":3001,"line":3364},[2999,13997,13697],{"class":3022},[2999,13999,14000],{"class":3001,"line":3370},[2999,14001,14002],{"class":3222},"    // Універсальний метод для складних запитів — ДОДАЄТЬСЯ\n",[2999,14004,14005,14007,14009,14011,14013,14015,14017,14019,14021,14023,14025,14027],{"class":3001,"line":3376},[2999,14006,3040],{"class":3012},[2999,14008,3023],{"class":3022},[2999,14010,3694],{"class":3012},[2999,14012,3047],{"class":3022},[2999,14014,2978],{"class":3050},[2999,14016,3054],{"class":3022},[2999,14018,3267],{"class":3012},[2999,14020,3023],{"class":3022},[2999,14022,3694],{"class":3012},[2999,14024,3047],{"class":3022},[2999,14026,4230],{"class":3060},[2999,14028,3064],{"class":3022},[2999,14030,14031],{"class":3001,"line":3382},[2999,14032,3101],{"class":3022},[2999,14034,14035],{"class":3001,"line":3388},[2999,14036,3351],{"emptyLinePlaceholder":3350},[2999,14038,14039],{"class":3001,"line":3394},[2999,14040,14041],{"class":3222},"// Клієнтський код будує запит через композицію\n",[2999,14043,14044,14046,14048,14050,14052,14054,14056,14058,14060,14062,14064],{"class":3001,"line":3399},[2999,14045,3267],{"class":3012},[2999,14047,3023],{"class":3022},[2999,14049,3694],{"class":3012},[2999,14051,3047],{"class":3022},[2999,14053,4230],{"class":3060},[2999,14055,3702],{"class":3022},[2999,14057,3706],{"class":3705},[2999,14059,8933],{"class":3050},[2999,14061,3054],{"class":3022},[2999,14063,3715],{"class":3714},[2999,14065,3830],{"class":3022},[2999,14067,14068,14070,14072,14074,14076,14078,14080,14082,14084,14086],{"class":3001,"line":3405},[2999,14069,3916],{"class":3022},[2999,14071,3919],{"class":3050},[2999,14073,3054],{"class":3022},[2999,14075,3706],{"class":3705},[2999,14077,8415],{"class":3050},[2999,14079,3054],{"class":3022},[2999,14081,3770],{"class":3769},[2999,14083,2975],{"class":3022},[2999,14085,8051],{"class":3005},[2999,14087,4261],{"class":3022},[2999,14089,14090,14092,14094,14096,14098,14100,14102,14104,14106,14108],{"class":3001,"line":3411},[2999,14091,3916],{"class":3022},[2999,14093,3919],{"class":3050},[2999,14095,3054],{"class":3022},[2999,14097,3706],{"class":3705},[2999,14099,7864],{"class":3050},[2999,14101,3054],{"class":3022},[2999,14103,12389],{"class":3769},[2999,14105,2975],{"class":3022},[2999,14107,8051],{"class":3005},[2999,14109,4337],{"class":3022},[2999,14111,14112],{"class":3001,"line":3417},[2999,14113,3351],{"emptyLinePlaceholder":3350},[2999,14115,14116,14118,14120,14122,14124,14126,14128,14130,14132,14134],{"class":3001,"line":3423},[2999,14117,3136],{"class":3012},[2999,14119,3023],{"class":3022},[2999,14121,3694],{"class":3012},[2999,14123,3047],{"class":3022},[2999,14125,11202],{"class":3060},[2999,14127,3702],{"class":3022},[2999,14129,4137],{"class":3060},[2999,14131,3803],{"class":3022},[2999,14133,2978],{"class":3050},[2999,14135,4364],{"class":3022},[2999,14137,14138],{"class":3001,"line":3428},[2999,14139,3351],{"emptyLinePlaceholder":3350},[2999,14141,14142],{"class":3001,"line":3433},[2999,14143,14144],{"class":3222},"// Нова комбінація = нова композиція існуючих специфікацій\n",[2999,14146,14147],{"class":3001,"line":3439},[2999,14148,14149],{"class":3222},"// Репозиторій НЕ змінюється\n",[14151,14152,14153,14167],"table",{},[14154,14155,14156],"thead",{},[14157,14158,14159,14163,14165],"tr",{},[14160,14161,14162],"th",{},"Характеристика",[14160,14164,3984],{},[14160,14166,4207],{},[14168,14169,14170,14182,14193,14204,14217,14228],"tbody",{},[14157,14171,14172,14176,14179],{},[14173,14174,14175],"td",{},"Кількість методів у репозиторії",[14173,14177,14178],{},"O(2ⁿ) для n критеріїв",[14173,14180,14181],{},"O(1) — один метод + специфічні",[14157,14183,14184,14187,14190],{},[14173,14185,14186],{},"Повторне використання логіки",[14173,14188,14189],{},"❌ Дублювання SQL",[14173,14191,14192],{},"✅ Специфікації багаторазові",[14157,14194,14195,14198,14201],{},[14173,14196,14197],{},"Динамічні запити",[14173,14199,14200],{},"❌ Складно",[14173,14202,14203],{},"✅ Природно",[14157,14205,14206,14209,14212],{},[14173,14207,14208],{},"Тестування бізнес-правил",[14173,14210,14211],{},"❌ Потребує БД",[14173,14213,14214,14215],{},"✅ In-memory ",[2968,14216,4531],{},[14157,14218,14219,14222,14225],{},[14173,14220,14221],{},"Читабельність клієнтського коду",[14173,14223,14224],{},"❌ Довгі імена методів",[14173,14226,14227],{},"✅ Fluent API",[14157,14229,14230,14233,14236],{},[14173,14231,14232],{},"Прості запити",[14173,14234,14235],{},"✅ Зручні методи",[14173,14237,14238],{},"✅ Зберігаються + Specification",[3278,14240],{},[2959,14242,14244],{"id":14243},"проблеми-та-обмеження-specification-pattern","Проблеми та обмеження Specification Pattern",[2964,14246,14247],{},"Незважаючи на переваги, Specification Pattern має низку обмежень, що стають помітними у реальних проєктах:",[3285,14249,14251],{"id":14250},"проблема-1-жорстка-привязка-до-структури-select","Проблема 1: Жорстка прив'язка до структури SELECT",[2964,14253,14254,14255,6557],{},"Специфікації генерують фрагменти SQL, що залежать від псевдонімів стовпців у базовому SELECT-запиті. Розглянемо ",[2968,14256,8861],{},[2990,14258,14260],{"className":2992,"code":14259,"language":2994,"meta":2995,"style":2995},"@Override\npublic String toSql() {\n    return \"genre_name = ?\";  // Припускає, що є псевдонім genre_name\n}\n",[2968,14261,14262,14269,14279,14292],{"__ignoreMap":2995},[2999,14263,14264,14267],{"class":3001,"line":3002},[2999,14265,14266],{"class":3022},"@",[2999,14268,5499],{"class":3012},[2999,14270,14271,14273,14275,14277],{"class":3001,"line":3037},[2999,14272,3006],{"class":3005},[2999,14274,5565],{"class":3012},[2999,14276,4856],{"class":3050},[2999,14278,5199],{"class":3022},[2999,14280,14281,14284,14287,14289],{"class":3001,"line":3067},[2999,14282,14283],{"class":3705},"    return",[2999,14285,14286],{"class":3714}," \"genre_name = ?\"",[2999,14288,12645],{"class":3022},[2999,14290,14291],{"class":3222},"// Припускає, що є псевдонім genre_name\n",[2999,14293,14294],{"class":3001,"line":3098},[2999,14295,3101],{"class":3022},[2964,14297,14298,14299,14301,14302,14304,14305,14308,14309,14312,14313,14316],{},"Якщо у ",[2968,14300,9476],{}," змінити псевдонім з ",[2968,14303,11757],{}," на ",[2968,14306,14307],{},"g.name AS genre",", всі специфікації, що використовують ",[2968,14310,14311],{},"genre_name",", стануть некоректними. Це порушує ",[3228,14314,14315],{},"інкапсуляцію",": специфікація (доменний шар) знає про деталі SQL-запиту (інфраструктурний шар).",[3285,14318,14320],{"id":14319},"проблема-2-складність-із-join","Проблема 2: Складність із JOIN",[2964,14322,14323],{},"Якщо специфікація потребує JOIN до таблиці, що не включена у базовий SELECT, виникає конфлікт. Наприклад, специфікація «автор має більше 10 опублікованих книг» потребує підзапиту або додаткового JOIN:",[2990,14325,14327],{"className":6407,"code":14326,"language":6409,"meta":2995,"style":2995},"SELECT ... FROM audiobooks ab\nJOIN authors a ON ab.author_id = a.id\nWHERE a.id IN (\n    SELECT author_id FROM audiobooks GROUP BY author_id HAVING COUNT(*) > 10\n)\n",[2968,14328,14329,14341,14355,14367,14396],{"__ignoreMap":2995},[2999,14330,14331,14333,14336,14338],{"class":3001,"line":3002},[2999,14332,6421],{"class":3005},[2999,14334,14335],{"class":3022}," ... ",[2999,14337,6426],{"class":3005},[2999,14339,14340],{"class":3022}," audiobooks ab\n",[2999,14342,14343,14346,14349,14352],{"class":3001,"line":3037},[2999,14344,14345],{"class":3005},"JOIN",[2999,14347,14348],{"class":3022}," authors a ",[2999,14350,14351],{"class":3005},"ON",[2999,14353,14354],{"class":3022}," ab.author_id = a.id\n",[2999,14356,14357,14359,14362,14365],{"class":3001,"line":3067},[2999,14358,4466],{"class":3005},[2999,14360,14361],{"class":3022}," a.id ",[2999,14363,14364],{"class":3005},"IN",[2999,14366,7640],{"class":3022},[2999,14368,14369,14372,14375,14377,14379,14382,14384,14387,14390,14393],{"class":3001,"line":3098},[2999,14370,14371],{"class":3005},"    SELECT",[2999,14373,14374],{"class":3022}," author_id ",[2999,14376,6426],{"class":3005},[2999,14378,6429],{"class":3022},[2999,14380,14381],{"class":3005},"GROUP BY",[2999,14383,14374],{"class":3022},[2999,14385,14386],{"class":3005},"HAVING",[2999,14388,14389],{"class":3050}," COUNT",[2999,14391,14392],{"class":3022},"(*) > ",[2999,14394,14395],{"class":3769},"10\n",[2999,14397,14398],{"class":3001,"line":3219},[2999,14399,3830],{"class":3022},[2964,14401,14402],{},"Специфікація не може «додати» JOIN до базового SELECT — вона генерує лише WHERE-умову. Це обмежує виразність патерну.",[3285,14404,14406],{"id":14405},"проблема-3-відсутність-типобезпеки","Проблема 3: Відсутність типобезпеки",[2964,14408,14409],{},"Специфікації генерують SQL як рядки. Помилка у назві стовпця виявляється лише під час виконання:",[2990,14411,14413],{"className":2992,"code":14412,"language":2994,"meta":2995,"style":2995},"@Override\npublic String toSql() {\n    return \"genreName = ?\";  // Помилка: має бути genre_name\n}\n// Компілятор не виявить помилку — SQLException під час виконання\n",[2968,14414,14415,14421,14431,14443,14447],{"__ignoreMap":2995},[2999,14416,14417,14419],{"class":3001,"line":3002},[2999,14418,14266],{"class":3022},[2999,14420,5499],{"class":3012},[2999,14422,14423,14425,14427,14429],{"class":3001,"line":3037},[2999,14424,3006],{"class":3005},[2999,14426,5565],{"class":3012},[2999,14428,4856],{"class":3050},[2999,14430,5199],{"class":3022},[2999,14432,14433,14435,14438,14440],{"class":3001,"line":3067},[2999,14434,14283],{"class":3705},[2999,14436,14437],{"class":3714}," \"genreName = ?\"",[2999,14439,12645],{"class":3022},[2999,14441,14442],{"class":3222},"// Помилка: має бути genre_name\n",[2999,14444,14445],{"class":3001,"line":3098},[2999,14446,3101],{"class":3022},[2999,14448,14449],{"class":3001,"line":3219},[2999,14450,14451],{"class":3222},"// Компілятор не виявить помилку — SQLException під час виконання\n",[2964,14453,14454,14455,14458],{},"Сучасні ORM (Hibernate Criteria API, JPA Specification, jOOQ) вирішують це через ",[3228,14456,14457],{},"type-safe query builders",", але це вимагає відмови від чистого JDBC.",[3285,14460,14462],{"id":14461},"проблема-4-складність-із-window-functions-та-union","Проблема 4: Складність із Window Functions та UNION",[2964,14464,14465],{},"Specification Pattern добре працює для WHERE-умов (включно з підзапитами та агрегаціями у WHERE), але має обмеження для:",[4397,14467,14468,14477,14483],{},[4400,14469,14470,4371,14473,14476],{},[3228,14471,14472],{},"Window functions:",[2968,14474,14475],{},"ROW_NUMBER() OVER (PARTITION BY genre ORDER BY release_year)"," — не є частиною WHERE",[4400,14478,14479,14482],{},[3228,14480,14481],{},"UNION/INTERSECT:"," комбінування результатів кількох SELECT-запитів",[4400,14484,14485,4371,14488,14491],{},[3228,14486,14487],{},"GROUP BY з агрегацією у SELECT:",[2968,14489,14490],{},"SELECT genre, COUNT(*) GROUP BY genre"," — змінює структуру ResultSet",[2964,14493,14494],{},"Для таких сценаріїв потрібні спеціалізовані методи у репозиторії або Query Object Pattern.",[3285,14496,14498],{"id":14497},"проблема-5-дублювання-логіки-між-issatisfiedby-та-tosql","Проблема 5: Дублювання логіки між isSatisfiedBy та toSql",[2964,14500,14501],{},"Кожна специфікація реалізує одну і ту ж логіку двічі — для Java і для SQL:",[2990,14503,14505],{"className":2992,"code":14504,"language":2994,"meta":2995,"style":2995},"// Java-версія\n@Override\npublic boolean isSatisfiedBy(Audiobook candidate) {\n    return candidate.getPrice().compareTo(maxPrice) \u003C= 0;\n}\n\n// SQL-версія\n@Override\npublic String toSql() {\n    return \"price \u003C= ?\";\n}\n",[2968,14506,14507,14512,14518,14533,14556,14560,14564,14569,14575,14585,14594],{"__ignoreMap":2995},[2999,14508,14509],{"class":3001,"line":3002},[2999,14510,14511],{"class":3222},"// Java-версія\n",[2999,14513,14514,14516],{"class":3001,"line":3037},[2999,14515,14266],{"class":3022},[2999,14517,5499],{"class":3012},[2999,14519,14520,14522,14524,14526,14528,14530],{"class":3001,"line":3067},[2999,14521,3006],{"class":3005},[2999,14523,5506],{"class":3012},[2999,14525,4788],{"class":3050},[2999,14527,3054],{"class":3022},[2999,14529,3694],{"class":3012},[2999,14531,14532],{"class":3022}," candidate) {\n",[2999,14534,14535,14537,14539,14541,14544,14546,14549,14552,14554],{"class":3001,"line":3098},[2999,14536,14283],{"class":3705},[2999,14538,4762],{"class":3060},[2999,14540,3803],{"class":3022},[2999,14542,14543],{"class":3050},"getPrice",[2999,14545,9062],{"class":3022},[2999,14547,14548],{"class":3050},"compareTo",[2999,14550,14551],{"class":3022},"(maxPrice) \u003C= ",[2999,14553,7299],{"class":3769},[2999,14555,4102],{"class":3022},[2999,14557,14558],{"class":3001,"line":3219},[2999,14559,3101],{"class":3022},[2999,14561,14562],{"class":3001,"line":3364},[2999,14563,3351],{"emptyLinePlaceholder":3350},[2999,14565,14566],{"class":3001,"line":3370},[2999,14567,14568],{"class":3222},"// SQL-версія\n",[2999,14570,14571,14573],{"class":3001,"line":3376},[2999,14572,14266],{"class":3022},[2999,14574,5499],{"class":3012},[2999,14576,14577,14579,14581,14583],{"class":3001,"line":3382},[2999,14578,3006],{"class":3005},[2999,14580,5565],{"class":3012},[2999,14582,4856],{"class":3050},[2999,14584,5199],{"class":3022},[2999,14586,14587,14589,14592],{"class":3001,"line":3388},[2999,14588,14283],{"class":3705},[2999,14590,14591],{"class":3714}," \"price \u003C= ?\"",[2999,14593,4102],{"class":3022},[2999,14595,14596],{"class":3001,"line":3394},[2999,14597,3101],{"class":3022},[2964,14599,14600,14601,14603],{},"Якщо логіка змінюється (наприклад, додається перевірка ",[2968,14602,3254],{},"), треба оновити обидва методи. Це джерело помилок.",[14605,14606,14607,14608,14611,14612,14614,14615,14618],"warning",{},"У production-системах Specification Pattern найчастіше використовується ",[3228,14609,14610],{},"лише для SQL-генерації",", без реалізації ",[2968,14613,4531],{},". In-memory фільтрація виконується через окремі предикати (",[2968,14616,14617],{},"Predicate\u003CT>"," у Java 8+), що не дублюють SQL-логіку.",[3278,14620],{},[2959,14622,14624],{"id":14623},"альтернативи-та-еволюція-патерну","Альтернативи та еволюція патерну",[2964,14626,14627],{},"Specification Pattern є лише одним із підходів до вирішення проблеми динамічних запитів. Розглянемо альтернативи:",[3106,14629,14630,14665,14700],{},[3109,14631,14634,14639,14650,14654],{"title":14632,"icon":14633},"Criteria API (JPA)","i-heroicons-code-bracket",[2964,14635,14636],{},[3228,14637,14638],{},"Hibernate/JPA Criteria API:",[4397,14640,14641,14644,14647],{},[4400,14642,14643],{},"Type-safe query builder",[4400,14645,14646],{},"Генерує SQL автоматично",[4400,14648,14649],{},"Підтримує JOIN, підзапити, агрегації",[2964,14651,14652],{},[3228,14653,4438],{},[4397,14655,14656,14659,14662],{},[4400,14657,14658],{},"Складний API",[4400,14660,14661],{},"Прив'язка до JPA",[4400,14663,14664],{},"Повільніший за нативний SQL",[3109,14666,14669,14674,14685,14689],{"title":14667,"icon":14668},"QueryDSL / jOOQ","i-heroicons-wrench-screwdriver",[2964,14670,14671],{},[3228,14672,14673],{},"Type-safe SQL builders:",[4397,14675,14676,14679,14682],{},[4400,14677,14678],{},"Генерують Java-класи зі схеми БД",[4400,14680,14681],{},"Повна виразність SQL",[4400,14683,14684],{},"Compile-time перевірка",[2964,14686,14687],{},[3228,14688,4438],{},[4397,14690,14691,14694,14697],{},[4400,14692,14693],{},"Додаткова залежність",[4400,14695,14696],{},"Кодогенерація",[4400,14698,14699],{},"Крива навчання",[3109,14701,14703,14708,14719,14723],{"title":14702,"icon":4453},"Specification (Spring Data)",[2964,14704,14705],{},[3228,14706,14707],{},"Spring Data JPA Specification:",[4397,14709,14710,14713,14716],{},[4400,14711,14712],{},"Інтеграція з JPA Criteria",[4400,14714,14715],{},"Стандартний підхід у Spring-екосистемі",[4400,14717,14718],{},"Підтримка пагінації",[2964,14720,14721],{},[3228,14722,4438],{},[4397,14724,14725,14728],{},[4400,14726,14727],{},"Прив'язка до Spring",[4400,14729,14730],{},"Складність для нетривіальних запитів",[2964,14732,14733,14734,14737],{},"Для чистого JDBC-проєкту без ORM ",[3228,14735,14736],{},"наш підхід (SQL Specification) є оптимальним компромісом"," між гнучкістю та складністю. Він не вимагає додаткових залежностей і дає повний контроль над SQL.",[3278,14739],{},[2959,14741,14743],{"id":14742},"підсумок","Підсумок",[2964,14745,14746,14747,14750],{},"Specification Pattern вирішує фундаментальну проблему enterprise-розробки: ",[3228,14748,14749],{},"як виразити складні бізнес-правила у вигляді композиційних об'єктів",", що можуть бути повторно використані у різних контекстах — пошуку, валідації, звітах.",[2964,14752,14753],{},"Ключові досягнення патерну:",[2964,14755,14756,14759,14760,14763,14764,2975,14767,2975,14770,3803],{},[3228,14757,14758],{},"1. Усунення комбінаторного вибуху методів."," Замість 2ⁿ методів для n критеріїв пошуку — один метод ",[2968,14761,14762],{},"findAll(Specification\u003CT>)"," і композиція специфікацій через ",[2968,14765,14766],{},"and()",[2968,14768,14769],{},"or()",[2968,14771,14772],{},"not()",[2964,14774,14775,14778],{},[3228,14776,14777],{},"2. Повторне використання бізнес-правил."," Специфікація «аудіокнига доступна для покупки» виражена один раз і використовується скрізь — у пошуку, валідації, звітах. Зміна правила вимагає правки в одному місці.",[2964,14780,14781,14784,14785,14788],{},[3228,14782,14783],{},"3. Читабельність клієнтського коду."," Fluent API (",[2968,14786,14787],{},"affordable.and(ukrainian).and(after2020)",") читається як природна мова і виражає намір, а не механізм.",[2964,14790,14791,14794,14795,14798],{},[3228,14792,14793],{},"4. Тестованість без бази даних."," Метод ",[2968,14796,14797],{},"isSatisfiedBy()"," дозволяє тестувати логіку специфікацій через прості unit-тести з mock-об'єктами.",[2964,14800,14801,14804,14805,3803],{},[3228,14802,14803],{},"5. Гнучкість для динамічних запитів."," Побудова запиту на основі користувацького вводу стає тривіальною — просто комбінуємо специфікації через ",[2968,14806,14766],{},[2964,14808,14809],{},"Але патерн не є срібною кулею. Він має обмеження:",[4397,14811,14812,14815,14818,14821,14824],{},[4400,14813,14814],{},"Жорстка прив'язка до структури SELECT-запиту (псевдоніми стовпців)",[4400,14816,14817],{},"Складність із JOIN та підзапитами",[4400,14819,14820],{},"Відсутність типобезпеки (помилки виявляються під час виконання)",[4400,14822,14823],{},"Обмежена виразність для агрегацій, window functions, UNION",[4400,14825,14826,14827,3656,14829],{},"Дублювання логіки між ",[2968,14828,4531],{},[2968,14830,5244],{},[2964,14832,14833],{},"Для складних запитів, що виходять за межі простих WHERE-умов, доводиться повертатися до спеціалізованих методів у репозиторії або використовувати більш потужні інструменти (Criteria API, QueryDSL, jOOQ).",[3270,14835,14836,14837,14840],{},"Specification Pattern є ",[3228,14838,14839],{},"тактичним патерном Domain-Driven Design",". Він належить до доменного шару і виражає бізнес-логіку мовою предметної області. Це принципово відрізняє його від Repository (інфраструктурний патерн) — специфікації описують «що шукати», а не «як шукати».",[2964,14842,14843,14844,14847,14848,14851,14852,14855],{},"У наступних статтях ми розглянемо патерни, що доповнюють Specification: ",[3228,14845,14846],{},"Query Object"," (для складних запитів із агрегаціями), ",[3228,14849,14850],{},"Criteria Builder"," (type-safe альтернатива) та інтеграцію специфікацій із ",[3228,14853,14854],{},"Unit of Work"," для транзакційної узгодженості.",[3278,14857],{},[2959,14859,14861],{"id":14860},"завдання","Завдання",[14863,14864,14865,14912,14924,15024,15036,15108],"accordion",{},[14866,14867,14870,14875,14909],"accordion-item",{"icon":14868,"label":14869},"i-lucide-circle-help","Завдання 1: Специфікація для Author",[2964,14871,14872,14873,6557],{},"Реалізуйте три специфікації для сутності ",[2968,14874,3026],{},[14876,14877,14878,14893,14901],"ol",{},[4400,14879,14880,14885,14886,14889,14890,14892],{},[3228,14881,14882],{},[2968,14883,14884],{},"AuthorWithBioSpecification"," — автор має біографію (поле ",[2968,14887,14888],{},"bio"," не ",[2968,14891,8051],{}," і не порожнє)",[4400,14894,14895,14900],{},[3228,14896,14897],{},[2968,14898,14899],{},"AuthorLastNameStartsWithSpecification"," — прізвище автора починається з заданого підрядка",[4400,14902,14903,14908],{},[3228,14904,14905],{},[2968,14906,14907],{},"AuthorWithPublishedBooksSpecification"," — автор має хоча б одну опубліковану аудіокнигу",[2964,14910,14911],{},"Підказка для третьої специфікації: потрібен підзапит або EXISTS-умова.",[14866,14913,14915,14918,14921],{"icon":14868,"label":14914},"Завдання 2: Композиція для складного запиту",[2964,14916,14917],{},"Використовуючи існуючі специфікації, побудуйте запит:",[2964,14919,14920],{},"«Знайти аудіокниги жанру 'Фантастика' або 'Детектив', опубліковані у 2018-2023 роках, з тривалістю від 5400 до 10800 секунд (1.5-3 години)».",[2964,14922,14923],{},"Виведіть згенерований SQL та параметри.",[14866,14925,14927,14934,15018],{"icon":14868,"label":14926},"Завдання 3: Динамічний фільтр для веб-API",[2964,14928,14929,14930,14933],{},"Реалізуйте метод ",[2968,14931,14932],{},"buildFilterFromRequest(FilterRequest request)",", що будує специфікацію на основі HTTP-запиту:",[2990,14935,14937],{"className":2992,"code":14936,"language":2994,"meta":2995,"style":2995},"class FilterRequest {\n    String genre;           // опціонально\n    Integer minYear;        // опціонально\n    Integer maxYear;        // опціонально\n    Integer minDuration;    // опціонально (у секундах)\n    Integer maxDuration;    // опціонально (у секундах)\n    String titlePart;       // опціонально (пошук у назві)\n}\n",[2968,14938,14939,14948,14960,14971,14981,14993,15003,15014],{"__ignoreMap":2995},[2999,14940,14941,14943,14946],{"class":3001,"line":3002},[2999,14942,10270],{"class":3005},[2999,14944,14945],{"class":3012}," FilterRequest",[2999,14947,8215],{"class":3022},[2999,14949,14950,14952,14954,14957],{"class":3001,"line":3037},[2999,14951,4039],{"class":3012},[2999,14953,10357],{"class":3060},[2999,14955,14956],{"class":3022},";           ",[2999,14958,14959],{"class":3222},"// опціонально\n",[2999,14961,14962,14964,14966,14969],{"class":3001,"line":3067},[2999,14963,4017],{"class":3012},[2999,14965,8440],{"class":3060},[2999,14967,14968],{"class":3022},";        ",[2999,14970,14959],{"class":3222},[2999,14972,14973,14975,14977,14979],{"class":3001,"line":3098},[2999,14974,4017],{"class":3012},[2999,14976,8453],{"class":3060},[2999,14978,14968],{"class":3022},[2999,14980,14959],{"class":3222},[2999,14982,14983,14985,14987,14990],{"class":3001,"line":3219},[2999,14984,4017],{"class":3012},[2999,14986,7891],{"class":3060},[2999,14988,14989],{"class":3022},";    ",[2999,14991,14992],{"class":3222},"// опціонально (у секундах)\n",[2999,14994,14995,14997,14999,15001],{"class":3001,"line":3364},[2999,14996,4017],{"class":3012},[2999,14998,7908],{"class":3060},[2999,15000,14989],{"class":3022},[2999,15002,14992],{"class":3222},[2999,15004,15005,15007,15009,15011],{"class":3001,"line":3370},[2999,15006,4039],{"class":3012},[2999,15008,9257],{"class":3060},[2999,15010,12663],{"class":3022},[2999,15012,15013],{"class":3222},"// опціонально (пошук у назві)\n",[2999,15015,15016],{"class":3001,"line":3376},[2999,15017,3101],{"class":3022},[2964,15019,15020,15021,15023],{},"Якщо параметр ",[2968,15022,8051],{}," або порожній — він не включається у запит.",[14866,15025,15027,15030],{"icon":14868,"label":15026},"Завдання 4: Специфікація з NOT",[2964,15028,15029],{},"Створіть запит: «Знайти аудіокниги, що НЕ є поезією, з тривалістю НЕ більше 7200 секунд (2 години), і опубліковані після 2015 року».",[2964,15031,15032,15033,15035],{},"Використовуйте метод ",[2968,15034,14772],{}," для інверсії умов.",[14866,15037,15039,15046,15049,15102],{"icon":14868,"label":15038},"Завдання 5★: Специфікація з підзапитом",[2964,15040,15041,15042,15045],{},"Реалізуйте ",[2968,15043,15044],{},"PopularAuthorSpecification"," — автор, у якого більше 5 опублікованих аудіокниг.",[2964,15047,15048],{},"SQL має містити підзапит:",[2990,15050,15052],{"className":6407,"code":15051,"language":6409,"meta":2995,"style":2995},"WHERE author_id IN (\n    SELECT author_id \n    FROM audiobooks \n    GROUP BY author_id \n    HAVING COUNT(*) > 5\n)\n",[2968,15053,15054,15064,15071,15079,15086,15098],{"__ignoreMap":2995},[2999,15055,15056,15058,15060,15062],{"class":3001,"line":3002},[2999,15057,4466],{"class":3005},[2999,15059,14374],{"class":3022},[2999,15061,14364],{"class":3005},[2999,15063,7640],{"class":3022},[2999,15065,15066,15068],{"class":3001,"line":3037},[2999,15067,14371],{"class":3005},[2999,15069,15070],{"class":3022}," author_id \n",[2999,15072,15073,15076],{"class":3001,"line":3067},[2999,15074,15075],{"class":3005},"    FROM",[2999,15077,15078],{"class":3022}," audiobooks \n",[2999,15080,15081,15084],{"class":3001,"line":3098},[2999,15082,15083],{"class":3005},"    GROUP BY",[2999,15085,15070],{"class":3022},[2999,15087,15088,15091,15093,15095],{"class":3001,"line":3219},[2999,15089,15090],{"class":3005},"    HAVING",[2999,15092,14389],{"class":3050},[2999,15094,14392],{"class":3022},[2999,15096,15097],{"class":3769},"5\n",[2999,15099,15100],{"class":3001,"line":3364},[2999,15101,3830],{"class":3022},[2964,15103,15104,15105,15107],{},"Підказка: ",[2968,15106,4881],{}," може повертати складні конструкції, не лише прості умови.",[14866,15109,15111,15118],{"icon":14868,"label":15110},"Завдання 6★: Специфікація для діапазону років випуску",[2964,15112,15113,15114,15117],{},"Створіть специфікацію ",[2968,15115,15116],{},"RecentAudiobooksSpecification",", що знаходить аудіокниги, опубліковані за останні N років від поточної дати.",[2964,15119,15120,15121,15124,15125,15128],{},"Підказка: використовуйте SQL-функцію ",[2968,15122,15123],{},"EXTRACT(YEAR FROM CURRENT_DATE)"," або ",[2968,15126,15127],{},"YEAR(CURRENT_DATE)"," залежно від діалекту БД.",[3278,15130],{},[2959,15132,15134],{"id":15133},"додаткові-матеріали","Додаткові матеріали",[3106,15136,15137,15143,15148,15153],{},[3109,15138,15142],{"title":15139,"target":15140,"to":15141},"📖 Domain-Driven Design","_blank","https://www.domainlanguage.com/ddd/","Книга Еріка Еванса (2003), де вперше описано Specification Pattern у контексті DDD.",[3109,15144,15147],{"title":15145,"target":15140,"to":15146},"📖 Patterns of Enterprise Application Architecture","https://martinfowler.com/books/eaa.html","Мартін Фаулер про Repository, Unit of Work та інші патерни персистентності.",[3109,15149,15152],{"title":15150,"target":15140,"to":15151},"🔗 Spring Data JPA Specification","https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl","Офіційна документація Spring Data про інтеграцію Specification з JPA.",[3109,15154,15157],{"title":15155,"target":15140,"to":15156},"🔗 QueryDSL","http://querydsl.com/","Type-safe query builder для Java — альтернатива Specification Pattern.",[15159,15160,15161],"style",{},"html pre.shiki code .su1O8, html code.shiki .su1O8{--shiki-light:#0000FF;--shiki-default:#569CD6;--shiki-dark:#569CD6}html pre.shiki code .sN1BT, html code.shiki .sN1BT{--shiki-light:#267F99;--shiki-default:#4EC9B0;--shiki-dark:#4EC9B0}html pre.shiki code .sHH4Y, html code.shiki .sHH4Y{--shiki-light:#000000;--shiki-default:#D4D4D4;--shiki-dark:#D4D4D4}html pre.shiki code .s8Opu, html code.shiki .s8Opu{--shiki-light:#795E26;--shiki-default:#DCDCAA;--shiki-dark:#DCDCAA}html pre.shiki code .siwwj, html code.shiki .siwwj{--shiki-light:#001080;--shiki-default:#9CDCFE;--shiki-dark:#9CDCFE}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spJ8K, html code.shiki .spJ8K{--shiki-light:#008000;--shiki-default:#6A9955;--shiki-dark:#6A9955}html pre.shiki code .sCDza, html code.shiki .sCDza{--shiki-light:#AF00DB;--shiki-default:#CE92A4;--shiki-dark:#CE92A4}html pre.shiki code .sbdoH, html code.shiki .sbdoH{--shiki-light:#A31515;--shiki-default:#CE9178;--shiki-dark:#CE9178}html pre.shiki code .sJj4R, html code.shiki .sJj4R{--shiki-light:#098658;--shiki-default:#B5CEA8;--shiki-dark:#B5CEA8}html pre.shiki code .sjcCO, html code.shiki .sjcCO{--shiki-light:#EE0000;--shiki-default:#D7BA7D;--shiki-dark:#D7BA7D}",{"title":2995,"searchDepth":3037,"depth":3037,"links":15163},[15164,15165,15169,15170,15171,15176,15177,15183,15184,15185,15186,15187,15194,15195,15196,15197],{"id":2961,"depth":3037,"text":2962},{"id":3282,"depth":3037,"text":3283,"children":15166},[15167,15168],{"id":3287,"depth":3067,"text":3288},{"id":3671,"depth":3067,"text":3672},{"id":4380,"depth":3037,"text":4381},{"id":4524,"depth":3037,"text":4525},{"id":5281,"depth":3037,"text":5282,"children":15172},[15173,15174,15175],{"id":5292,"depth":3067,"text":3652},{"id":5727,"depth":3067,"text":3655},{"id":6116,"depth":3067,"text":3659},{"id":6549,"depth":3037,"text":6550},{"id":7607,"depth":3037,"text":7608,"children":15178},[15179,15180,15181,15182],{"id":7783,"depth":3067,"text":7784},{"id":8336,"depth":3067,"text":8337},{"id":8860,"depth":3067,"text":8861},{"id":9159,"depth":3067,"text":9160},{"id":9469,"depth":3037,"text":9470},{"id":11770,"depth":3037,"text":11771},{"id":13349,"depth":3037,"text":13350},{"id":13611,"depth":3037,"text":13612},{"id":14243,"depth":3037,"text":14244,"children":15188},[15189,15190,15191,15192,15193],{"id":14250,"depth":3067,"text":14251},{"id":14319,"depth":3067,"text":14320},{"id":14405,"depth":3067,"text":14406},{"id":14461,"depth":3067,"text":14462},{"id":14497,"depth":3067,"text":14498},{"id":14623,"depth":3037,"text":14624},{"id":14742,"depth":3037,"text":14743},{"id":14860,"depth":3037,"text":14861},{"id":15133,"depth":3037,"text":15134},"Від жорстко закодованих методів пошуку до гнучких композицій: реалізація Specification Pattern за Еріком Евансом, інтеграція з Repository через SqlSpecification, логічні оператори AND/OR/NOT та побудова динамічних WHERE-умов.","md",null,{},{"title":2329,"description":15198},"2p-QodI9obu-Mkwvf1ca-QVb9gnP5xccgCPRagf1Yxg",[15205,15207],{"title":2325,"path":2326,"stem":2327,"description":15206,"children":-1},"Усунення дублювання між JdbcAuthorRepository, JdbcGenreRepository та JdbcAudiobookRepository через власні анотації @Table, @Column, @Id та Java Reflection API. Динамічна генерація INSERT/UPDATE/SELECT/DELETE без жодного SQL у коді сутностей. Порівняння з JPA і аналіз обмежень рефлексійного підходу.",{"title":2333,"path":2334,"stem":2335,"description":15208,"children":-1},"Від простих WHERE-умов до складних підзапитів: реалізація специфікацій з EXISTS, IN (SELECT), HAVING, агрегатними функціями. Порівняння підходів: чисті специфікації vs спеціалізовані методи репозиторію. Коли використовувати що.",1777909228464]