Platform Manual
CRUD Operations
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 the efficiency, speed, and precision of your create , read , update , and delete (crud) operations this article offers a variety of optimization techniques for improving app performance querying dynamic data using $args when invoking the find and observe local 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 let query = "color == $args color" let args = \[ "color" "blue" ] let documents = ditto store collection("your collection name") find(query, args) exec()val founddocs = ditto store collection("people") find("name == \\$args name && age <= \\$args age", mapof("name" to "max", "age" to 32))const query = 'name == $args name && age <= $args age' const documents = await ditto store collection('people') find(query, { age 32, name 'max', })map\<string, object> queryargs = new hashmap<>(); queryargs put("name", "max"); queryargs put("age", 32); list\<dittodocument> founddocs = ditto store collection("users") find("name == $args name && age <= $args age", queryargs) exec();var docs = ditto store collection("cars") find( "color == $args color", new dictionary\<string, object> { "color", "blue" } ) exec();json args = json({{"color", "red"}}); std vector\<document> big c values = ditto get store() collection("cars") find("color == $args color, args) exec();let query = "color == $args color"; let args = json!({ "color" "blue" }); let documents = collection find with args(query, args) exec()?; batching multiple operations 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 ditto store write { transaction in let cars = transaction scoped(tocollectionnamed "cars") let people = transaction scoped(tocollectionnamed "people") let docid = "abc123" do { try people upsert(\[" id" docid, "name" "susan"] as \[string any?]) try cars upsert(\["make" "ford", "color" "red", "owner" docid] as \[string any?]) try cars upsert(\["make" "toyota", "color" "black", "owner" docid] as \[string any?]) } catch (let err) { print(err localizeddescription) } people findbyid(docid) evict() }val results = ditto store write { transaction > val cars = transaction scoped("cars") val people = transaction scoped("people") val docid = "abc123" people upsert(mapof(" id" to docid, "name" to "susan")) cars upsert(mapof("make" to "hyundai", "color" to "red", "owner" to docid)) cars upsert(mapof("make" to "jeep", "color" to "pink", "owner" to docid)) people findbyid(dittodocumentid(docid)) evict() }const results = await ditto store write(async (transaction) => { // use the `writetransaction` object that is available in the // transaction closure to acquire a handle to any collection, which // allows modifying and accessing its documents in the transaction // context const cars = transaction scoped('cars') const people = transaction scoped('people') // in this example a new person and car document are created, and // finally the person document that was just created is evicted // if any of these operations fail, all others are not applied const susanid = await people upsert({ name 'susan', }) await cars upsert({ make 'hyundai', color 'red', owner susanid, }) await people findbyid(susanid) evict() }) // the return value of a transaction is a list that contains a // summary of all operations in the transaction and the document ids // that were affected // results == \[ // { // type 'inserted', // docid documentid { }, // collectionname 'people' // }, // { // type 'inserted', // docid documentid { }, // collectionname 'cars' // }, // { // type 'evicted', // docid documentid { }, // collectionname 'people' // } // ]val results = ditto store write { transaction > val cars = transaction scoped("cars") val people = transaction scoped("people") val docid = "abc123" people upsert(mapof(" id" to docid, "name" to "susan")) cars upsert(mapof("make" to "hyundai", "color" to "red", "owner" to docid)) cars upsert(mapof("make" to "jeep", "color" to "pink", "owner" to docid)) people findbyid(dittodocumentid(docid)) evict() }var results = ditto store write(writetxn => { var collonetx = writetxn scoped(collectionone name); var colltwotx = writetxn\[collectiontwo name];auto results = ditto get store() write(\[&]\(writetransaction \&write txn) { scopedwritetransaction people = write txn scoped("people"); scopedwritetransaction cars = write txn scoped("cars"); auto docid = "abc123"; people upsert({{"name", "susan"}, {" id", documentid(docid)}}); cars upsert({{"make", "hyundai"}, {"owner", documentid(docid)}}); cars upsert({{"make", "toyota"}, {"owner", documentid(docid)}}); });ditto store() with batched write(|batch| { let mut foo coll = batch collection("foo"); foo coll find () remove(); let mut bar coll = batch collection("bar"); // expensive multi mutation op for in 0 10 000 { let doc = ; bar coll insert(doc, none, false); } // commit (or revert) these changes batch commit changes() }) using iso 8601 for date strings 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 > docid\ esq8nyek3mspdw9 ecdh1 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 > https //www iso org/iso 8601 date and time format html implementing write strategies 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 insert if absent the insert if absent 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 insert if absent 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 let initialdocument \[string any] = \[ " id" "123456", "color" "blue" ] let documentid = ditto store collection("cars") upsert(initialdocument, writestrategy insertifabsent)val initialdocument = mapof( " id" to "123456", "color" to "blue" ) val documentid = ditto store collection("cars") upsert(initialdocument, dittowritestrategy insertifabsent)var results = ditto store write(writetxn => { var collonetx = writetxn scoped(collectionone name); // we are also testing the custom subscript accessor var colltwotx = writetxn\[collectiontwo name]; docthreeid = collonetx upsert(new dictionary\<string, object> { { "three", "document three" } }); docfourid = collonetx upsert(new dictionary\<string, object> { { "four", "document four" } }); collonetx findbyid(doconeid) evict(); docfiveid = colltwotx upsert(new dictionary\<string, object> { { "five", "document five" } }); colltwotx findbyid(doctwoid) update(doc => { doc\["two"] set("updated document two"); });auto results = ditto get store() write(\[&]\(writetransaction \&write txn) { scopedwritetransaction people = write txn scoped("people"); scopedwritetransaction cars = write txn scoped("cars"); auto docid = "abc123"; people upsert({{"name", "susan"}, {" id", documentid(docid)}}); cars upsert({{"make", "hyundai"}, {"owner", documentid(docid)}}); cars upsert({{"make", "toyota"}, {"owner", documentid(docid)}}); });ditto store() with batched write(|batch| { let mut foo coll = batch collection("foo"); foo coll find () remove(); let mut bar coll = batch collection("bar"); // expensive multi mutation op for in 0 10 000 { let doc = ; bar coll insert(doc, none, false); } // at this point, we must say whether we commit or revert // these changes batch commit changes() }) insert default if absent the insert default if absent 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 the upsert method do { let docid = try ditto store\["people"] upsert(\[ "name" "susan", "age" 31 ], writestrategy insertdefaultifabsent) } catch { //handle error print(error) }val initialdocument = mapof( " id" to "123456", "color" to "blue" ) val documentid = ditto store collection("cars") upsert(initialdocument, dittowritestrategy insertdefaultifabsent)const initialdocument = { id "123456", color "blue" } const documentid = await ditto store collection("cars") upsert(initialdocument, { writestrategy "insertdefaultifabsent" })map\<string, object> content = new hashmap<>(); content put("name", "susan"); content put("age", 31); dittodocumentid docid = ditto store collection("people") upsert(content, dittowritestrategy insertdefaultifabsent);// set the intial color to blue var initialdocument = new dictionary\<string, object> { { " id", "123456" }, { "color", "blue" }, }; ditto store collection("cars") upsert(initialdocument, dittowritestrategy insertdefaultifabsent)documentid doc id = ditto get store() collection("people") upsert( content, writestrategy insertdefaultifabsent);//not supported in rust example use case upserting initial data 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 {"firstname" "adam"} device a then upserts changes to the document at time = 1 {"firstname" "max"} device b syncs with device a at time = 2 {"firstname" "max"} 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 {"firstname" "adam"} implementing global async transactions 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 dispatchqueue global(qos default) async { ditto store write { transaction in let scope = transaction scoped(tocollectionnamed "passengers \\(thisflight)") // loop inside the transaction to avoid writing to database too frequently self passengers foreach { scope upsert($0 dict) } } }dispatchqueue global(qos default) async { ditto store write { transaction in let scope = transaction scoped(tocollectionnamed "passengers \\(thisflight)") // loop inside the transaction to avoid writing to database too frequently self passengers foreach { scope upsert($0 dict) } } }dispatchqueue global(qos default) async { ditto store write { transaction in let scope = transaction scoped(tocollectionnamed "passengers \\(thisflight)") // loop inside the transaction to avoid writing to database too frequently self passengers foreach { scope upsert($0 dict) } } }dispatchqueue global(qos default) async { ditto store write { transaction in let scope = transaction scoped(tocollectionnamed "passengers \\(thisflight)") // loop inside the transaction to avoid writing to database too frequently self passengers foreach { scope upsert($0 dict) } } }dispatchqueue global(qos default) async { ditto store write { transaction in let scope = transaction scoped(tocollectionnamed "passengers \\(thisflight)") // loop inside the transaction to avoid writing to database too frequently self passengers foreach { scope upsert($0 dict) } } }dispatchqueue global(qos default) async { ditto store write { transaction in let scope = transaction scoped(tocollectionnamed "passengers \\(thisflight)") // loop inside the transaction to avoid writing to database too frequently self passengers foreach { scope upsert($0 dict) } } }//not supported in rust avoiding deadlocks 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 example of a deadlock 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 // start a write transaction ditto store\["people"] findbyid(docid) update { mutabledoc in // start a write transaction within a write transacton // !! deadlocks !! let docid = try! ditto store\["settings"] upsert(\[ " id" "abc123", "preference" 31, ]) // }// start a write transaction ditto store\["people"] findbyid(docid) update { mutabledoc in // start a write transaction within a write transacton // !! deadlocks !! let docid = try! ditto store\["settings"] upsert(\[ " id" "abc123", "preference" 31, ]) // }// start a write transaction ditto store\["people"] findbyid(docid) update { mutabledoc in // start a write transaction within a write transacton // !! deadlocks !! let docid = try! ditto store\["settings"] upsert(\[ " id" "abc123", "preference" 31, ]) // }// start a write transaction ditto store\["people"] findbyid(docid) update { mutabledoc in // start a write transaction within a write transacton // !! deadlocks !! let docid = try! ditto store\["settings"] upsert(\[ " id" "abc123", "preference" 31, ]) // }// start a write transaction ditto store\["people"] findbyid(docid) update { mutabledoc in // start a write transaction within a write transacton // !! deadlocks !! let docid = try! ditto store\["settings"] upsert(\[ " id" "abc123", "preference" 31, ]) // }// start a write transaction ditto store\["people"] findbyid(docid) update { mutabledoc in // start a write transaction within a write transacton // !! deadlocks !! let docid = try! ditto store\["settings"] upsert(\[ " id" "abc123", "preference" 31, ]) // }//not supported in rust log message transaction remains blocked if you have a queued write transaction that remains blocked, the following message appears in your logs log level waiting for write transaction (elapsed xxs), originator=user blocked by=user