I'm facing an issue in the Reliable Dictionary where I update a particular entry, but do not commit the transaction. It appears that the transaction abort does not reset the updated entry.
In my logic, I have to check if a few seats are available in a reliable dictionary. If they are, I assign them to the Order. If one of them isn't available, I'd ideally like to abort the transaction ensuring that the previously assigned seats would roll back to their original state, since I didn't commit the transaction.
Could I be doing something wrong here?
Here is the code I'm building:
var unavailableSeats = new List<string>();
using (var tx = StateManager.CreateTransaction())
{
foreach (var requestSeat in request.Seats)
{
var match = await dict.Value.TryGetValueAsync(tx, requestSeat.ToString(), LockMode.Update);
if (!match.HasValue)
{
response.SetError($"No Seat found matching the Seat Key: {requestSeat} provided.");
return response;
}
var seatEntry = match.Value;
if (seatEntry.IsAvailable())
{
seatEntry.AssignToOrder(request.OrderId, request.RequestId.ToString());
await dict.Value.SetAsync(tx, requestSeat.ToString(), seatEntry, TimeSpan.FromSeconds(4),
cancellationToken);
}
else
{
unavailableSeats.Add(requestSeat.ToString());
}
}
if (!unavailableSeats.Any())
{
await tx.CommitAsync();
response.Success = true;
response.RequestId = request.RequestId;
return response;
}
tx.Abort();
}
You are modifying the in memory entity which is what is stored in the dictionary. You need to make a copy of the object before you modify any property. This is documented in a few places, but specifically mentioned in this article https://learn.microsoft.com/en-us/azure/service-fabric/service-fabric-work-with-reliable-collections in the common pitfalls section.
Instead of
var seatEntry = match.Value
do
var seatEntry = new SeatEntryType(match.Value) // assuming copy constructor
Related
how I can show DeliveryBoys in a specific location, in my realtime database I have a value that I need to compare drivers with which is "City" I would like to have all DeliveryBoys that are in a specific city. How can I do that? Using flutter
Am only able to get all drivers without a conditional statement
**This is my Function that i want to modify **
retrieveOnlineDriversInformation(List onlineNearestDriversList) async {
DatabaseReference ref =
FirebaseDatabase.instance.ref().child("DeliveryBoys");
for (int i = 0; i < onlineNearestDriversList.length; i++) {
await ref
.child(onlineNearestDriversList[i].driverId.toString())
.once()
.then((dataSnapshot) {
var driverKeyInfo = dataSnapshot.snapshot.value;
dList.add(driverKeyInfo);
});
}
}
Database Structure
Based on your responses and as far as I can see, you don't need the loop where you have it. Therefore, I am going to ignore it and simply show you the code that will return the list of driver ids of all drivers for city 'Lusaka'.
Future<List<String>> retrieveOnlineDriversInformation() async {
final driverIds = <String>[];
DatabaseReference ref = FirebaseDatabase.instance.ref().child("drivers");
try {
await ref.orderByChild("city")
.equalTo("Lusaka")
.once()
.then(
(event) {
if (event.snapshot.value != null) {
final driverListData =
Map<String, dynamic>.from(event.snapshot.value! as Map);
driverListData.forEach((key, value) {
driverIds.add(key);
});
}
},
} on FirebaseException catch (error, stackTrace) {
// < Some code here to print database error details or otherwise deal with it >
} catch (error, stackTrace) {
// < Some code here to print other error details or otherwise deal with it >
}
return driverIds;
}
You could instead modify this to just return the Map 'driverListData' which contains each driver's id and associated driver data.
A couple of other points:
You don't stick to a standard naming convention for your database node and field names. I suggest that you always use lowerCamelCase as the standard (so for example, change DriverLicense to driverLicense) as it will match what you typically name the variables within the Flutter/Dart code.
You don't need to hold the driver id as a separate field in the driver node. It is a duplicate (and therefore wastes space on the database) of the driver record key, which is already accessible to you.
As you see, you should always wrap your database call logic in a try / catch clauses in order to handle any errors that the call to the database may return. There are specific exceptions that can be tested for with the on clause.
Currently, I am working on a Flutter project, which is a tricycle booking system. Right now, I want to implement the functionality of having life points for every account. I have thought of this for some time and I have decided to store a value in the real-time database in firebase, it is where I will be decrementing the life points every time a user cancels the booking. What I want to do right now, is to check if the value stored in lifePoints is equal to a certain value, if yes, then I will be placing some functions and restrictions in there. So, how do i get the data from realtime database using flutter and add conditions to it?
Any help will be much appreciated. Thank you!
If you want to read data from Firebase, the documentation on reading data is a great starting point.
From there comes this great example of reading data once:
final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
print(snapshot.value);
} else {
print('No data available.');
}
And this example for listening for data, which provides both the current value right away, and then continues to listen for updates:
DatabaseReference starCountRef =
FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
final data = event.snapshot.value;
updateStarCount(data);
});
If you want to increment/decrement a value in the database, have a look at this example from the documentation on atomic increments/decrements:
void addStar(uid, key) async {
Map<String, Object?> updates = {};
updates["posts/$key/stars/$uid"] = true;
updates["posts/$key/starCount"] = ServerValue.increment(1);
updates["user-posts/$key/stars/$uid"] = true;
updates["user-posts/$key/starCount"] = ServerValue.increment(1);
return FirebaseDatabase.instance.ref().update(updates);
}
Or if you want to perform a more complex update of a value based on its current value, you'll want to use a transaction:
DatabaseReference postRef =
FirebaseDatabase.instance.ref("posts/foo-bar-123");
TransactionResult result = await postRef.runTransaction((Object? post) {
// Ensure a post at the ref exists.
if (post == null) {
return Transaction.abort();
}
Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
if (_post["stars"] is Map && _post["stars"][uid] != null) {
_post["starCount"] = (_post["starCount"] ?? 1) - 1;
_post["stars"][uid] = null;
} else {
_post["starCount"] = (_post["starCount"] ?? 0) + 1;
if (!_post.containsKey("stars")) {
_post["stars"] = {};
}
_post["stars"][uid] = true;
}
// Return the new data.
return Transaction.success(_post);
});
As you might notice these are all code snippets from the documentation, all from the same page even. I recommend spending some time studying that documentation, and then trying to apply these to your own use-case. If you then run into problems while implementing the use-case, post a question with the minimal code that reproduces where you got stuck and we can probably help further.
I've been trying to figure out transactions all morning and I'm stuck. I keep going around in circles, and I know what I need to do(I think), but I'm not sure how to do it.
Here's my code:
Future<String> obtainUniqueUsername(
String requestedUsername,
String currentUserID,
) async {
var userID = currentUserID;
var userName = requestedUsername.toLowerCase();
try {
// Create a reference to a usernames collection .doc(id)
final userRef = FirebaseFirestore.instance.doc('usernames/$userName');
// Start a transaction to check if the username is assigned
// if assigned a .doc(userName) will exist.
FirebaseFirestore.instance.runTransaction((transaction) async {
var userSnapshot = await transaction.get(userRef);
if (userSnapshot.exists) {
// Username is assigned as .doc(userName) exists
return 'Username is already assigned';
} else {
// Assign username by created .doc(userName)
// insert document reference to user
transaction.set(userRef, {'uid': userID});
}
}).then((value) => 'Username assigned', onError: (e) => 'Transaction error ${e.toString()}');
} // endtry
catch (e) {
return e.toString();
} // endcatch
return 'Transaction has failed, please try again later';
} // end
My problem is that it keeps hitting the final return statement, even if it has created the document. My understanding is that the transaction will keep trying until it is successful and returns a value, or it times out and throws an error. I've read that using .then doesn't await a value, and the function continues uninterrupted until it hits the end, but shouldn't it be either a value or an error?
I feel like I'm missing the point somewhere, sorry if this is super basic, I've really been trying to get it to work.
I got it sorted out! It took forever though, lots of trial and error. I think my issue was that I didn't understand how to return the value from the future using the dart shorthand from the examples I was following.
My understanding now is that I'm awaiting usernameAssigned to complete, and the future is completed when I return 'success'.
If the transaction didn't go through, it would throw an exception, and be caught and I would get a message about what went wrong. I think the assignment of transactionStatus isn't required, but I kind of like having it there for my own understanding of what's happening. Maybe when I get more experience I wont need stuff like that.
I also didn't really know how to use a try/catch block, but I think I've improved how I do that as well. Now, I throw an exception if something weird happens instead of going right to return. I think that's better?
Also, the comment about letting firestore assign the unique uid: Yeah, you are right, but I want to have firestore enforce unique human readable usernames. The autogenerated uid is used for everything else.
The usecase is having a usernames collection built like: .doc(uniqueUsername).data({"uid:" uid})
Firestore won't let duplicate doc ID's happen in that collection, so it gives me what I want. My understanding is this is one of the "easier" ways to enforce unique usernames.
Anyways, thanks for the comments and please send me any feedback you may have!
Future<String> obtainUniqueUsername(
String requestedUsername,
String currentUserID,
) async {
Future<String> obtainUniqueMarkerUsername(
String requestedUsername,
String currentUserID,
) async {
var userID = currentUserID;
var userName = requestedUsername.toLowerCase();
var transactionStatus = '';
try {
// Start a transaction to check if the username is assigned
// if assigned a .doc(userName) will exist.
var usernameAssigned = await FirebaseFirestore.instance
.runTransaction((transaction) async {
// Create a reference to a usernames collection .doc(id)
var userRef = FirebaseFirestore.instance.doc('usernames/$userName');
var userSnapshot = await transaction.get(userRef);
if (userSnapshot.exists == true) {
// Username is assigned as .doc(userName) exists
throw Exception('Username already exists');
} else {
// Assign username by created .doc(userName)
// insert document reference to user
transaction.set(userRef, {"uid": userID});
return 'Success';
}
});
transactionStatus = usernameAssigned;
} // endtry
catch (e) {
return e.toString();
} // endcatch
userID = '';
userName = '';
return transactionStatus;
} // end
I'm having a huge issue that I've been trying for days to get through. I have a scenario in which I'm trying to handle an Insert Conflict in my Xamarin project. The issue is that the record in the Cloud DB doesn't exist because there was an issue with a foreign key constraint so I'm in a scenario in which the sync conflict handler needs to delete the local record along with the record in the __operations table in SQLite. I've tried everything. Purge with the override set to 'true' so that it should delete the local record and all operations associated. Doesn't work. I've been just trying to force delete it by accessing the SQL store manually:
var id = localItem[MobileServiceSystemColumns.Id];
var operationQuery = await store.ExecuteQueryAsync("__operations", $"SELECT * FROM __operations WHERE itemId = '{id}'", null).ConfigureAwait(false);
var syncOperation = operationQuery.FirstOrDefault();
var tableName = operation.Table.TableName;
await store.DeleteAsync(tableName, new List<string>(){ id.ToString() });
if (syncOperation != null)
{
await store.DeleteAsync("__operations", new List<string>() { syncOperation["id"].ToString() }).ConfigureAwait(false);
}
I am able to query the __operations table and I can see the ID of the item I want to delete. The DeleteAsync method runs without exception but no status is returned so I have no idea if this worked or not. When I try to sync again the operation stubbornly exists. This seems ridiculous. How do I just delete an operation without having to sync with the web service? I'm about to dig down further and try to force it even harder by using the SQLiteRaw library but I'm really really hoping I'm missing something obvious? Can anyone help? THANKS!
You need to have a subclass of the Microsoft.WindowsAzure.MobileServices.Sync.MobileServiceSyncHandler class, which overrides OnPushCompleteAsync() in order to handle conflicts and other errors. Let's call the class SyncHandler:
public class SyncHandler : MobileServiceSyncHandler
{
public override async Task OnPushCompleteAsync(MobileServicePushCompletionResult result)
{
foreach (var error in result.Errors)
{
await ResolveConflictAsync(error);
}
await base.OnPushCompleteAsync(result);
}
private static async Task ResolveConflictAsync(MobileServiceTableOperationError error)
{
Debug.WriteLine($"Resolve Conflict for Item: {error.Item} vs serverItem: {error.Result}");
var serverItem = error.Result;
var localItem = error.Item;
if (Equals(serverItem, localItem))
{
// Items are the same, so ignore the conflict
await error.CancelAndUpdateItemAsync(serverItem);
}
else // check server item and local item or the error for criteria you care about
{
// Cancels the table operation and discards the local instance of the item.
await error.CancelAndDiscardItemAsync();
}
}
}
Include an instance of this SyncHandler() when you initialize your MobileServiceClient:
await MobileServiceClient.SyncContext.InitializeAsync(store, new SyncHandler()).ConfigureAwait(false);
Read up on the MobileServiceTableOperationError to see other conflicts you can handle as well as its methods to allow resolving them.
I've got a mySql db with non-standard IDs and field names, so I was trying to use both jsonResultsAdapterProvider and setRestangularFields. Here's the code in my app.config file:
RestangularProvider.setBaseUrl(remoteServiceName);
RestangularProvider.setRestangularFields({id: 'personID'});
RestangularProvider.addResponseInterceptor(function(data, operation, what, url, response, deferred) {
if (data.error) {
return data.error;
}
var extractedData = data.result;
return jsonResultsAdapterProvider.$get().camelizeKeys(extractedData);
});
RestangularProvider.addRequestInterceptor(function(elem, operation, what, url) {
return jsonResultsAdapterProvider.$get().decamelizeKeys(elem);
});
It's all good until I try to do a put/save. When I look at the request payload within the browser dev tools, it's: {"undefined":12842} (but the url is correct, so I know the id is set) If I don't use the ResultsAdapter and change the id field to Person_ID, payload looks good, so I know I'm making the right calls to Get and Save the Restangular objects. But for what it's worth, here's the code:
$scope.tests = Restangular.all('members').getList().$object;
vm.testEdit = function () {
$scope.test = Restangular.one('members', 12842).get().then(function(test) {
var copy = Restangular.copy(test);
copy.title = 'xxxx';
copy.put(); // payload was: undefined: 12842
});
}
// I also tried customPUT...
// copy.customPUT(copy, '', {}, {'Content-Type':'application/x-www-form-urlencoded'});
I tried "fixing" the id other ways too, too. like this:
Restangular.extendModel('members', function(model) {
model.id = model.personID;
return model;
});
but that messed up the urls, causing missing ids. And I tried getIdFromElem, but it only got called for my objects created with Restangular.one(), not with Restangular.all()
Restangular.configuration.getIdFromElem = function(elem) {
console.log('custom getIdFromElem called');
if (elem.route === 'members') { // this was never true
return elem[personID];
}
};
It seems like Restangular needs to substitute 'personID' most of the time, but maybe it needs 'Person_ID' at some point during the Save? Any ideas on what I could try to get the Save working?
I finally figured it out! The problem was in my config code and in the way I was decamelizing. Because of inconsistencies in my db field names (most use underscores, but some are already camelCase), I was storing the server's original elem names in an array within the jsonResultsAdapterProvider. But since I was calling jsonResultsAdapterProvider.$get().camelizeKeys(extractedData); within the interceptors, I was reinstantiating the array each time I made a new request. So, the undefined in the PUT request was coming from my decamelizeKeys() method.
My updated config code fixed the problem:
RestangularProvider.setBaseUrl(remoteServiceName);
RestangularProvider.setRestangularFields({id: 'personID'});
var jsonAdapter = jsonResultsAdapterProvider.$get();
RestangularProvider.addResponseInterceptor(function(data, operation, what, url, response, deferred) {
if (data.error) {
return data.error;
}
var extractedData = data.result;
// return extractedData;
return jsonAdapter.camelizeKeys(extractedData);
});
RestangularProvider.addRequestInterceptor(function(elem, operation, what, url) {
return jsonAdapter.decamelizeKeys(elem);
});