{"id":26062001,"url":"https://github.com/okaeripoland/okaeri-persistence","last_synced_at":"2026-03-13T08:34:15.442Z","repository":{"id":47070168,"uuid":"362364915","full_name":"OkaeriPoland/okaeri-persistence","owner":"OkaeriPoland","description":"Object Document Mapping (ODM) library allowing to focus on data instead of the storage layer","archived":false,"fork":false,"pushed_at":"2024-08-28T16:10:23.000Z","size":375,"stargazers_count":20,"open_issues_count":1,"forks_count":1,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-03-25T07:22:21.525Z","etag":null,"topics":["h2","java","jdbc","json","mariadb","minecraft","mongodb","mysql","odm","redis","yaml"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/OkaeriPoland.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-04-28T06:41:52.000Z","updated_at":"2025-03-05T17:42:21.000Z","dependencies_parsed_at":"2025-03-08T21:15:19.597Z","dependency_job_id":null,"html_url":"https://github.com/OkaeriPoland/okaeri-persistence","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OkaeriPoland%2Fokaeri-persistence","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OkaeriPoland%2Fokaeri-persistence/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OkaeriPoland%2Fokaeri-persistence/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OkaeriPoland%2Fokaeri-persistence/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/OkaeriPoland","download_url":"https://codeload.github.com/OkaeriPoland/okaeri-persistence/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248381648,"owners_count":21094524,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["h2","java","jdbc","json","mariadb","minecraft","mongodb","mysql","odm","redis","yaml"],"created_at":"2025-03-08T15:49:02.592Z","updated_at":"2026-03-13T08:34:15.426Z","avatar_url":"https://github.com/OkaeriPoland.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Okaeri Persistence\n\n![License](https://img.shields.io/github/license/OkaeriPoland/okaeri-persistence?style=for-the-badge\u0026color=blue)\n[![Codecov](https://img.shields.io/codecov/c/github/OkaeriPoland/okaeri-persistence?style=for-the-badge\u0026logo=codecov)](https://codecov.io/gh/OkaeriPoland/okaeri-persistence)\n[![Discord](https://img.shields.io/discord/589089838200913930?style=for-the-badge\u0026logo=discord\u0026color=blue)](https://discord.gg/hASN5eX)\n\nObject Document Mapping (ODM) library for Java - store JSON documents in MongoDB, PostgreSQL, MariaDB, H2, Redis, or flat files with a consistent API. Write your data layer once, run it anywhere.\n\n## Features\n\n- **Write Once, Run Anywhere**: Swap databases with one line - the core Java philosophy (without the XML hell)\n- **Fluent Query DSL**: Filtering, ordering, and pagination across all backends (native for MongoDB/PostgreSQL/MariaDB/H2, in-memory for others)\n- **Fluent Update DSL**: Field and array operations (native atomic for MongoDB/PostgreSQL/MariaDB, in-memory for others)\n- **Repository Pattern**: Define method names, get auto-implemented finders (`findByName`, `streamByLevel`, etc.)\n- **Unified Indexing**: Declare indexes once, backends create native indexes when supported\n- **Document-Based**: Store data as JSON/YAML documents - flexible but not schema-free\n- **Streaming Support**: Process large datasets with Java streams and automatic batching\n\n## The Philosophy (and the Pitfalls)\n\n**The Good**: Write your persistence code once against our interface, switch from MongoDB to PostgreSQL without changing application code. Your dev team can use H2, staging uses PostgreSQL, production uses MongoDB. Same code.\n\n**The Catch**: You're trading database-specific optimizations for portability. Need MongoDB's aggregation pipeline? You'll have to fetch and process in Java. Need PostgreSQL's full-text search? Same deal. This library is for when you value flexibility and developer velocity over squeezing every bit of performance from your database.\n\n**Good For**: Apps where data naturally clusters around an ID (user profiles, game state, session data), rapid prototyping, when you want to defer the database choice.\n\n**Not Good For**: Complex joins, analytical queries, when you need database-specific features, when you need every bit of performance.\n\n## Requirements\n\n### Java\n- **Java 8 or higher** for library code\n- **Java 21** for running tests (but your app can use Java 8)\n\n### Backends\n\nPick one (or multiple):\n\n**Native Document Support:**\n\n| Backend        | Artifact                   | Description                                                                                                                                  |\n|----------------|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|\n| **MongoDB**    | `okaeri-persistence-mongo` | Uses the official MongoDB driver. Native document store with automatic index creation and native filtering by properties.                    |\n| **PostgreSQL** | `okaeri-persistence-jdbc`  | Uses the official PostgreSQL JDBC driver with HikariCP. Stores documents as JSONB with native GIN indexes and JSONB operators for filtering. |\n\n**Other Storage:**\n\n| Backend        | Artifact                   | Description                                                                                                                                                                          |\n|----------------|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| **MariaDB**    | `okaeri-persistence-jdbc`  | Uses HikariCP with MariaDB. Stores documents using native JSON datatype with native query translation (`JSON_EXTRACT`, `JSON_UNQUOTE`). Native indexes via stored generated columns. |\n| **H2**         | `okaeri-persistence-jdbc`  | Uses HikariCP with H2. Stores documents as native JSON type with native query translation using field reference syntax `(value).\"field\"`. No index support.                          |\n| **Redis**      | `okaeri-persistence-redis` | Uses Lettuce client. Stores JSON as strings in Redis hashes. No index support - filtering done in memory.                                                                            |\n| **Flat Files** | `okaeri-persistence-flat`  | File-based storage using any okaeri-configs format (YAML/JSON/HOCON). In-memory indexes.                                                                                             |\n| **In-Memory**  | `okaeri-persistence-core`  | Pure in-memory storage with in-memory indexes. Zero persistence.                                                                                                                     |\n\n## Installation\n\n![Version](https://img.shields.io/badge/dynamic/xml?style=for-the-badge\u0026label=version\u0026logo=apachemaven\u0026query=%2F*%5Blocal-name()%3D'project'%5D%2F*%5Blocal-name()%3D'version'%5D%2Ftext%28%29\u0026url=https%3A%2F%2Fraw.githubusercontent.com%2FOkaeriPoland%2Fokaeri-persistence%2Frefs%2Fheads%2Fmaster%2Fpom.xml)\n![Java](https://img.shields.io/badge/java-8%2B-blue.svg?style=for-the-badge)\n\n### Maven\n```xml\n\u003crepositories\u003e\n    \u003crepository\u003e\n        \u003cid\u003eokaeri-releases\u003c/id\u003e\n        \u003curl\u003ehttps://repo.okaeri.cloud/releases\u003c/url\u003e\n    \u003c/repository\u003e\n\u003c/repositories\u003e\n```\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eeu.okaeri\u003c/groupId\u003e\n    \u003cartifactId\u003eokaeri-persistence-mongo\u003c/artifactId\u003e\n    \u003cversion\u003e3.0.1-beta.19\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n### Gradle (Kotlin DSL)\n```kotlin\nrepositories {\n    maven(\"https://repo.okaeri.cloud/releases\")\n}\n```\n```kotlin\ndependencies {\n    implementation(\"eu.okaeri:okaeri-persistence-mongo:3.0.1-beta.19\")\n}\n```\n\n**Replace `mongo` with:** `jdbc`, `redis`, `flat` depending on your backend.\n\n## Quick Start\n\n### 1. Define Your Document\n\n```java\n@Data\npublic class User extends Document {\n    private String name;\n    private int level;\n    private Instant lastLogin;\n    private List\u003cString\u003e achievements;\n}\n```\n\n### 2. Create a Repository\n\n```java\n@DocumentCollection(\n    path = \"users\",\n    // keyLength auto-detected: UUID=36, Integer=11, Long=20, others=255\n    indexes = {\n        @DocumentIndex(path = \"name\", maxLength = 32),  // Optional (default 255). Used only by MariaDB\n        @DocumentIndex(path = \"level\")\n    }\n)\npublic interface UserRepository extends DocumentRepository\u003cUUID, User\u003e {\n\n    // Method names are parsed automatically - no annotations needed!\n    Optional\u003cUser\u003e findByName(String name);\n    Stream\u003cUser\u003e streamByLevel(int level);\n    List\u003cUser\u003e findByLevelAndName(int level, String name);\n}\n```\n\n### 3. Use It\n\n```java\nimport static eu.okaeri.persistence.filter.OrderBy.*;\nimport static eu.okaeri.persistence.filter.condition.Condition.*;\nimport static eu.okaeri.persistence.filter.predicate.SimplePredicate.*;\n\n// Setup (MongoDB example - swap for any backend)\nMongoClient mongo = MongoClients.create(\"mongodb://localhost\");\nDocumentPersistence persistence = new DocumentPersistence(\n    new MongoPersistence(mongo, \"mydb\", JsonSimpleConfigurer::new)\n);\n\n// Create repository (convenience method)\nUserRepository users = persistence.createRepository(UserRepository.class);\n\n// Advanced: manual approach for custom ClassLoader or collection customization\n// PersistenceCollection collection = PersistenceCollection.of(UserRepository.class);\n// persistence.registerCollection(collection);\n// UserRepository users = RepositoryDeclaration.of(UserRepository.class)\n//     .newProxy(persistence, collection, customClassLoader);\n\n// Create (UUID auto-generated on save)\nUser alice = new User();\nalice.setName(\"alice\");\nalice.setLevel(42);\nalice.setAchievements(List.of(\"speedrun\", \"pacifist\"));\nusers.save(alice);\n\n// Find by ID\nUser found = users.findByPath(alice.getPath()).orElseThrow();\n\n// Find by indexed field (auto-implemented from method name)\nUser byName = users.findByName(\"alice\").orElseThrow();\n\n// Query with filtering and ordering\nList\u003cUser\u003e topPlayers = users.find(q -\u003e q\n  .where(on(\"level\", gt(10)))\n  .orderBy(desc(\"level\"), asc(\"name\"))\n  .limit(10))\n  .toList();\n\n// Stream processing\nusers.streamByLevel(42)\n  .filter(u -\u003e u.getAchievements().size() \u003e 1)\n  .forEach(u -\u003e System.out.println(u.getName()));\n```\n\n## Query DSL\n\nThe `find()` method takes a lambda that builds a query and returns a Stream:\n\n```java\n// Filtering\nList\u003cUser\u003e users = userRepo.find(q -\u003e q\n  .where(on(\"level\", gt(10))))\n  .toList();\n\n// Multiple conditions\nList\u003cUser\u003e users = userRepo.find(q -\u003e q\n  .where(and(\n    on(\"level\", gte(10)),\n    on(\"lastLogin\", gt(yesterday)))))\n  .toList();\n\n// Ordering (single or multiple)\nList\u003cUser\u003e users = userRepo.find(q -\u003e q\n  .orderBy(desc(\"level\")))\n  .toList();\n\nList\u003cUser\u003e users = userRepo.find(q -\u003e q\n  .orderBy(desc(\"score\"), asc(\"name\")))\n  .toList();\n\n// Nested properties\nList\u003cProfile\u003e profiles = profileRepo.find(q -\u003e q\n  .where(on(\"address.city\", eq(\"London\")))\n  .orderBy(asc(\"profile.age\")))\n  .toList();\n\n// Pagination\nList\u003cUser\u003e users = userRepo.find(q -\u003e q\n  .where(on(\"active\", eq(true)))\n  .orderBy(desc(\"score\"))\n  .skip(20)\n  .limit(10))\n  .toList(); // Page 3 of results\n\n// Advanced: string predicates, case-insensitive matching, IN/NOT IN, null checks\nList\u003cUser\u003e results = userRepo.find(q -\u003e q\n  .where(and(\n    on(\"name\", contains(\"smith\").ignoreCase()),      // .ignoreCase() works with startsWith/endsWith/contains\n    on(\"username\", eqi(\"alice\")),                    // eqi() or eq().ignoreCase() for case-insensitive equals\n    on(\"role\", in(\"ADMIN\", \"MODERATOR\")),            // in() and notIn() for collections\n    on(\"level\", between(10, 50)),                    // between() is sugar for gte + lte\n    on(\"deletedAt\", notNull()),                      // isNull()/notNull() for null checks\n    or(\n      on(\"verified\", eq(true)),\n      on(\"email\", endsWith(\"@trusted.com\"))\n    )))\n  .orderBy(desc(\"level\"), asc(\"name\"))\n  .skip(0)\n  .limit(25))\n  .toList();\n```\n\n**Backend Support**:\n- **MongoDB**: Native query translation with `$gt`, `$and`, etc.\n- **PostgreSQL**: Native JSONB operators (`-\u003e`, `-\u003e\u003e`, `@\u003e`) with GIN indexes\n- **MariaDB**: Native JSON functions (`JSON_EXTRACT`, `JSON_UNQUOTE`) with proper type casting\n- **H2**: Native JSON field reference syntax (`(column).\"field\"`) with type casting\n- **Redis, Flat Files, In-Memory**: In-memory filter evaluation (fetch all, filter in Java)\n\n**Performance Note**: Native backends (MongoDB, PostgreSQL, MariaDB, H2) push filtering to the database. Other backends fetch all documents and filter in memory.\n\n## Update DSL\n\nModify documents with field and array operations:\n\n```java\nimport static eu.okaeri.persistence.filter.UpdateBuilder.*;\n\n// Update by ID - returns boolean (true if modified)\nboolean updated = users.updateOne(userId, u -\u003e u\n  .set(\"level\", 43)\n  .increment(\"exp\", 100));\n\n// Update by entity - returns boolean\nboolean updated = users.updateOne(alice, u -\u003e u\n  .push(\"achievements\", \"speedrun\"));\n\n// Update multiple with WHERE - returns count\nlong count = users.update(u -\u003e u\n  .where(on(\"level\", gte(10)))\n  .increment(\"exp\", 50));\n\n// Update and return NEW version\nOptional\u003cUser\u003e newVersion = users.updateOneAndGet(userId, u -\u003e u\n  .set(\"verified\", true));\n\n// Update and return OLD version\nOptional\u003cUser\u003e oldVersion = users.getAndUpdateOne(userId, u -\u003e u\n  .unset(\"tempToken\"));\n```\n\n**Operations**:\n\n```java\n// Field operations\n.set(\"name\", \"bob\")              // Set field value\n.set(\"profile.age\", 25)          // Nested fields supported\n.unset(\"token\")                  // Remove field (set to null)\n.increment(\"score\", 10)          // Add to number (use negative to subtract)\n.multiply(\"damage\", 1.5)         // Multiply number\n.min(\"bestTime\", 42.5)           // Update only if new value is smaller\n.max(\"highScore\", 1000)          // Update only if new value is larger\n.currentDate(\"updatedAt\")        // Set to current timestamp (ISO-8601)\n\n// Array operations\n.push(\"tags\", \"a\")               // Append value(s) to array\n.push(\"tags\", \"a\", \"b\", \"c\")     // Varargs for multiple values\n.popFirst(\"queue\")               // Remove first element\n.popLast(\"history\")              // Remove last element\n.pull(\"tags\", \"old\")             // Remove all occurrences of value\n.pull(\"flags\", null)             // Supports null\n.pullAll(\"roles\", \"A\", \"B\")      // Remove multiple values (varargs)\n.addToSet(\"badges\", \"new\")       // Add if not present (varargs, supports null)\n```\n\n**Important**: Each field can only appear once per update. Use multiple `.set()` calls for different fields, or chain separate `updateOne()` calls for complex scenarios.\n\n**Backend Support**:\n- **MongoDB/PostgreSQL**: Native atomic operations\n- **MariaDB**: Native atomic* operations\n  - *Non-atomic in-memory fallback for `pull`/`pullAll`/`addToSet`\n- **In-Memory**: Synchronized operations with per-document locking\n- **H2/Redis/Flat Files**: In-memory evaluation (non-atomic)\n\n## Repository Methods\n\nDefine methods in your repository interface and they're auto-implemented based on method name parsing (works for any field, but indexing recommended for performance):\n\n```java\n@DocumentCollection(path = \"players\", indexes = {\n    @DocumentIndex(path = \"username\", maxLength = 16),\n    @DocumentIndex(path = \"rank\", maxLength = 32),\n    @DocumentIndex(path = \"stats.level\")\n})\npublic interface PlayerRepository extends DocumentRepository\u003cUUID, Player\u003e {\n\n    // === Simple equality (parsed from method name) ===\n    Optional\u003cPlayer\u003e findByUsername(String username);\n    Stream\u003cPlayer\u003e streamByRank(String rank);\n    List\u003cPlayer\u003e findByRank(String rank);\n\n    // === Multiple conditions (AND/OR) ===\n    List\u003cPlayer\u003e findByRankAndUsername(String rank, String username);\n    List\u003cPlayer\u003e findByRankOrUsername(String rank, String username);\n    // AND has precedence: A OR B AND C → A OR (B AND C)\n    List\u003cPlayer\u003e findByUsernameOrRankAndLevel(String username, String rank, int level);\n\n    // === Nested properties (auto-discovered from camelCase or use $ as separator) ===\n    Stream\u003cPlayer\u003e findByStatsLevel(int level);      // statsLevel → stats.level\n    List\u003cPlayer\u003e findByStats$Score(int score);       // stats$Score → stats.score (explicit)\n\n    // === Ordering ===\n    List\u003cPlayer\u003e findByRankOrderByUsernameAsc(String rank);\n    List\u003cPlayer\u003e findAllOrderByStats$LevelDesc();\n    Stream\u003cPlayer\u003e streamAllOrderByUsernameAscRankDesc();\n\n    // === Limiting ===\n    Optional\u003cPlayer\u003e findFirstByOrderByStats$LevelDesc();  // First = limit 1\n    List\u003cPlayer\u003e findTop10ByRank(String rank);              // TopN = limit N\n\n    // === Count/Exists/Delete ===\n    long countByRank(String rank);\n    boolean existsByUsername(String username);\n    long deleteByRank(String rank);\n\n    // === Alternative prefixes (all equivalent to find) ===\n    Optional\u003cPlayer\u003e readByUsername(String username);\n    Optional\u003cPlayer\u003e getByUsername(String username);\n    List\u003cPlayer\u003e queryByRank(String rank);\n\n    // === Underscores for readability (ignored in parsing) ===\n    Optional\u003cPlayer\u003e findBy_username(String username);\n    List\u003cPlayer\u003e findBy_rank_and_username(String rank, String username);\n\n    // === Custom logic with default methods ===\n    default boolean isUsernameTaken(String username) {\n        return this.existsByUsername(username);\n    }\n\n    default Player getOrCreate(UUID id, String username) {\n        return findByPath(id).orElseGet(() -\u003e {\n            Player p = new Player();\n            p.setPath(id);\n            p.setUsername(username);\n            return save(p);\n        });\n    }\n}\n```\n\n**Method Name Syntax:**\n\n| Pattern                           | Example                                 | Description                    |\n|-----------------------------------|-----------------------------------------|--------------------------------|\n| `findBy{Field}`                   | `findByName(String)`                    | Simple equality                |\n| `findBy{A}And{B}`                 | `findByNameAndLevel(String, int)`       | AND conditions                 |\n| `findBy{A}Or{B}`                  | `findByNameOrEmail(String, String)`     | OR conditions                  |\n| `findBy{Field}OrderBy{F}Asc/Desc` | `findByActiveOrderByLevelDesc(boolean)` | With ordering                  |\n| `findAllOrderBy{Field}`           | `findAllOrderByNameAsc()`               | All with ordering              |\n| `findFirst...`                    | `findFirstByOrderByLevelDesc()`         | Limit to 1                     |\n| `findTop{N}...`                   | `findTop10ByActive(boolean)`            | Limit to N                     |\n| `countBy{Field}`                  | `countByActive(boolean)`                | Count matching                 |\n| `existsBy{Field}`                 | `existsByEmail(String)`                 | Check existence                |\n| `deleteBy{Field}`                 | `deleteByLevel(int)`                    | Delete matching                |\n| `streamBy{Field}`                 | `streamByLevel(int)`                    | Must return `Stream\u003cT\u003e`        |\n| `{field}${nested}`                | `findByProfile$Age(int)`                | Nested field (→ `profile.age`) |\n\n**Return Types:**\n- `Optional\u003cT\u003e` - Single result or empty\n- `Stream\u003cT\u003e` - Lazy stream (required for `stream` prefix)\n- `List\u003cT\u003e`, `Collection\u003cT\u003e`, `Set\u003cT\u003e` - Collected results\n- `T` (naked entity) - Single result or null\n- `long` - For count/delete operations\n- `boolean` - For exists operations\n\n**Note:** For complex queries (comparisons like `\u003e`, `\u003c`, `\u003e=`, regex, etc.), use the Query DSL instead:\n```java\nusers.find(q -\u003e q.where(on(\"level\", gt(10))).orderBy(desc(\"score\")));\n```\n\n**Built-in Methods** (from `DocumentRepository`):\n\n```java\n// Metadata\nDocumentPersistence getPersistence()\nPersistenceCollection getCollection()\nClass\u003c? extends Document\u003e getDocumentType()\n\n// Counting\nlong count()\n\n// Finding - by path\nOptional\u003cT\u003e findByPath(PATH path)\nT findOrCreateByPath(PATH path)\nCollection\u003cT\u003e findAll()\nCollection\u003cT\u003e findAllByPath(Iterable\u003cPATH\u003e paths)\nCollection\u003cT\u003e findOrCreateAllByPath(Iterable\u003cPATH\u003e paths)\nStream\u003cT\u003e streamAll()              // Safe but loads all data\nStream\u003cT\u003e stream(int batchSize)    // Memory-efficient, requires closing\nStream\u003cT\u003e stream()                 // stream(100) - requires closing\n\n// Finding - with queries\nStream\u003cT\u003e find(FindFilter filter)\nStream\u003cT\u003e find(Function\u003cFindFilterBuilder, FindFilterBuilder\u003e function)\nStream\u003cT\u003e find(Condition condition)\nOptional\u003cT\u003e findOne(Condition condition)\n\n// Saving\nT save(T document)\nIterable\u003cT\u003e saveAll(Iterable\u003cT\u003e documents)\n\n// Deleting - by path\nboolean deleteByPath(PATH path)\nlong deleteAllByPath(Iterable\u003cPATH\u003e paths)\nboolean deleteAll()\n\n// Deleting - with queries\nlong delete(DeleteFilter filter)\nlong delete(Function\u003cDeleteFilterBuilder, DeleteFilterBuilder\u003e function)\n\n// Updating - by path\nboolean updateOne(PATH path, Function\u003cUpdateBuilder, UpdateBuilder\u003e operations)\nboolean updateOne(T entity, Function\u003cUpdateBuilder, UpdateBuilder\u003e operations)\nOptional\u003cT\u003e updateOneAndGet(PATH path, Function\u003cUpdateBuilder, UpdateBuilder\u003e operations)\nOptional\u003cT\u003e getAndUpdateOne(PATH path, Function\u003cUpdateBuilder, UpdateBuilder\u003e operations)\n\n// Updating - with queries\nlong update(Function\u003cUpdateFilterBuilder, UpdateFilterBuilder\u003e updater)\n\n// Existence\nboolean existsByPath(PATH path)\n```\n\n## Switching Backends\n\nChange one line, everything else stays the same:\n\n```java\n// MongoDB\nnew DocumentPersistence(new MongoPersistence(mongoClient, \"mydb\", JsonSimpleConfigurer::new));\n\n// PostgreSQL\nnew DocumentPersistence(new PostgresPersistence(hikariDataSource, JsonSimpleConfigurer::new));\n\n// MariaDB\nnew DocumentPersistence(new MariaDbPersistence(hikariDataSource, JsonSimpleConfigurer::new));\n\n// H2\nnew DocumentPersistence(new H2Persistence(hikariDataSource, JsonSimpleConfigurer::new));\n\n// Redis\nnew DocumentPersistence(new RedisPersistence(redisClient, JsonSimpleConfigurer::new));\n\n// Flat files (YAML/JSON/HOCON)\nnew DocumentPersistence(new FlatPersistence(new File(\"./data\"), YamlBukkitConfigurer::new));\n\n// In-memory (volatile, no persistence)\nnew DocumentPersistence(new InMemoryPersistence());\n```\n\n**Namespace support**: Add `PersistencePath.of(\"prefix\")` as first parameter to prevent collection name conflicts when multiple apps share storage (e.g., `new MongoPersistence(PersistencePath.of(\"app\"), mongoClient, \"mydb\", JsonSimpleConfigurer::new)`).\n\nYour repositories, queries, and business logic stay the same.\n\n### Builder API\n\nAll backends support a fluent builder pattern for more explicit configuration:\n\n```java\n// MongoDB with builder\nMongoPersistence.builder()\n    .client(mongoClient)\n    .databaseName(\"mydb\")\n    .configurer(JsonSimpleConfigurer::new)\n    .serdes(new MySerdesPack())  // optional\n    .basePath(\"myapp\")           // optional namespace prefix\n    .build();\n\n// PostgreSQL with builder\nPostgresPersistence.builder()\n    .hikariConfig(hikariConfig)  // or .dataSource(hikariDataSource)\n    .configurer(JsonSimpleConfigurer::new)\n    .serdes(new MySerdesPack())\n    .basePath(\"myapp\")\n    .build();\n\n// MariaDB with builder\nMariaDbPersistence.builder()\n    .hikariConfig(hikariConfig)  // or .dataSource(hikariDataSource)\n    .configurer(JsonSimpleConfigurer::new)\n    .build();\n\n// H2 with builder\nH2Persistence.builder()\n    .hikariConfig(hikariConfig)  // or .dataSource(hikariDataSource)\n    .configurer(JsonSimpleConfigurer::new)\n    .build();\n\n// Redis with builder\nRedisPersistence.builder()\n    .client(redisClient)\n    .configurer(JsonSimpleConfigurer::new)\n    .basePath(\"myapp\")\n    .build();\n\n// Flat files with builder\nFlatPersistence.builder()\n    .storageDir(new File(\"./data\"))  // or .storageDir(Path.of(\"./data\"))\n    .configurer(YamlBukkitConfigurer::new)\n    .extension(\"yml\")                 // optional: override auto-detected extension\n    .build();\n```\n\n## Indexing\n\nDeclare indexes once in your `@DocumentCollection`:\n\n```java\n@DocumentCollection(\n    path = \"users\",\n    // keyLength auto-detected: UUID=36, Integer=11, Long=20, others=255 (override by specifying explicitly)\n    indexes = {\n        @DocumentIndex(path = \"username\", maxLength = 32),  // Optional (default: 255). Used only by MariaDB\n        @DocumentIndex(path = \"email\"),\n        @DocumentIndex(path = \"profile.age\"),\n        @DocumentIndex(path = \"settings.notifications.email\")\n    }\n)\n```\n\n| Backend        | keyLength Usage      | maxLength Usage            | Index Type                      |\n|----------------|----------------------|----------------------------|---------------------------------|\n| **MongoDB**    | Ignored              | Ignored                    | Native `createIndex()`          |\n| **PostgreSQL** | Uses for key VARCHAR | Ignored (uses JSONB GIN)   | Native JSONB expression indexes |\n| **MariaDB**    | Uses for key VARCHAR | Used for generated column* | Native stored generated columns |\n| **H2**         | Uses for key VARCHAR | Ignored                    | None                            |\n| **Redis**      | Ignored              | Ignored                    | None                            |\n| **Flat Files** | Ignored              | Ignored                    | In-memory (TreeMap + HashMap)   |\n| **In-Memory**  | Ignored              | Ignored                    | In-memory (TreeMap + HashMap)   |\n\n- `keyLength` auto-detected (UUID=36, Integer=11, Long=20, others=255) - used by JDBC backends for primary key VARCHAR\n- `maxLength` used by MariaDB for string fields only (numeric/boolean use fixed types)\n\n## Streaming Datasets\n\nTwo methods for processing collections:\n\n### streamAll() - Simple with Tradeoffs\n\nLoads all data, no resource management required. Best for small collections:\n\n```java\n// Stream all users - safe, no try-with-resources needed\nuserRepository.streamAll()\n  .filter(u -\u003e u.getLevel() \u003e 50)\n  .map(User::getName)\n  .forEach(System.out::println);\n\n// Custom queries return streams\nuserRepository.find(q -\u003e q.where(on(\"active\", eq(true))))\n  .parallel() // Process in parallel\n  .map(this::calculateStats)\n  .toList();\n```\n\n### stream(batchSize) - Memory Efficient\n\nFetches data in batches. **Must be closed** (use try-with-resources or `@Cleanup`):\n\n```java\n// Memory-efficient streaming with batches of 100\ntry (Stream\u003cUser\u003e stream = userRepository.stream(100)) {\n    return stream\n        .filter(u -\u003e u.isActive())\n        .map(User::getName)\n        .collect(Collectors.toList());\n}\n\n// Process large collection without loading all into memory\ntry (Stream\u003cUser\u003e stream = userRepository.stream(50)) {\n    stream.forEach(user -\u003e {\n        // Process each user as it's fetched (e.g., export, transform)\n        exportUser(user);\n    });\n}\n\n// Alternative: Lombok @Cleanup\n@Cleanup Stream\u003cUser\u003e stream = userRepository.stream(100);\nList\u003cString\u003e names = stream.map(User::getName).toList();\n```\n\n**Backend-specific batching:**\n- **PostgreSQL**: JDBC cursor (requires open transaction until closed)\n- **H2/MariaDB**: LIMIT/OFFSET pagination\n- **MongoDB**: Driver cursor with batchSize hint\n- **Redis**: HSCAN with custom step size\n\n## Advanced: Document References\n\nStore references to other documents using `EagerRef` or `LazyRef`:\n\n```java\npublic class Book extends Document {\n    private String title;\n    // EagerRef: fetches authors immediately when Book is loaded\n    // LazyRef: defers fetch until .get() is called\n    private List\u003cEagerRef\u003cAuthor\u003e\u003e authors;\n}\n\n// Creating references\nAuthor author = authorRepository.findOrCreateByPath(authorId);\nauthor.setName(\"Alice\");\nauthor.save();\n\nBook book = new Book();\nbook.setTitle(\"Some Book\");\nbook.setAuthors(List.of(EagerRef.of(author))); // Store reference\nbook.save();\n\n// Accessing references\nBook loaded = bookRepository.findByPath(bookId).orElseThrow();\nfor (Ref\u003cAuthor\u003e authorRef : loaded.getAuthors()) {\n    // EagerRef: already loaded, LazyRef: fetches now\n    Author author = authorRef.orNull();\n    System.out.println(author.getName());\n}\n```\n\n**How it works:** Refs serialize as `{\"_collection\": \"author\", \"_id\": \"uuid\"}` in the database. The field type (`EagerRef` vs `LazyRef`) controls when referenced documents are fetched during deserialization.\n\n**N+1 Warning:** Each ref triggers a separate database query (EagerRef on load, LazyRef on `.get()`). For documents with many refs, fetch referenced documents in bulk using `findAllByPath()` instead.\n\n## Real-World Example\n\nComplete user management system:\n\n```java\n// Document model\n@Data\npublic class UserAccount extends Document {\n    private String email;\n    private String username;\n    private UserProfile profile;\n    private String role; // e.g., \"USER\", \"ADMIN\", \"MODERATOR\"\n    private Instant createdAt;\n    private Instant lastLogin;\n}\n\n@Data\npublic class UserProfile {\n    private String displayName;\n    private String bio;\n    private String avatarUrl;\n    private Map\u003cString, Object\u003e preferences;\n}\n\n// Repository\n@DocumentCollection(\n    path = \"accounts\",\n    indexes = {\n        @DocumentIndex(path = \"email\"),\n        @DocumentIndex(path = \"username\", maxLength = 32),\n        @DocumentIndex(path = \"role\", maxLength = 16)\n    }\n)\npublic interface UserAccountRepository extends DocumentRepository\u003cUUID, UserAccount\u003e {\n\n    // Method names are parsed automatically - no annotations needed!\n    Optional\u003cUserAccount\u003e findByEmail(String email);\n    Optional\u003cUserAccount\u003e findByUsername(String username);\n    Stream\u003cUserAccount\u003e streamByRole(String role);\n\n    default UserAccount register(String email, String username) {\n        // WARNING: This has a race condition! In production, use:\n        // - External locking (e.g., Redisson distributed locks)\n        // - Action queue/message broker for sequential processing\n        if (findByEmail(email).isPresent()) {\n            throw new IllegalStateException(\"Email already registered\");\n        }\n\n        UserAccount account = new UserAccount();\n        account.setEmail(email);\n        account.setUsername(username);\n        account.setRole(\"USER\");\n        account.setCreatedAt(Instant.now());\n\n        UserProfile profile = new UserProfile();\n        profile.setDisplayName(username);\n        account.setProfile(profile);\n\n        return save(account);\n    }\n\n    default void updateLastLogin(UUID userId) {\n        findByPath(userId).ifPresent(account -\u003e {\n            account.setLastLogin(Instant.now());\n            save(account);\n        });\n    }\n\n    default List\u003cUserAccount\u003e getAdmins() {\n        return streamByRole(\"ADMIN\").toList();\n    }\n}\n\n// Usage\nUserAccountRepository accounts = persistence.createRepository(UserAccountRepository.class);\n\n// Register new user\nUserAccount alice = accounts.register(\"alice@example.com\", \"alice\");\n\n// Login\naccounts.findByEmail(\"alice@example.com\").ifPresent(account -\u003e {\n    accounts.updateLastLogin(account.getPath());\n    System.out.println(\"Welcome back, \" + account.getUsername());\n});\n\n// Find all admins (using indexed field)\nList\u003cUserAccount\u003e admins = accounts.getAdmins();\n\n// Search users by role with ordering\nList\u003cUserAccount\u003e moderators = accounts.find(q -\u003e q\n  .where(on(\"role\", eq(\"MODERATOR\")))\n  .orderBy(asc(\"username\")))\n  .toList();\n```\n\n## Backend Comparison\n\n| Backend        | Indexes            | Query DSL | Update DSL               | Best For                 |\n|----------------|--------------------|-----------|--------------------------|--------------------------|\n| **MongoDB**    | Native             | Native    | Native (atomic)          | Document workloads       |\n| **PostgreSQL** | Native (JSONB)     | Native    | Native (atomic)          | Already using Postgres   |\n| **MariaDB**    | Native (gen. col.) | Native    | Native (atomic)*         | Already using MariaDB    |\n| **H2**         | None               | Native    | In-memory                | Testing/Embedded         |\n| **Redis**      | None               | In-memory | In-memory                | Fast key-value access    |\n| **Flat Files** | In-memory          | In-memory | In-memory                | Config files, small apps |\n| **In-Memory**  | In-memory          | In-memory | In-memory (synchronized) | Testing, temp state      |\n\n## Configurer Support\n\nSerialization formats from [okaeri-configs](https://github.com/OkaeriPoland/okaeri-configs):\n\n```java\n// JSON (all backends) - configurer passed to backend constructor\nnew DocumentPersistence(new MongoPersistence(mongoClient, \"mydb\", JsonSimpleConfigurer::new))\n\n// YAML/HOCON/TOML (flat files only)\nnew DocumentPersistence(new FlatPersistence(new File(\"./data\"), YamlBukkitConfigurer::new))\n```\n\nMongoDB, PostgreSQL, MariaDB, H2, and Redis require a JSON configurer. In-Memory uses an internal configurer. Flat Files support any format.\n\n## Related Projects\n\n- [okaeri-configs](https://github.com/OkaeriPoland/okaeri-configs) - Configuration library powering the serialization\n- [okaeri-platform](https://github.com/OkaeriPoland/okaeri-platform) - Full application framework using okaeri-persistence\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fokaeripoland%2Fokaeri-persistence","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fokaeripoland%2Fokaeri-persistence","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fokaeripoland%2Fokaeri-persistence/lists"}