Writing
Ditto is a distributed database that optimizes for availability, which means that you can always write to your local database even if you are disconnected from the internet. Writes from all peers will synchronize and Ditto will resolve any conflicts.
Ditto is a delta state CRDT, which means Ditto only replicates the data that has changed. This makes Ditto very fast.
Upserting
Ditto doesn't have a concept of "inserting" data, only "upserting" data. This means that each device must assume that their operations are modifying documents that may already exist on some other device in the network. This is because it is possible that the current device just hasn't synchronized that document yet.
Creating a document
If you do not supply an _id
, Ditto will create one for you and return it. Note that the _id
is
immutable. This means that you cannot change the _id
once you have created the document.
const docID = await ditto.store.collection('people').upsert({ name: 'Susan', age: 31,})console.log(docID) // "507f191e810c19729de860ea"
do { // upsert JSON-compatible data into Ditto let docID = try ditto.store["people"].upsert([ "name": "Susan", "age": 31 ])} catch { //handle error print(error)}
DITDocumentID *docID = [[ditto.store collection:@"people"] upsert:@{ @"name": @"Susan", @"age": @31 } error:nil];
val docId2 = ditto.store["people"].upsert( mapOf( "name" to "Susan", "age" to 31 ))
Map<String, Object> content = new HashMap<>();content.put("name", "Susan");content.put("age", 31);DittoDocumentId docId = ditto.store.collection("people").upsert(content);// docId => 507f191e810c19729de860ea
var docId = ditto.Store.Collection("people").Upsert( new Dictionary<string, object> { { "name", "Susan" }, { "age", 31 }, });
json person = json({{"name", "Susan"}, {"age", 31}});DocumentId doc_id = ditto.get_store().collection("people").upsert(person);
let person = json!({ "name": "Susan".to_string(), "age": 31,});let collection = ditto.store().collection("people").unwrap();let id = collection.upsert(person).unwrap();
curl -X POST 'https://<CLOUD_ENDPOINT>/api/v3/store/write' \ --header 'X-DITTO-CLIENT-ID: AAAAAAAAAAAAAAAAAAAABQ==' \ --header 'Content-Type: application/json' \ --data-raw '{ "commands": [{ "method": "upsert", "collection": "people", "id": "abc123", "value": { "name": "Susan", "age": 31 } }] }'
Upserting with a specific _id
Upserting also supports behavior where you would want to modify fields in a
document, even if the document _id
does not not exist.
As an example, let's say we have a document in the people
collection that looks like this:
{ "_id": "123abc", "name": "Sam", "age": 45, "isOnline": false}
When you upsert
a document, only the supplied fields will be modified. Existing fields are left unmodified.
const docID = await ditto.store.collection('people').upsert({ _id: '123abc', name: 'Susan', age: 31,})
console.log(docID) // "123abc"
do { // upsert JSON-compatible data into Ditto let docID = try ditto.store["people"].upsert([ "_id": "abc123", "name": "Susan", "age": 31 ]) XCTAssertEqual(docID, "abc123")} catch { //handle error print(error)}
DITDocumentID *docId = [collection upsert:@{@"_id": @"123abc", @"name": @"Susan", @"age": @32 } error:nil];NSLog(@"%@", docId); // => "123abc"
val docId = ditto.store["people"].upsert( mapOf( "_id" to "123abc", "name" to "Susan", "age" to 31 ))
Map<String, Object> content = new HashMap<>();content.put("_id", "123abc");content.put("name", "Susan");content.put("age", 31);DittoDocumentId docId = ditto.store.collection("people").upsert(content);// docId => 123abc
var returnedId = ditto.Store.Collection("people").Upsert( new Dictionary<string, object> { { "_id", "123abc" }, { "name", "Joe" }, { "age", 32 }, { "isOnline", true } });
json person = json({{"_id", "123abc"}, {"name", "Susan"}, {"age", 31}});DocumentId doc_id = ditto.get_store().collection("people").upsert(person);
let doc_id = DocumentId::new(&"123abc".to_string()).unwrap();let person = json!({ // Person implements serde::Serialize "_id": doc_id, "name": "Susan".to_string(), "age": 31,});collection.upsert(person).unwrap();
curl -X POST 'https://<CLOUD_ENDPOINT>/api/v3/store/write' \ --header 'X-DITTO-CLIENT-ID: AAAAAAAAAAAAAAAAAAAABQ==' \ --header 'Content-Type: application/json' \ --data-raw '{ "commands": [{ "method": "upsert", "collection": "people", "id": "456abc", "value": { "name": "Susan", "age": 31 } }] }'
This results in the name
and age
changing, but isOnline
is untouched:
{ "_id": "123abc", "name": "Susan", "age": 31, "isOnline": false}
Default Data
Default Data can be thought of as "placeholder" data. Default Data is useful when your app needs to load "starter" data from an external data source, like from a backend API, on multiple devices without creating odd "overwriting" behavior.
Ditto's approach to conflict resolution orders changes by time. In most situations, this leads to predictable behavior. However, if your application is upserting the same initial data into multiple devices, such as common data from a central backend API, this could result in overwriting later changes:
- Device A upserts a document
{"firstName": "Adam"}
at time = 0 after downloading from a central API. - Device A updates the document to
{"firstName": "Max"}
at time = 1. - Device B synchronizes with Device A receiving the document
{"firstName": "Max"}
at time = 2. - Device B downloads the same document from the backend API
{"firstName": "Adam"}
and upserts at t = 3, which overwrites the previous change synced at time = 1.
In the above example, both Device A and B want to preserve the change by Device A that occurred after downloading the common data. To do so, Ditto offers an additional parameter: isDefault.
const docID = await ditto.store.collection('people').upsert( { name: 'Susan', age: 31, }, { writeStrategy: 'insertDefaultIfAbsent' },)
do { let docID = try ditto.store["people"].upsert([ "name": "Susan", "age": 31 ], writeStrategy: .insertDefaultIfAbsent)} catch { //handle error print(error)}
DITDocumentID *defaultDocID = [[ditto.store collection:@"people"] upsert:@{ @"name": @"Susan", @"age": @31 } writeStrategy: DITWriteStrategyInsertDefaultIfAbsent error:nil];
val docId = ditto.store.collection("people").upsert(mapOf( "name" to "Susan", "age" to 31), DittoWriteStrategy.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);
// Immediately try and insert some new default datavar docId = coll.Upsert( new Dictionary<string, object> { { "name", "Susan" } }, DittoWriteStrategy.InsertDefaultIfAbsent);
DocumentId doc_id = ditto.get_store().collection("people").upsert( content, WriteStrategy::insertDefaultIfAbsent);
let default_id = DocumentId::new(&"123abc".to_string()).unwrap();let data = json!({ // Person implements serde::Serialize "_id": default_id, "name": "Susan".to_string(), "age": 31,});collection .upsert_with_strategy(data, WriteStrategy::InsertDefaultIfAbsent) .unwrap();
Updating existing documents
Updating an existing document is different depending on the type being updated
- Set - sets value for a given key in the document
- Remove - removes a value for a given key in the document
- Replace with counter - will convert a number value for a given key into a counter
- Increment - unlike a number, a counter allows for increment operations (decrement is performed by incrementing by a negative increment) and these operations will converge
- Set - sets value for a given key in the map
- Remove - removes a value for a given key in the map
const docID = await ditto.store.collection('people').upsert({ name: 'Frank', age: 31, ownedCars: 0,})
await ditto.store .collection('people') .findByID(docID) .update((mutableDoc) => { mutableDoc.at('age').set(32)
mutableDoc.at('ownedCars').set(new Counter()) mutableDoc.at('ownedCars').counter.increment(1) })
do { let docID = try ditto.store["people"].upsert([ "name": "Frank", "age": 31, "ownedCars": 0 ])
ditto.store["people"].findByID(docID).update { mutableDoc in mutableDoc?["age"] = 32 mutableDoc?["ownedCars"].set(DittoCounter()) mutableDoc?["ownedCars"].counter?.increment(by: 1) }} catch { //handle error print(error)}
DITDocumentID *docID = [[ditto.store collection:@"people"] upsert:@{ @"name": @"Frank", @"age": [NSNumber numberWithInt:31], @"ownedCars": [NSNumber numberWithInt:0],} error:nil];
DITCollection *collection = [ditto.store collection:@"people"];[[collection findByID:docID] updateWithBlock:^(DITMutableDocument *doc) { [doc[@"age"] set:[NSNumber numberWithInt:32]]; [doc[@"ownedCars"] set:DITCounter.new]; [doc[@"ownedCars"].counter incrementBy:1];}];
ditto.store.collection("people").findById(frankId).update { mutableDoc -> mutableDoc?.let { it["age"].set(32) it["ownedCars"].counter!!.increment(amount = 1.0) }}
var content = new Dictionary<string, object>{ { "name", "Bob" }, { "age", 40 }, { "ownedCars", new DittoCounter() }};
var docId = Ditto.Store.Collection("people").Upsert(content);Ditto.Store.Collection("people").FindById(docId).Update(mutableDoc =>{ mutableDoc["age"].Set(32); mutableDoc["ownedCars"].Counter.Increment(1);});
DocumentId doc_id = ditto.get_store().collection("people").upsert( {{"name", "Frank"}, {"age", 31}, {"ownedCars", 0}});
ditto.get_store().collection("people").find_by_id(doc_id).update( [](MutableDocument &doc) { doc["age"].set(32); doc["ownedCars"].set(Counter()); auto counter = doc["ownedCars"].get_counter(); counter->increment(1); });
let collection = ditto.store().collection("people").unwrap();let doc_id = collection .upsert(json!({"name": "Frank", "owned_cars": 0})) .unwrap();
collection .find_by_id(doc_id) .update(|opt_doc| { if let Some(doc) = opt_doc { doc.set("age", 32).unwrap(); doc.set("owned_cars", DittoCounter::new()).unwrap(); doc.increment("owned_cars", 1.0).unwrap(); } }) .unwrap();
curl -X POST 'https://<CLOUD_ENDPOINT>/api/v3/store/write' \ --header 'X-DITTO-CLIENT-ID: AAAAAAAAAAAAAAAAAAAABQ==' \ --header 'Content-Type: application/json' \ --data-raw '{ "commands": [{ "method": "upsert", "collection": "people", "id": "123abc", "value": { "name": "Frank", "age": 32, "friends": ["Susan"], "owned_cars": 0 } }] }'
warning
Blocked transactions
You cannot start a write transaction within a write transaction.
At any given time, there can be only one write transaction. Any subsequent attempts to open another write transaction will become blocked and not progress until the existing write transaction finishes. The process of unblocking is automatic and you don’t need to write any code to handle this.
The following code will create a deadlock which never resolves itself.
// 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, ]) // ...}
This might manifest itself in the logs as:
LOG_LEVEL: Waiting for write transaction (elapsed: XXs), originator=User blocked_by=User
If the transaction never unblocks and the log messages at ERROR
level continue
forever - you have a strong indication that there’s a deadlock and should
investigate the application code.
Batching multiple updates in a single transaction
A write transaction allows a device to perform multiple operations within a single database call, that will be synchronized at the same time to other devices. You can perform insert, update, remove or evict operations on multiple collections more efficiently using this method.
warning
You cannot combine remove()
and upsert()
on the same document in the same write transaction. This is because you cannot update a document that has been removed.
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()}
NSArray *results = [store write:^(DITWriteTransaction *tx) { DITScopedWriteTransaction *cars = tx[@"cars"]; DITScopedWriteTransaction *people = tx[@"people"]; DITDocumentID *docID = [[DITDocumentID alloc] initWithValue: @"abc123"]; [people upsert:@{@"_id": docID, @"name": @"Susan"} error:nil]; [cars upsert:@{@"make": @"Ford", @"color": @"black", @"owner": docID} error:nil]; [cars upsert:@{@"make": @"Toyota", @"color": @"red", @"owner": docID} error:nil];}];
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()}
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)}});});