This guide covers migrating from Ditto’s legacy query builder API to DQL (Ditto Query Language) in Java. DQL provides a SQL-like syntax that’s more powerful and intuitive for querying and manipulating data.Note: This migration is required alongside the V4→V5 API Migration for a complete upgrade.
Use this prompt when working with an AI coding assistant to migrate your Ditto Java app to DQL.
Copy AI Migration Prompt (Click to Expand)
I need help migrating a Ditto Java application from legacy query builder to DQL (Ditto Query Language). Focus on these critical changes:QUERIES:Replace collection().find() with DQL SELECT statements- Use parameterized queries with Map<String, Object> arguments- Pass null as second argument for queries without parameters- Quote string literals in queries or use parametersMUTATIONS:Replace upsert/update/remove with DQL INSERT/UPDATE/EVICT- Use parameterized arguments for all values- Results are CompletableFuture-based with .thenAccept()OBSERVERS:Convert to registerObserver(query, args, callback)- Results are List<Map<String, Object>> instead of List<DittoDocument>- Same cleanup pattern with .stop()SUBSCRIPTIONS:Convert to registerSubscription(query, args)- Cleanup uses .cancel() instead of .close()COUNTERS:Critical change - use PN_INCREMENT BY in APPLY clause- Never initialize counter fields- Use APPLY clause: "APPLY viewCount = PN_INCREMENT BY :amount"BEFORE (LEGACY):```javaditto.getStore() .collection("cars") .find("color == 'red'") .exec() .thenAccept(docs -> { // Use docs });ditto.getStore() .collection("cars") .findById(new DittoDocumentID("abc123")) .update(mutableDoc -> { ((DittoCounter) mutableDoc.get("viewCount")).increment(1); });```AFTER (DQL):```javaMap<String, Object> args = Map.of("color", "red");ditto.getStore().execute( "SELECT * FROM cars WHERE color = :color", args).thenAccept(result -> { List<Map<String, Object>> cars = result.getItems(); // Use cars});Map<String, Object> args = Map.of( "id", "abc123", "amount", 1);ditto.getStore().execute( "UPDATE cars APPLY viewCount = PN_INCREMENT BY :amount WHERE _id = :id", args);```COMMON PITFALLS:- Always quote string literals or use parameters- Never initialize counter fields to 0- Use APPLY (not SET) for counter operations- Pass null for queries without parameters- Cast result values to appropriate typesWork through the codebase systematically. Show me each file's changes.
Map<String, Object> args = Map.of("id", "abc123");ditto.getStore().execute( "SELECT * FROM cars WHERE _id = :id", args).thenAccept(result -> { if (!result.getItems().isEmpty()) { Map<String, Object> car = result.getItems().get(0); // Use car }});
ditto.getStore().execute( "SELECT * FROM cars WHERE color = 'red'", null).thenAccept(result -> { List<Map<String, Object>> redCars = result.getItems(); // Use redCars});
ditto.getStore().execute( "SELECT * FROM cars", null).thenAccept(result -> { List<Map<String, Object>> items = result.getItems(); for (Map<String, Object> car : items) { String id = (String) car.get("_id"); String color = (String) car.get("color"); Integer miles = (Integer) car.get("miles"); System.out.println("Car " + id + " is " + color + " with " + miles + " miles"); }});
String query = "SELECT * FROM cars WHERE color = :color";Map<String, Object> args = Map.of("color", "red");DittoStoreObserver observer = ditto.getStore().registerObserver( query, args, (result) -> { List<Map<String, Object>> redCars = result.getItems(); // Update UI with redCars });// Later: clean upobserver.stop();
Key Differences:
DQL observers use registerObserver(query, args, callback) instead of .observe()
Results are List<Map<String, Object>> instead of List<DittoDocument>
Same cleanup pattern with .stop()
Performance Consideration: DQL observers provide more advanced return results including aggregates and projections. This requires more database full scans to ensure consistent results compared to the legacy query builder.Use indexes on query fields to maintain and improve observer performance. Indexes ensure your observers remain functional with optimal query performance.
Best Practice: Create Indexes for Observer Queries
// Create index on frequently queried fieldsditto.getStore().execute(""" CREATE INDEX idx_cars_color ON cars (color) """, null);// Then register observer - queries will use the indexDittoStoreObserver observer = ditto.getStore().registerObserver( "SELECT * FROM cars WHERE color = :color", Map.of("color", "red"), (result) -> { // Process results });
Critical Change: Counters are now modified using the PN_INCREMENT BY operation in the APPLY clause, not by setting values directly.
PN_COUNTER is the DQL equivalent of the legacy DittoCounter type. When migrating counter operations from the legacy query builder’s counter methods, use PN_INCREMENT BY in the APPLY clause. This maintains full compatibility with existing counter data created by DittoCounter.
DQL observers and queries benefit significantly from proper indexing. When migrating from the legacy query builder to DQL, creating indexes on frequently queried fields is essential for maintaining optimal performance.Why Indexes Matter for DQL:
DQL observers support advanced features like aggregates and projections
These advanced features require full database scans to ensure consistent results
Indexes dramatically reduce query execution time by avoiding full scans
Combining indexes with observers provides better performance than legacy query builder
Creating Indexes:
// Create index on single fieldditto.getStore().execute(""" CREATE INDEX idx_cars_color ON cars (color) """, null);// Create compound index on multiple fieldsditto.getStore().execute(""" CREATE INDEX idx_cars_color_year ON cars (color, year) """, null);// Create index on nested fieldditto.getStore().execute(""" CREATE INDEX idx_cars_location ON cars (_id.locationId) """, null);
Best Practices:
Create indexes on fields used in WHERE clauses
Create indexes before registering observers for those queries
Use compound indexes for queries with multiple filter conditions
Monitor query performance and add indexes as needed
For comprehensive information on indexing strategies, syntax, and best practices, see the DQL Indexing documentation.
Use :paramName for parameters, not string literals or concatenation.
// ❌ Wrong: Hardcoded string literal without quotesditto.getStore().execute("SELECT * FROM cars WHERE color = red", null);// ❌ Wrong: String concatenationString color = "red";ditto.getStore().execute("SELECT * FROM cars WHERE color = '" + color + "'", null);// ✅ Correct: Using :paramName with Map<String, Object>Map<String, Object> args = Map.of("color", "red");ditto.getStore().execute("SELECT * FROM cars WHERE color = :color", args);
Use COUNTER annotation in collection definitions. Do NOT use SET with COUNTER fields. Use APPLY with PN_INCREMENT BY. Pass negative values for decrements.
// ❌ Wrong: Initializing counter with a number (creates REGISTER, not COUNTER)Map<String, Object> doc = Map.of("_id", id, "counter", 0);ditto.getStore().execute( "INSERT INTO items DOCUMENTS (:doc)", Map.of("doc", doc));// ❌ Wrong: Using SET on counter fieldMap<String, Object> args = Map.of("id", id);ditto.getStore().execute( "UPDATE items SET counter = 5 WHERE _id = :id", args);// ✅ Correct: Use PN_INCREMENT BY with APPLY clause (creates counter on first use)Map<String, Object> args = Map.of("value", 1, "id", id);ditto.getStore().execute( "UPDATE COLLECTION items (counter COUNTER) APPLY counter PN_INCREMENT BY :value WHERE _id = :id", args);// ✅ Correct: Decrement by passing negative valueMap<String, Object> args = Map.of("value", -1, "id", id);ditto.getStore().execute( "UPDATE items APPLY counter PN_INCREMENT BY :value WHERE _id = :id", args);