Best Practices for CRUD
To help you make the most out of Ditto's capabilities and ensure a frictionless experience for your end users, implement optimization strategies to enhance efficiency, speed, and precision of your operations.
This article offers a variety of optimization techniques that you can implement to improve the performance of your application.
When invoking the find and observeLocal methods to query values that may change dynamically during runtime, you can declare a top-level args variable that defines the dynamic values, and then pass the the $args identifier within your query conditions. That way, the query engine separates your query logic from the data so you can easily define and pass values as needed to adapt without having to change the query structure itself.
Using strings to filter dynamic data may impact the maintainability, security, and performance of your app by introducing various issues such as syntax errors in your code.
To reduce the number of network roundtrips needed for each individual operation, ensure data consistency, and simplify your code, use the write method to perform multiple upsert, update, remove, and evict operations across two or more document collections within a single Ditto call.
Additionally, you can incorporate logic that facilitates the retrieval of a document's current state and, when necessary, initiate a conditional update operation based on that state.
Initiating a write transaction within another write transaction can cause Ditto to hang indefinitely, resulting in a deadlock.
If a deadlock situation occurs, both transactions are unable to complete, the log messages at the ERROR level persist in a forever loop, and you’ll need to review your app’s codebase.
Do not combine remove and upsert functions for the same document within a single write transaction.
If you remove a document as part of the operation, potential concurrency conflicts may occur for it is impossible to update a document that no longer exists in Ditto.
The following snippet demonstrates a batch operation in which two write transactions are contained in a single transaction enclosure:
For more precise representations of date strings in queries for comparison operations, use the International Organization for Standardization (ISO) 8601 standard format. For an overview of operators and string functions and formats, see Ditto Basics > Query Tools.
ISO-8601 is an international standard for representing dates, times, and durations. The format adheres to specific patterns, such as the following standard formatting for date and time:
Date | Time |
YYYY-MM-DD | HH:mm:ss |
For more information about ISO 8601, see the official ISO documentation > ISO 8601 Date and time format.
The Ditto SDK offers write strategies that you can incorporate into your code to control how Ditto makes changes and resolves any potential concurrency conflicts, helping you manage your data more effectively.
A write strategy is effectively a set of instructions that tell Ditto how to handle specific changes and conflicts when you are updating and upserting data to the local Ditto store.
The Ditto SDK provides the following write strategies that you can use when performing update and upsert operation in your app:
The insertIfAbsent write strategy ensures that a new document with the same _id value is inserted only if there is no existing document with that ID in the given collection.
If a document with the same ID already exists, the upsert operation does not perform any changes and instead maintains the existing document.
The insertIfAbsent write strategy is useful for scenarios where you want to add new fields only when they are missing; if the fields exists, but the values are changed, the upsert operation does not update the changed fields.
The insertDefaultIfAbsent write strategy, when implemented, ensures that the given document upserts only if a document with the same ID does not already exist in the local Ditto store.
If the document does not exist and it is is upserted, it will be timestamped with a value of 0, making it appear as if it was performed before any other action. This approach helps manage concurrency conflicts and ensure that the data is inserted only when necessary.
The following snippet shows an example of the writeStrategy: insertDefaultIfAbsent parameter passed as an argument to theupsert method:
When upserting initial data from an external source to all peers at app startup, you must pass the enumeration value: writeStrategy: 'insertDefaultIfAbsent' as an argument in the upsert function.
Initial data is common data like sample chat messages from a central backend API that is accessible to end users at app startup.
Failing to indicate initial upserted data may risk future overwrites due to unbounded metadata, impacting storage, app performance, and stability.
For example, consider the concurrency conflict scenario and resolution outcome: Both Device A and Device B must retain the the change that occurred after Device B downloaded the common data — specifically, Device A’s change executed in step 2 at time = 1:
Device A downloads common data from a central API, and then upserts it as a document at time = 0:
Device A then upserts changes to the document at time = 1:
Device B syncs with Device A at time = 2:
Device B downloads the same common data, but from a backend API instead, and then upserts it as a document at time = 3.
The concurrent conflict that results from Device B’s upsert results in the previous upsert synced at time = 1 to be overwritten instead of being preserved:
All write transactions are event-driven, or asynchronous, but locally scoped by default.
When you have many documents to write to a peer, you can initiate a transaction in a way that avoids blocking or slowing down the main thread of your app by starting the transaction asynchronously; as in, it can occur concurrently with other Ditto operations.
For example, use DispatchQueue.global, as follows:
To avoid deadlocks, it is important to ensure that nested write transactions do not initiate within the scope of another write transaction.
A deadlock is a situation that occurs when a conflicting code pattern blocks two or more threads or processes from proceeding with their execution because each is waiting for a resource that the other holds (and never receives), resulting in a standstill of circular dependency.
In order to prevent potential conflicts that could arise from concurrent write operations, do not initiate a new write transaction to modify data until the current clause or write transaction is complete.
The following snippet demonstrates a queued write transaction that has the potential to create a deadlock situation. This is because the write transaction initiated on the "people" collection, along with its nested write transaction on the "settings" collection both attempt to acquire resources held by the other:
If you have a queued write transaction that remains blocked, the following message appears in your logs: