What happens if you put a transaction inside a transaction in objectify/datastore? What is the order of execution and how do things resolve?
For example,
1) If the inner transaction fails, will the outer transaction also fail?
2) If the outer transaction fails, will the inner transaction be rolled back if it happened to finish?
// Outer Transaction
Thing th = ofy().transact(() -> {
Thing thing = ofy().load().key(thingKey).now();
thing.modify();
ofy().save().entity(thing);
// Inner Transaction
// This transaction could be in another method used in various other places
Thing th2 = ofy().transact(() -> {
Thing thing2 = ofy().load().key(thingKey2).now();
thing2.modify();
Thing thing2 = ofy().load().key(thingKey3).now();
thing3.modify();
ofy().save().entity(thing2);
ofy().save().entity(thing3);
return thing;
});
return thing;
});
There is extensive documentation about this here:
https://github.com/objectify/objectify/wiki/Transactions
What question do you want to ask that isn't answered there?
Related
All of my firestore transactions fail when I want to get document.
I've tried getting other files changed rules to be public. I've found out that when i use if checking it seems like get function returned data.
val currentUserDocument = firebaseFirestore.collection("user").document(firebaseAuth.currentUser!!.uid)
val classMemberDocument = firebaseFirestore.collection("class").document(remoteClassID).collection("member").document(firebaseAuth.currentUser!!.uid)
firebaseFirestore.runTransaction { transaction ->
val userSnapshot = transaction.get(currentUserDocument)
val isInClass = userSnapshot.getBoolean("haveRemoteClass")!!
val classID = userSnapshot.getString("remoteClassID")!!
if (isInClass == true && classID == remoteClassID) {
transaction.update(currentUserDocument, "haveRemoteClass", false)
transaction.update(currentUserDocument, "remoteClassID", "")
transaction.delete(classMemberDocument)
} else {
throw FirebaseFirestoreException("You aren't in this class!", FirebaseFirestoreException.Code.ABORTED)
}
null
}
This typically means that the data that you're using in the transaction is seeing a lot of contention.
Each time you run a transaction, Firebase determines the current state of all documents you use in the transaction, and sends that state and the new state of those documents to the server. If the documents that you got were changed between when the transaction started and when the server gets it, it rejects the transaction and the client retries.
For the client to fail like this, it has to retry more often than is reasonable. Consider reducing the scope of your transaction to cover fewer documents, or find another way to reduce contention (such as the approach outlined for distributed counters).
We're using Firebase DB together with RxSwift and are running into problems with transactions. I don't think they're related to the combination with RxSwift but that's our context.
Im observing a data in Firebase DB for any value changes:
let child = dbReference.child(uniqueId)
let dbObserverHandle = child.observe(.value, with: { snapshot -> () in
guard snapshot.exists() else {
log.error("empty snapshot - child not found in database")
observer.onError(FirebaseDatabaseConsumerError(type: .notFound))
return
}
//more checks
...
//read the data into our object
...
//finally send the object as Rx event
observer.onNext(parsedObject)
}, withCancel: { _ in
log.error("could not read from database")
observer.onError(FirebaseDatabaseConsumerError(type: .databaseFailure))
})
No problems with this alone. Data is read and observed without any problems. Changes in data are propagated as they should.
Problems occur as soon as another part of the application modifies the data that is observer with a transaction:
dbReference.runTransactionBlock({ (currentData: FIRMutableData) -> FIRTransactionResult in
log.debug("begin transaction to modify the observed data")
guard var ourData = currentData.value as? [String : AnyObject] else {
//seems to be nil data because data is not available yet, retry as stated in the transaction example https://firebase.google.com/docs/database/ios/read-and-write
return TransactionResult.success(withValue: currentData)
}
...
//read and modify data during the transaction
...
log.debug("complete transaction")
return FIRTransactionResult.success(withValue: currentData)
}) { error, committed, _ in
if committed {
log.debug("transaction commited")
observer(.completed)
} else {
let error = error ?? FirebaseDatabaseConsumerError(type: .databaseFailure)
log.error("transaction failed - \(error)")
observer(.error(error))
}
}
The transaction receives nil data at first try (which is something you should be able to handle. We just just call
return TransactionResult.success(withValue: currentData)
in that case.
But this is propagated to the observer described above. The observer runs into the "empty snapshot - child not found in database" case because it receives an empty snapshot.
The transaction is run again, updates the data and commits successfully. And the observer receives another update with the updated data and everything is fine again.
My questions:
Is there any better way to handle the nil-data during the transaction than writing it to the database with FIRTransactionResult.success
This seems to be the only way to complete this transaction run and trigger a re-run with fresh data but maybe I'm missing something-
Why are we receiving the empty currentData at all? The data is obviously there because it's observed.
The transactions seem to be unusable with that behavior if it triggers a 'temporary delete' to all observers of that data.
Update
Gave up and restructured the data to get rid of the necessity to use transactions. With a different datastructure we were able to update the dataset concurrently without risking data corruption.
I have this function in my application. If the insert of Phrase fails then can someone tell me if the Audit entry still gets added? If that's the case then is there a way that I can package these into a single transaction that could be rolled back.
Also if it fails can I catch this and then still have the procedure exit with an exception?
[Route("Post")]
[ValidateModel]
public async Task<IHttpActionResult> Post([FromBody]Phrase phrase)
{
phrase.StatusId = (int)EStatus.Saved;
UpdateHepburn(phrase);
db.Phrases.Add(phrase);
var audit = new Audit()
{
Entity = (int)EEntity.Phrase,
Action = (int)EAudit.Insert,
Note = phrase.English,
UserId = userId,
Date = DateTime.UtcNow,
Id = phrase.PhraseId
};
db.Audits.Add(audit);
await db.SaveChangesAsync();
return Ok(phrase);
}
I have this function in my application. If the insert of Phrase fails
then can someone tell me if the Audit entry still gets added?
You have written your code in a correct way by calling await db.SaveChangesAsync(); only one time after doing all your modifications on the DbContext.
The answer to your question is: No, the Audit will not be added if Phrase fails.
Because you are calling await db.SaveChangesAsync(); after doing all your things with your entities, Entity Framework wil generate all the required SQL Queries and put them in a single SQL transaction which makes the whole queries as an atomic operation to your database. If one of the generated query e.g. Auditgenerated query failed then the transaction will be rolled back. So every modification that have been done to your database will be removed and so Entity Framework will let your database in a coherent state.
The application I have taken over has this code:
db.RunInTransaction(() =>
{
foreach (CategorySource category in categories)
{
db.Insert(category);
}
Console.Out.WriteLine("Categories: \r\n {0}", categories.Count);
}); db.Commit();
Can anyone tell me what is the benefit to running this in a transaction if there is no way to handle or check if there was a rollback?
For the example you showed us, namely doing a single insert, I don't see any benefit from doing it inside a transaction if there is no possibility to catch an exception and rollback. However, more likely the intention was to make a simple way to execute multiple SQL commands, e.g. multiple inserts, inside a single transaction. There would be a point of using a transaction there, if your code wanted those inserts to be made atomically from the point of view of other observers of your database. This holds true even if there is no chance for rollback, provided that you are reasonably certain/unconcerned that a failure might not happen.
From the sqlite-net documentation, there is a way to do a rollback, but you need to use the low level API, e.g.
db.BeginTransaction ();
try {
foreach (CategorySource category in categories)
{
db.Insert(category);
}
Console.Out.WriteLine("Categories: \r\n {0}", categories.Count);
db.Commit ();
}
catch (Exception) {
db.Rollback ();
throw;
}
I'm using Stephen Celis iOS lib for handling SQLite3 databases, here is the github link.
Taking the example on the git :
try db.transaction {
let rowid = try db.run(users.insert(email <- "betty#icloud.com"))
try db.run(users.insert(email <- "cathy#icloud.com", managerId <- rowid))
}
// BEGIN DEFERRED TRANSACTION
// INSERT INTO "users" ("email") VALUES ('betty#icloud.com')
// INSERT INTO "users" ("email", "manager_id") VALUES ('cathy#icloud.com', 2)
// COMMIT TRANSACTION
I tried to implement the commitHook block but it is fired for each insert. I'd like to fire an action only when all the requests are sent :-D
What should I do ?
Cheers
Edit :
Here is how I implemented the commit hook.
for bay in list{
try! self.themanager.db.transaction {
try! self.themanager.db.run(self.themanager.bays.insert(
//insert values
))
self.themanager.db.commitHook({
print("end commit hook")
})
}
}
Maybe it's related to my main loop :/
From SQLite TRIGGER docs:
At this time SQLite supports only FOR EACH ROW triggers, not FOR EACH STATEMENT triggers. Hence explicitly specifying FOR EACH ROW is optional. FOR EACH ROW implies that the SQL statements specified in the trigger may be executed (depending on the WHEN clause) for each database row being inserted, updated or deleted by the statement causing the trigger to fire.
Commit hooks work like triggers. Unfortunately, the "FOR EACH STATEMENT" behavior is not supported yet.