* ObjectControllerTests.cs: chore: 简化 Object 保存逻辑
* ObjectTest.cs: * AVObject.cs: * AVRemoveOperation.cs:
parent
b0cf1caa5e
commit
bc5d396ab9
|
@ -35,20 +35,19 @@ namespace LeanCloudTests {
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task SaveWithPointer() {
|
public async Task SaveWithPointer() {
|
||||||
AVObject comment = AVObject.Create("Comment");
|
AVObject comment = new AVObject("Comment") {
|
||||||
comment["content"] = "Hello, Comment";
|
{ "content", "Hello, Comment" }
|
||||||
|
};
|
||||||
|
|
||||||
|
AVObject post = new AVObject("Post") {
|
||||||
AVObject post = AVObject.Create("Post");
|
{ "name", "New Post" },
|
||||||
post["name"] = "New Post";
|
{ "category", new AVObject("Category") }
|
||||||
|
};
|
||||||
AVObject category = AVObject.Create("Category");
|
|
||||||
post["category"] = category;
|
|
||||||
|
|
||||||
comment["post"] = post;
|
comment["post"] = post;
|
||||||
|
|
||||||
AVObject testPost = AVObject.Create("Post");
|
AVObject testPost = new AVObject("Post") {
|
||||||
testPost["name"] = "Test Post";
|
{ "name", "Test Post" }
|
||||||
|
};
|
||||||
comment["test_post"] = testPost;
|
comment["test_post"] = testPost;
|
||||||
|
|
||||||
await comment.SaveAsync();
|
await comment.SaveAsync();
|
||||||
|
@ -126,5 +125,12 @@ namespace LeanCloudTests {
|
||||||
await objList.SaveAllAsync();
|
await objList.SaveAllAsync();
|
||||||
await AVObject.DeleteAllAsync(objList);
|
await AVObject.DeleteAllAsync(objList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Set() {
|
||||||
|
AVObject obj = AVObject.Create("Foo");
|
||||||
|
obj["hello"] = "world";
|
||||||
|
TestContext.Out.WriteAsync(obj["hello"] as string);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using LeanCloud;
|
using LeanCloud;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
@ -32,12 +31,13 @@ namespace LeanCloudTests {
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task TestMassiveRequest() {
|
public async Task TestMassiveRequest() {
|
||||||
|
ThreadPool.SetMaxThreads(1, 1);
|
||||||
await Task.Run(() => {
|
await Task.Run(() => {
|
||||||
for (int i = 0; i < 10; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
for (int j = 0; j < 50; j++) {
|
for (int j = 0; j < 50; j++) {
|
||||||
AVObject obj = AVObject.Create("Foo");
|
AVObject obj = AVObject.Create("Foo");
|
||||||
obj.SaveAsync().ContinueWith(_ => {
|
obj.SaveAsync().ContinueWith(_ => {
|
||||||
TestContext.Out.WriteLine($"{obj.ObjectId} saved");
|
TestContext.Out.WriteLine($"{obj.ObjectId} saved at {Thread.CurrentThread.ManagedThreadId}");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Thread.Sleep(1000);
|
Thread.Sleep(1000);
|
||||||
|
|
|
@ -18,9 +18,9 @@ namespace LeanCloud.Storage.Internal
|
||||||
public object Encode()
|
public object Encode()
|
||||||
{
|
{
|
||||||
return new Dictionary<string, object> {
|
return new Dictionary<string, object> {
|
||||||
{"__op", "Remove"},
|
{ "__op", "Remove" },
|
||||||
{"objects", PointerOrLocalIdEncoder.Instance.Encode(objects)}
|
{ "objects", PointerOrLocalIdEncoder.Instance.Encode(objects) }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous)
|
public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous)
|
||||||
|
|
|
@ -378,8 +378,8 @@ string propertyName
|
||||||
if (value is AVObject) {
|
if (value is AVObject) {
|
||||||
// Resolve fetched object.
|
// Resolve fetched object.
|
||||||
var avObject = value as AVObject;
|
var avObject = value as AVObject;
|
||||||
if (fetchedObject.ContainsKey(avObject.ObjectId)) {
|
if (fetchedObject.TryGetValue(avObject.ObjectId, out AVObject obj)) {
|
||||||
value = fetchedObject[avObject.ObjectId];
|
value = obj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newServerData[pair.Key] = value;
|
newServerData[pair.Key] = value;
|
||||||
|
@ -542,10 +542,10 @@ string propertyName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual Task SaveAsync(bool fetchWhenSave = false, AVQuery<AVObject> query = null, CancellationToken cancellationToken = default) {
|
public virtual async Task SaveAsync(bool fetchWhenSave = false, AVQuery<AVObject> query = null, CancellationToken cancellationToken = default) {
|
||||||
IDictionary<string, IAVFieldOperation> currentOperations = null;
|
IDictionary<string, IAVFieldOperation> currentOperations = null;
|
||||||
if (!IsDirty) {
|
if (!IsDirty) {
|
||||||
return Task.FromResult(0);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Task deepSaveTask;
|
Task deepSaveTask;
|
||||||
|
@ -555,21 +555,18 @@ string propertyName
|
||||||
deepSaveTask = DeepSaveAsync(estimatedData, cancellationToken);
|
deepSaveTask = DeepSaveAsync(estimatedData, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
return deepSaveTask.OnSuccess(_ => {
|
try {
|
||||||
return ObjectController.SaveAsync(state,
|
await deepSaveTask;
|
||||||
currentOperations,
|
IObjectState objState = await ObjectController.SaveAsync(state,
|
||||||
FetchWhenSave || fetchWhenSave,
|
currentOperations,
|
||||||
query,
|
FetchWhenSave || fetchWhenSave,
|
||||||
cancellationToken);
|
query,
|
||||||
}).Unwrap().ContinueWith(t => {
|
cancellationToken);
|
||||||
if (t.IsFaulted || t.IsCanceled) {
|
HandleSave(objState);
|
||||||
HandleFailedSave(currentOperations);
|
} catch (Exception e) {
|
||||||
} else {
|
HandleFailedSave(currentOperations);
|
||||||
var serverState = t.Result;
|
throw e;
|
||||||
HandleSave(serverState);
|
}
|
||||||
}
|
|
||||||
return t;
|
|
||||||
}).Unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal virtual Task<AVObject> FetchAsyncInternal(
|
internal virtual Task<AVObject> FetchAsyncInternal(
|
||||||
|
@ -591,69 +588,60 @@ string propertyName
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task DeepSaveAsync(object obj, CancellationToken cancellationToken) {
|
private static async Task DeepSaveAsync(object obj, CancellationToken cancellationToken) {
|
||||||
var objects = new List<AVObject>();
|
var objects = new List<AVObject>();
|
||||||
CollectDirtyChildren(obj, objects);
|
CollectDirtyChildren(obj, objects);
|
||||||
|
|
||||||
var uniqueObjects = new HashSet<AVObject>(objects,
|
var uniqueObjects = new HashSet<AVObject>(objects, new IdentityEqualityComparer<AVObject>());
|
||||||
new IdentityEqualityComparer<AVObject>());
|
|
||||||
|
|
||||||
|
// 先保存文件对象(后面可以考虑将 AVFile 作为 AVObject 的子类型进行保存)
|
||||||
var saveDirtyFileTasks = DeepTraversal(obj, true)
|
var saveDirtyFileTasks = DeepTraversal(obj, true)
|
||||||
.OfType<AVFile>()
|
.OfType<AVFile>()
|
||||||
.Where(f => f.IsDirty)
|
.Where(f => f.IsDirty)
|
||||||
.Select(f => f.SaveAsync(cancellationToken: cancellationToken)).ToList();
|
.Select(f => f.SaveAsync(cancellationToken: cancellationToken)).ToList();
|
||||||
|
await Task.WhenAll(saveDirtyFileTasks);
|
||||||
|
|
||||||
return Task.WhenAll(saveDirtyFileTasks).OnSuccess(_ => {
|
IEnumerable<AVObject> remaining = new List<AVObject>(uniqueObjects);
|
||||||
IEnumerable<AVObject> remaining = new List<AVObject>(uniqueObjects);
|
while (remaining.Any()) {
|
||||||
return InternalExtensions.WhileAsync(() => Task.FromResult(remaining.Any()), () => {
|
// Partition the objects into two sets: those that can be saved immediately,
|
||||||
// Partition the objects into two sets: those that can be saved immediately,
|
// and those that rely on other objects to be created first.
|
||||||
// and those that rely on other objects to be created first.
|
var current = (from item in remaining
|
||||||
var current = (from item in remaining
|
where item.CanBeSerialized
|
||||||
where item.CanBeSerialized
|
select item).ToList();
|
||||||
select item).ToList();
|
var nextBatch = (from item in remaining
|
||||||
var nextBatch = (from item in remaining
|
where !item.CanBeSerialized
|
||||||
where !item.CanBeSerialized
|
select item).ToList();
|
||||||
select item).ToList();
|
remaining = nextBatch;
|
||||||
remaining = nextBatch;
|
|
||||||
|
|
||||||
if (current.Count == 0) {
|
if (current.Count == 0) {
|
||||||
// We do cycle-detection when building the list of objects passed to this
|
// We do cycle-detection when building the list of objects passed to this
|
||||||
// function, so this should never get called. But we should check for it
|
// function, so this should never get called. But we should check for it
|
||||||
// anyway, so that we get an exception instead of an infinite loop.
|
// anyway, so that we get an exception instead of an infinite loop.
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Unable to save a AVObject with a relation to a cycle.");
|
"Unable to save a AVObject with a relation to a cycle.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var states = (from item in current
|
||||||
|
select item.state).ToList();
|
||||||
|
var operationsList = (from item in current
|
||||||
|
select item.StartSave()).ToList();
|
||||||
|
|
||||||
|
var saveTasks = ObjectController.SaveAllAsync(states,
|
||||||
|
operationsList,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var serverStates = await Task.WhenAll(saveTasks);
|
||||||
|
foreach (var pair in current.Zip(serverStates, (item, state) => new { item, state })) {
|
||||||
|
pair.item.HandleSave(pair.state);
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
// Save all of the objects in current.
|
foreach (var pair in current.Zip(operationsList, (item, ops) => new { item, ops })) {
|
||||||
return AVObject.EnqueueForAll<object>(current, toAwait => {
|
pair.item.HandleFailedSave(pair.ops);
|
||||||
return toAwait.OnSuccess(__ => {
|
}
|
||||||
var states = (from item in current
|
throw e;
|
||||||
select item.state).ToList();
|
}
|
||||||
var operationsList = (from item in current
|
}
|
||||||
select item.StartSave()).ToList();
|
|
||||||
|
|
||||||
var saveTasks = ObjectController.SaveAllAsync(states,
|
|
||||||
operationsList,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
return Task.WhenAll(saveTasks).ContinueWith(t => {
|
|
||||||
if (t.IsFaulted || t.IsCanceled) {
|
|
||||||
foreach (var pair in current.Zip(operationsList, (item, ops) => new { item, ops })) {
|
|
||||||
pair.item.HandleFailedSave(pair.ops);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var serverStates = t.Result;
|
|
||||||
foreach (var pair in current.Zip(serverStates, (item, state) => new { item, state })) {
|
|
||||||
pair.item.HandleSave(pair.state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
return t;
|
|
||||||
}).Unwrap();
|
|
||||||
}).Unwrap().OnSuccess(t => (object)null);
|
|
||||||
}, cancellationToken);
|
|
||||||
});
|
|
||||||
}).Unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1091,10 +1079,8 @@ string propertyName
|
||||||
|
|
||||||
var value = estimatedData[key];
|
var value = estimatedData[key];
|
||||||
|
|
||||||
// A relation may be deserialized without a parent or key. Either way,
|
if (value is AVRelationBase) {
|
||||||
// make sure it's consistent.
|
var relation = value as AVRelationBase;
|
||||||
var relation = value as AVRelationBase;
|
|
||||||
if (relation != null) {
|
|
||||||
relation.EnsureParentAndKey(this, key);
|
relation.EnsureParentAndKey(this, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue