• GitHub下载最新消息

介绍

除了查询基于JSON / REST的远程服务的简洁方法之外,您通常还需要一种方法来缓存和索引您获取的数据。这对于Web服务尤为重要,因为它们的性质,以及连接服务的延迟+无状态,更不用说请求限制,它们往往会返回“大块”数据——即大块数据。在这种情况下,基于JSON的系统通常会在单个查询中返回多个嵌套的数据级联。正确有效地处理这个问题有几个复杂问题,其中一些我们将在本文中讨论。

了解混乱

我将展示REST调用URL以及在我们浏览时打印出来的结果。我们来看一下展示的数据吧。正如我所说,它很大。

以下是来自themoviedb.org的API的“Burn Notice”的电视节目信息:

“invoke:” https://api.themoviedb.org/3/tv/2919?api_key=c83a68923b7fe1d18733e8776bba59bb

{"id": 2919,"backdrop_path": "/lgTB0XOd4UFixecZgwWrsR69AxY.jpg","created_by": [{"id": 1233032,"credit_id": "525749f819c29531db09b231","name": "Matt Nix","gender": 2,"profile_path": "/qvfbD7kc7nU3RklhFZDx9owIyrY.jpg"}],"episode_run_time": [45],"first_air_date": "2007-06-28","genres": [{"id": 10759,"name": "Action & Adventure"},{"id": 18,"name": "Drama"}],"homepage": "http://usanetwork.com/burnnotice","in_production": false,"languages": ["en"],...

就这样继续下去。查看节点,例如created_by——它们是子对象。再往下(这里省略),有完整的数据!(如果单击我提供的链接,您将获得所有这些。)

这里要说的是你需要一种方法来存储这些数据并保留层次结构。它已经是JSON了, 所以如果你把它保存在JSON格式的某种表示中,那么你的工作就会更深入。

这可能看起来不那么糟糕,但是当它包含重叠数据时,这种大数据变得更加困难 例如,我可能已经执行了查询以获得上面的结果,然后我想获得TMDb关于created_by字段的人“Matt Nix”的所有信息。好吧,我可以这么做,但我不一定非得这么做,因为正如你所见,有些信息已经在里面了:

{"id": 1233032,"credit_id": "525749f819c29531db09b231","name": "Matt Nix","gender": 2,"profile_path": "/qvfbD7kc7nU3RklhFZDx9owIyrY.jpg"
}

这是一个相当多的信息。也许这就是我们需要的一切,也许不是。如果我想要他的IMDb ID怎么办?如果我想要他的生日怎么办?我们必须去服务器。所以我们提出另一个请求,使用上面的id

...

“invoke:”https://api.themoviedb.org/3/person/1233032?api_key=c83a68923b7fe1d18733e8776bba59bb

{"id": 1233032,"credit_id": "525749f819c29531db09b231","name": "Matt Nix","gender": 2,"profile_path": "/qvfbD7kc7nU3RklhFZDx9owIyrY.jpg","birthday": "1971-09-04","known_for_department": "Writing","also_known_as": [],"biography": "","popularity": 0.742,"adult": false,"imdb_id": "nm0633180"
}

这是相同的数据,只是其中包含更多数据。这是在线可查询JSON存储库的典型模式。这很好,只是,我们现在用它做什么?好吧,我们显然希望将它与我们在创建的字段中已有的数据合并,对吧?嗯,类似的东西,但没有。我们稍后会介绍。不管怎样,结果是一样的,但我们会很聪明的。但基本上,我们需要能够最终存储并查询这些信息,因此我们不必每次都返回服务器,并且我们需要一种智能地推迟到服务器的方法,直到我们真正需要我们想要的数据。换句话说,如果我们已经缓存了一些数据,我们不希望再次访问服务器,但如果我们不这样做,我们需要透明地获取它,并按需将其放入缓存中。

显然,第二个问题是远程和本地解决。我们怎么知道使用上面的“id”来运行第二个查询?我们如何确切地知道在本地存储库中存储信息的位置,以便我们以后可以快速获取?

所以基本上,我们必须面对存储和寻址的问题。我提出了一个简单的解决方案,使用JSON本身来完成大部分繁重的工作。

我将再一次重新审视我在这里发布的TMDb和Json代码库,其中的链接位于顶部。

编码这个混乱

存储此混乱

提到的第一个问题是存储,所以我们将从那里开始。我们用IDictionary<string,object>来保存我们的JSON {}对象。这样做的原因是我们有索引来加快查找速度。与此同时,我们必须为我们的值使用object,因为它们可以是映射到JSON的任意类型之一——JSON []数组的IList<object>,映射到JSON的数值类型,当然还有string,bool和null。

我们使用包含雄心勃勃命名为“Json”的库来将JSON文本转换为此文本并返回,并支持对其进行查询。我们还将使用它来执行远程RPC / REST调用(在本例中为TMDb)。所有那些繁重的小包装。如果你愿意,你可以用NewtonSoft的产品或其他东西转换它,但要小心,因为我已经围绕字典类编排了这个。如果第三方不使用这些,你的工作就会变得更加困难。

首先,我们需要一个字典来查找我们的所有数据的根。我们所有的实体都会在这个下面的某个地方使用词典。除非你想为你的实体搞乱一个构造函数,并且代码很复杂,否则你需要保持一些静态状态。因此,每个实体都需要知道如何找到根,我们使用的机制涉及保持静态。这不是线程安全的,所以我们所做的就是使用static ThreadLocal<IDictionary<string,object>> 保存我们的数据。这意味着数据基于每个线程。这样做有好处,比如不需要锁定,并增加了简单性,以及缓存未命中(我们会达到此目的)和增加多线程应用程序中的内存使用量等缺点,因为您必须为每个线程保留一个数据存储。如果您使用某种二级缓存机制(如此这个小Json 库),那么前面的缓存丢失问题就会得到缓解。后一个内存使用问题在ASP.NET Web服务器环境中得到缓解,因为页面运行时很短并且保持活动连接往往在请求相同的线程请求上提供,这意味着您通常可以从以前的请求访问缓存,而不是在每个请求上创建一个新的缓存。

public static class Tmdb
{const string _apiUrlBase = "https://api.themoviedb.org/3";static ThreadLocal<IDictionary<string, object>> _json=new ThreadLocal<IDictionary<string, object>>(()=>new JsonObject());public static IDictionary<string,object> Json { get { return _json.Value; } }public static string ApiKey { get; set; }public static string Language { get; set; }
}

上面的JsonObject只是我们的“Json”库提供的一个薄包装器,它包装了一个Dictionary<string,object> 类。它并没有什么特别之处,尽管它具有标准字典不共享的特性,如值语义。我们不在这里使用它们。你可以把它变成你想要的Dictionary<string,object>。

你会注意到除了这个static ThreadLocal<IDictionary<string,object> _json 字段,我们还有其他一些字段。原因是TMDb服务与大多数服务一样,需要“API密钥”。您必须在每次调用服务时提供此功能,以便您可以在应用程序级别进行设置。它不会被用户改变。Language是TMDb全局接受的另一个参数,在这种情况下不会因用户而异,但如果您正在制作多语言Web应用程序,则需要确保_json结构也考虑到每种语言的基础。例如,我可以在根目录下放置更多的dictionary/JSON对象,这样就可以在store/cache的根目录下使用一种语言,比如“$.en US.movie.219”aka“/en US/movie/219”(所有内容都在其I so 639.1代码或其他代码下)。别担心这里。所提出的解决方案足够灵活,可以容纳它,只需要预先计划。

请注意,我们在此对象上有一个Json属性,用于检索我们的根字典。我们所有的实体也将如此。每个人都指向JSON以获取自己的数据。根类是静态的,并且包含所有内容——所有其他只是字典的对象存在于整个图/树中的某个位置。

这使得寻址更简单。

解决这个混乱

每当你保存数据时,你必须有办法再次恢复它。文件具有文件名和路径,关系数据库具有主键。我们有什么?我们将索引器放入对象中,因为它们都是字典和列表。

...
object o;
// get the "created_by" field from the show's JSON
if (showData.TryGetValue("created_by", out o))
{// make sure it's a list.var l = o as IList<object>;if (null != l){if(0 < l.Count){// get the dictionary for the personvar personData = l[0] as IDictionary<string, object>;if(null != personData){// now get their name and write itstring name=null;if (personData.TryGetValue("name", out o))name = o as string;Console.WriteLine(name);}}                }
}

这是使用索引字段以有效方式在树中移动的方法。然而,这对于手动来说并不容易。它需要一个包装器。再次拯救“Json”库:

// basically works like this should: showData["created_by"][0]["name"]
Console.WriteLine(JsonObject.Get(showData, "created_by", 0,"name") as string);

这将得到完全相同的东西。

或者,您可以使用更熟悉但效率更低的Select()机制来运行JSON路径查询

Console.WriteLine(JsonObject.Select(showData, "$.created_by[0].name").First() as string);

所有路径都通向“Matt Nix”(在这种情况下)

好。使用上面的某种形式,我们可以回到我们的元素。我更喜欢使用第二种机制,因为最后,使用Get()实际上是最简单和最适合的,并且效率很高。

现在,就像关系数据库中的一行有一个id,一个文件有它的路径和名称,我们需要一些东西来唯一地识别我们的对象,并且这样做的方法让我们再次找到它们——再次寻址,但我们需要以某种方式存储我们的地址。我们不能存储我们上面写的内容。

取而代之的是,我们最终会保持我们的对象“identity”保存为string[]数组中的路径段,以便于将Get()或string.Join()传递到'.'上的JSON路径或使用'/'给我们一个路径——这是我们将与TMDb一起使用的一个技巧,使本地数据与远程终结点同步变得更加容易。

因为TMDb也使用路径来公开其数据,所以我们只需在本地存储中使用几乎相同的路径。例如,电视节目“Burn Notice”(TMDb id为2919)位于/tv/2919,电影“Volver”(TMDb 219)位于/movie/219

实际上,从TMDb的根目录获取名称为“Matt Nix”的JSON路径(或者更确切地说,其中之一)是“ $.tv.2919.created_by[0].name”

而节目本身的根源是:

"<code>$.tv.2919</code>"

或者,使用我们的方法, new string[] { "tv","2919" }

JsonObject.Get(Tmdb.Json,"tv",2919");

我们将这个字符串数组称为我们的路径PathIdentity ,许多实体将有一个,但我们稍后会介绍。请记住,上面的节目是“Burn Notice”。我们继续吧。

我们将使用CreatePath()而不是Get() 获取对象,因为如果我们没有它们,我们想要创建它们。所以在这种情况下,把它放在一起——这是实体在创建时基本上做的事情。

this.Json = JsonObject.CreatePath(Tmdb.Json,this.PathIdentity);

上面的代码不仅创建了这个对象和任何指向它的对象,还为Tmdb.Json中的一个节点分配了自己的存储指针,我们将讨论这个问题。现在重要的是,PathIdentity有助于在本地和远程定位我们的对象。

以下是节目的远程数据库的实际URL:

https://api.themoviedb.org/3 / tv / 2919?api_key = c83a68923b7fe1d18733e8776bba59bb

我已经包含了API密钥,因此您可以对其进行测试。所有API的根目录是https://api.themoviedb.org/3。

使用PathIdentity定位对象

现在,当我们获取数据时,基本上我们将把它放在Tmdb.Json与PathIdentity对象所指示的相同的“地址”下。在这种情况下,对于“Burn Notice”,它的地址是/tv/2919。

我们可以简单地通过调用JsonObject.CreatePath(Tmdb.Json,show.PathIdentity)在根目录中“创建地址”, JsonObject.CreatePath(Tmdb.Json,show.PathIdentity)也将返回它刚创建的最内层节点。这基本上只是创建了路径/tv/2919("$.tv.2919"),因为这就是节目从PathIdentity上面返回的内容。如上所述,这也将返回我们的节点2919。请记住,这种方法不具有破坏性——如果路径已经存在,它只是导航它——它不会破坏任何东西。

在内部,我们基本上只是创建彼此嵌套的字典。

请记住,这个PathIdentity也是远程服务器上的地址。如果不是,我们必须有一个额外的属性,但这使它变得非常简单。还记得上面“Burn Notice”(2919)的URL如何在其中包含此路径标识?是啊。你可以看到它的发展方向。有了这个PathIdentity,我们知道足以获取我们还没有的数据。

此外,并非所有实体都可以拥有PathIdentity。请记住,该服务返回深层嵌套数据,因此某些数据仅作为父查询的一部分提供。这些数据没有自己的路径。它不从服务器获取自己。它基本上代表其父节点的一部分。

把这些乱七八糟的东西放在一起:JSON支持的实体基类

首先,最基本的类:

// Represents a basic entity in the TmdbApi library
// This object uses a custom form of reference semantics
// for equality comparison - it's Json property is compared.
public abstract class TmdbEntity : IEquatable<TmdbEntity>
{protected TmdbEntity(IDictionary<string, object> json){Json = json ?? throw new ArgumentNullException(nameof(json));}public IDictionary<string, object> Json { get; protected set; }protected T GetField<T>(string name,T @default=default(T)){object o;if (Json.TryGetValue(name, out o) && o is T)return (T)o;return @default;}// objects are considered equal if they// point to the same actual json referencepublic bool Equals(TmdbEntity rhs){if (ReferenceEquals(this, rhs))return true;if (ReferenceEquals(rhs, null))return false;return ReferenceEquals(Json, rhs.Json);}public override bool Equals(object obj){return Equals(obj as TmdbEntity);}public static bool operator==(TmdbEntity lhs, TmdbEntity rhs){if (object.ReferenceEquals(lhs, rhs)) return true;if (object.ReferenceEquals(lhs, null)) return false;return lhs.Equals(rhs);}public static bool operator!=(TmdbEntity lhs, TmdbEntity rhs){if (object.ReferenceEquals(lhs, rhs)) return false;if (object.ReferenceEquals(lhs, null)) return true;return !lhs.Equals(rhs);}public override int GetHashCode(){var jo = Json as JsonObject; // should always be but it doesn't *have* to beif(null!=jo){// we don't want our wrapper's hashcode since// JsonObject implements value semantics// So get the "real" dictionary and // GetHashCode() on that. return jo.BaseDictionary.GetHashCode();}return Json.GetHashCode();}
}

从上到下:

我们得到的第一个是构造函数,它接受一个JSON对象(JsonObject或IDictionary<string,object>)

我们的类将使用这些信息来填补其字段。这基本上是该类的初始JSON。有时,这样的JSON将包含一个单独的字段——只有足够的信息来从服务器中提取剩余的信息。经常

{ "id": 2919 }

或类似的。以上指向“Burn Notice”的ID,但它需要服务器调用才能检索其他任何内容。

我们接下来要做的就是必需的Json属性。这只是保持/返回JsonObject,其保持你的状态。

第三件事是采用名称和可选的默认值的GetField<T>()。它所做的只是尝试将值作为指定的类型返回,如果不能,则返回指定的默认值。这只是派生类的辅助方法。它本身并不重要,但它有一个大兄弟,GetCachedField<T>()在派生类中做得更多,所以使用GetField<T>()和GetCachedField<T>()串联只会使事情更加一致。

其余部分涉及实现我们的等式比较语义。基本上,我们想要的是两个对象是相同的,如果它们各自的Json属性引用相同的字典——相同的内存位置。这是一种一次性要求,因此在.NET中实现它有点奇怪——它通常是默认行为,但我们不希望我们的实体与引用相等进行比较——我们希望我们所持有的JSON对象Json 是这个的仲裁者。这样,如果它们都指向内存中Tmdb.Json根目录下的相同位置,则我们的对象被认为是相等的。这是我们如何做到这一点的一部分。上面提到了另一个步骤,但我们还没有探讨它。我们将来会。

GetHashCode()中的怪异是必要的,因为我们不想在这个类中使用值语义,所以我们要重写JsonObject的行为,但是没有直接的方法可以从类外部做到这一点。此外,对象不必是 JsonObject,它可以是任何字典,因此我们必须接受其中一个并检查它。

public class TmdbImage : TmdbEntity
{public TmdbImage(IDictionary<string,object> json) : base(json) {}public int Width => GetField("width",0);public int Height => GetField("height", 0);public double AspectRatio => GetField("aspect_ratio", 0);public string Path => GetField<string>("file_path");public string Language => GetField<string>("iso_639_1");public double VoteAverage => GetField("vote_average",0d);public int VoteCount => GetField("vote_count", 0);public TmdbImageType ImageType {get {switch(GetField<string>("image_type")){case "poster":return TmdbImageType.Poster;case "backdrop":return TmdbImageType.Backdrop;case "logo":return TmdbImageType.Logo;}return TmdbImageType.Unknown;}}// only present for logo imagespublic string FileType => GetField<string>("file_type");
}

这支持一个看起来基本上像这个例子的JSON对象:

(这个没有我可以给你的URL,因为它们只作为子查询的一部分返回)

{"aspect_ratio": 0.666666666666667,"file_path": "/lYqC8Amj4owX05xQg5Yo7uUHgah.jpg","height": 3000,"iso_639_1": null,"vote_average": 0,"vote_count": 0,"width": 2000
}

派生类中的代码足够常规,可以生成,或者可以使用属性和反射来使其更加自动化。

注意:您可能想知道我们为什么不使用expando对象或其他自动包装工具。我已经考虑过了,但是当一个字段不存在时你必须知道如何要求加载——这本身可以通过将事件连接到底层字典类的访问器来解决,但你还必须知道如何获得自己的来自代表您的JSON的远程和本地地址,这并不容易,因为JSON没有架构信息。你的密钥是什么字段?你如何建立他们的路径?您可以使用JSON模式来提供此功能,但是您必须声明一个模式,这就像声明一个包装器一样艰巨,而代码使它实际上做起来比你想要的更复杂。所有路径都导致编码整个API,这样或那样——或者至少是你需要的字段。无论您是将“代码”编写为JSON模式,还是我们这样做的方式,都是如此。这样做是最简单的方法,可以立即解决所有上述问题。在任何情况下,如果你想要它,所有实体上的Json属性都已通过c#中的“dynamic”支持expando访问,这是因为JsonObject的工作方式。JSON字段成为您期望的对象的访问器属性。

另一种选择是根本不使用实体,只是以未经处理的形式使用JSON,但这带来了一些不利因素,但有一些令人信服的好处。即使您放弃了实体,树/图的根和路径标识概念也很有用,但是您必须再次提出另一种需求加载和寻址机制,我们将要介绍这些机制。考虑派生类,TmdbCachedEntity:

public abstract class TmdbCachedEntity : TmdbEntity
{protected TmdbCachedEntity(IDictionary<string, object> json) : base(json){}public abstract string[] PathIdentity { get; }// overload this in a derived class and when called, get your JSON from the remote source.// if you don't do it, it will be done for you using PathIdentityprotected virtual void Fetch(){// in case you forget to override and the// API doesn't accept a language argument // all this does is send an extra parameterFetchJsonLang();}// helper method to fetch remote data from a TMDb path and merge it with our data.protected void FetchJson(string path = null, Func<object, object> fixupResponse = null, Func<object, object> fixupError = null){var json = Tmdb.Invoke(path ?? string.Join("/", PathIdentity), null, null, fixupResponse, fixupError);JsonObject.CopyTo(json, Json);}// helper method to fetch remote data from a TMDb path and merge it with our data.// sends the current languageprotected void FetchJsonLang(string path = null, Func<object, object> fixupResponse = null, Func<object, object> fixupError = null){var json = Tmdb.InvokeLang(path ?? string.Join("/", PathIdentity), null, null, fixupResponse, fixupError);JsonObject.CopyTo(json, Json);}// demand loads if a field is not present.protected T GetCachedField<T>(string name, T @default = default(T)){object o;if (Json.TryGetValue(name, out o) && o is T)return (T)o;Fetch();if (Json.TryGetValue(name, out o) && o is T)return (T)o;return @default;}// Call this method in your entity's constructor to root it in // the in memory cache. This is important.protected void InitializeCache(){var path = PathIdentity;if (null != path){var json = JsonObject.CreatePath(Tmdb.Json, path);JsonObject.CopyTo(Json, json);Json = json;} elsethrow new Exception("Error in entity implementation. PathIdentity was not set.");}
}

从上到下:

首先,我们有构造函数重载,它将我们的JSON数据传递给基类。在你派生的大多数构造函数中,你应该调用我们将要覆盖的InitializeCache()内容,但它只能在PathIdentity创建之后完成。

这将我们带到PathIdentity——我们必须在派生类中创建它,以便我们的对象可以定位自己。我们之前进行了探讨。

接下来我们有告诉派生类我们要从服务器获取的方法Fetch()。通常,基类可以处理这个问题,但您可能希望重载它。这就是为什么有FetchXXXX()辅助方法,我们即将到达。

我们有一个帮助器FetchJson(),它使用PathIdentity远程端点进行REST“RPC调用” ,我们有FetchJsonLang(),它与此完全相同,只是它将&language参数与查询字符串一起发送。根据需要,每个调用本身都被委托给Tmdb.Invoke()或Tmdb.InvokeLang() 。这是因为有些调用接受语言,而其他调用则不接受语言。如果不接受语言参数,您可以安全地发送语言参数,但最好不要这样做。这两个例程都委托给JsonRpc.Invoke(),但是处理TMDb服务返回的连接速率限制和专门错误。

接下来我们有GetCachedField<T>(),它将获取字段名称和可选的默认值并返回该字段。如果该字段不存在,或者没有正确的类型(可能是本地存储被更改?),则通过调用Fetch() 从服务器获取,然后再次尝试获取该值,如果获取没有得到任何内容,则最终返回null。这是处理它的一种理想的方式,因为它可以在值永远不存在时导致永久提取,但是由于 JsonRpc.Invoke()支持二级缓存,您可以使用它来缓解问题——这不一定是一个大问题的开始。这就是为什么没有更复杂的空处理方案(例如插入DBNull字段或其他东西)。不管怎样,从最终使用的角度来看,这与GetField<T>()的工作原理是一样的,只是很明显,如果必须获取,它可能会延迟。

最后,我们有InitializeCache(),其任务是在Tmdb.Json下的某个地方“根化”我们的对象。

它执行以下步骤:

  1. 创建或导航到JSON中的指定路径。这样就可以创建我们在路径中创建的最终节点。我们在这里传递PathIdentity,它在Tmdb.Json下的指定的路径上创建一个新的IDictionary<string,object>/ JsonObject。
  2. 将我们持有的任何当前状态复制到我们刚创建的新节点或我们刚刚导航到的节点(来自#1)
  3. 用我们创建的或从步骤#1导航到的“指针”(引用)替换我们自己状态的指针。

最后一步是魔术,因为它不仅使我们在树中,而且它允许我们回收分支,因此我们不会复制(尽可能多)状态。更重要的是,这样的回收分支通过此过程合并在一起,因此无论您收到的重叠数据中存在多少逻辑副本,您总是有一个地方可以获得任何缓存项目的最完整状态。它的工作方式类似于Linux或Windows文件系统中的符号链接。另一种看待它的方法是你在某个根树中“挂载”你自己的状态。像POSIX文件系统一样。考虑它的另一种方法是将树转换为图形,因为一个节点可以有多个父节点。这是一个简单的技巧,与之相关的一些重大胜利。

让我们看一下TmdbCachedEntity 与复杂(多部分)键的精简派生,以及进行二次提取的能力(使用除主方法之外的其他提取方法来提取相关数据)。

// represents a TV episode
public sealed class TmdbEpisode : TmdbCachedEntity
{public TmdbEpisode(int showId, int seasonNumber,int episodeNumber) : base(_CreateJson(showId, seasonNumber,episodeNumber)){InitializeCache();}public TmdbEpisode(IDictionary<string, object> json) : base(json){InitializeCache();}static IDictionary<string, object> _CreateJson(int showId, int seasonNumber, int episodeNumber){var result = new JsonObject();// add our "key fields" to the jsonresult.Add("show_id", showId);result.Add("season_number", seasonNumber);result.Add("episode_number", episodeNumber);return result;}// our path needs to look like this:// /tv/{show_id}/season/{season_number}/episode/{episode_number}public override string[] PathIdentity=> new string[] {"tv",GetField("show_id", -1).ToString(),"season",GetField("season_number", -1).ToString(),"episode",GetField("episode_number", -1).ToString(),};public TmdbShow Show {get {int showId = GetField("show_id", -1);if (-1 < showId)return new TmdbShow(showId);return null;}}public TmdbSeason Season {get {int showId = GetField("show_id", -1);if (-1 < showId){int seasonNum = GetField("season_number", -1);if (-1 < seasonNum)return new TmdbSeason(showId,seasonNum);}return null;}}public int Number => GetField("episode_number", -1);public string Name => GetCachedField<string>("name");public DateTime AirDate => Tmdb.DateToDateTime(GetCachedField<string>("air_date"));public TmdbCrewMember[] Crew=> JsonArray.ToArray(GetCachedField<IList<object>>("crew"),(d)=>new TmdbCrewMember((IDictionary<string,object>)d));public TmdbCastMember[] GuestStars=> JsonArray.ToArray(GetCachedField<IList<object>>("guest_stars"),(d) => new TmdbCastMember((IDictionary<string, object>)d));public string ImdbId {get {_EnsureFetchedExternalIds();var d = GetField<IDictionary<string, object>>("external_ids");if (null != d){object o;if (d.TryGetValue("imdb_id", out o))return o as string;}return null;}}public string TvdbId {get {_EnsureFetchedExternalIds();var d = GetField<IDictionary<string, object>>("external_ids");if (null != d){object o;if (d.TryGetValue("tvdb_id", out o))return o as string;}return null;}}// TODO: figure out what this means and make an enum possiblypublic string ProductionCode => GetCachedField<string>("production_code");public string StillPath => GetCachedField<string>("still_path");public TmdbCastMember[] Cast {get {_EnsureFetchedCredits();var credits = GetField("credits", (IDictionary<string, object>)null);if (null != credits){object o;if (credits.TryGetValue("cast", out o)){var l = o as IList<object>;return JsonArray.ToArray(l, (d) => new TmdbCastMember((IDictionary<string, object>)d));}}return null;}}void _EnsureFetchedCredits(){var credits = GetField<IList<object>>("credits");if (null != credits) return;var json = Tmdb.Invoke(string.Concat("/", string.Join("/", PathIdentity), "/credits"));if (null != json)Json["credits"] = json;}void _EnsureFetchedExternalIds(){var l = GetField<IList<object>>("external_ids");if (null == l){var json = Tmdb.InvokeLang(string.Concat("/", string.Join("/", PathIdentity), "/external_ids"));if (null != json)Json.Add("external_ids", json);}}...
}

好吧,诚然,甚至配对了一点,这是很多东西。我们将从顶部开始,并且通常从上到下,但有些跳跃可能是为了使这一点更清楚。

首先,我们有一个熟悉的构造函数,它从一些JSON数据初始化。一个值得注意的区别是它的调用InitializeCache(),我们在上面进行了调查。这会将对象置于缓存中,并且对于直接缓存的所有对象来说,在构造函数中调用此方法非常重要。这将我们的Json属性设置到正确的位置,并确保我们拥有所需的所有可用数据。

我们有第二个构造函数,它带有几个整数,一个show id,一个 season number和一个episode number。

如果这是在关系数据库中,则这三个项将包含主键。在这个范例中,这些是此对象从远程存储中获取其余部分所需的最少信息量。

如果我们运行以下代码,初始JSON将如下所示:

// burn notice pilot episode
var episode = new TmdbEpisode(2919, 1, 1);
Console.WriteLine(episode.Json);
{"show_id": 2919,"season_number": 1,"episode_number": 1
}

这组成一个/tv/2919/season/1/episode/1的PathIdentity

对于最终请求的URL:

https://api.themoviedb.org/3/tv/2919/season/1/episode/1?api_key=c83a68923b7fe1d18733e8776bba59bb

这是用于满足他们对更多剧集数据的请求的URL FetchJson()和FetchJsonLang()——因为我们的路径标识就是这样。Fetch()将通过GetCachedField<T>()自动处理。

请注意,我们调用GetField<T>()而不是GetCachedField<T>()构成路径标识的这些值,例如Number。您无法从远程源获取标识字段,因为您需要它们来完成提取,但是如何解析这是一个堆栈溢出,所以不要这样做。始终为这些字段使用GetField<T>()。我们永远不会使用缓存的版本来检索show_id,season_number或episode_number

关于命名的一个小注释:Number和索引是不一样的。航空订单可以不同,季节可以有指数为零的特价,但可能没有,因此季节指数可能与季节数字不匹配。因此,季节也有一个Number属性。

注意在Show和Season属性中,我们只是创建相关类的实例并将其传递给id(s)。由于方法InitializeCache() 有效,show和season对象可以将自己定位在本地存储/缓存中,这意味着它们可以立即访问已存在的任何数据。在大多数情况下,当您检索到这一集时,您已经检索到了节目和季节,因此这些值通常被缓存,因此即使这些包装只有它们可能被指向的id,并在实例化后与其余数据合并。如果他们没有完整的设置,任何时候要求提取的内容都没有被提取,则会进行提取,然后获取剩余部分,并自动将其复制回商店。

接下来的三个字段很无聊。它们只是直接包装底层JSON,但请注意如何使用GetField<T>()vs GetCachedField<T>(); Number是我们PathIdentity的一部分,所以我们绝不能尝试取得它。最后,该AirDate字段以“yyyy-MM-dd”格式获取字符串,并使用辅助方法将其转换为DateTime。

接下来的事情变得有趣

public TmdbCrewMember[] Crew=> JsonArray.ToArray(GetCachedField<IList<object>>("crew"),(d)=>new TmdbCrewMember((IDictionary<string,object>)d));

这将在字段“crew”处获取一个JSON数组,然后将其传递给JsonArray.ToArray<T>()(给它两个参数):

第一个是我们刚从GetCachedField<... >("crew")收到的JSON数组,第二个是lambda表达式,包括获取和返回System.Object。基本上它需要一个数据对象,并从中创建一个类型为T的对象。每个对象都来自JSON数组,因此它可以是字典,列表或某些标量JSON值。请记住,我们的每个实体都接受一个IDictionary<string,object>构造函数参数?好吧,这里我们将JSON数组的每个元素传递给构造函数,TmdbCrewMember构造函数创建该类型的实例以传递以填充目标数组元素。

下一个属性GuestStars做同样的事情,但是TmdbCastMember来自“guest_stars”。

现在我们得到ImbdId属性:

public string ImdbId {get {_EnsureFetchedExternalIds();var d = GetField<IDictionary<string, object>>("external_ids");if (null != d){object o;if (d.TryGetValue("imdb_id", out o))return o as string;}return null;}
}

首先要注意的是它的调用_EnsureFetchedExternalIds()是因为这些数据(如果它尚未存在)必须作为对TMDb的单独调用来检索:

https://api.themoviedb.org/3/tv/2919/season/1/episode/1/external_ids?api_key=c83a68923b7fe1d18733e8776bba59bb

{"id": 223655,"imdb_id": null,"freebase_mid": "/m/02vxx4g","freebase_id": null,"tvdb_id": 330913,"tvrage_id": 574476
}

然后将结果存储在剧集的JSON 的“external_ids”字段中。

这是通过以下例程完成的:

void _EnsureFetchedExternalIds()
{var l = GetField<IList<object>>("external_ids");if (null == l){var json = Tmdb.InvokeLang(string.Concat("/", string.Join("/", PathIdentity), "/external_ids"));if (null != json)Json.Add("external_ids", json);}
}

它只返回字段或以其他方式从URL中提取它。注意它对我们的PathIdentity做了什么:它在它之前加上“/”,然后在“/”上加入它,然后添加“external_ids”作为后缀。然后它调用委托Tmdb.Invoke()并将结果存储在“external_ids”字段下的json中。请注意,这使我们本地存储的虚拟路径与远程服务器的实际路径相同。再一次,我们在这里利用了TMDb API寻址的简单性。

无论如何,在确保获取数据之后,ImdbId导航JSON(我在这里手动完成,因为这是旧代码)并返回imdb_id字段的结果。

TvdbId属性和相应tvdb_id字段也会发生同样的事情。

接下来的属性ProductionCode以及StillPath,其分别获得production_code和still_path字段。

现在我们在Cast——我们的一个属性返回一个数组,但是这个也使用一个单独的查询来获取它的数据:

public TmdbCastMember[] Cast {get {_EnsureFetchedCredits();var credits = GetField("credits", (IDictionary<string, object>)null);if (null != credits){object o;if (credits.TryGetValue("cast", out o)){var l = o as IList<object>;return JsonArray.ToArray(l, (d) => new TmdbCastMember((IDictionary<string, object>)d));}}return null;}
}

单独的查询由一个单独的例程处理,具有以下URL:

https://api.themoviedb.org/3/tv/2919/season/1/episode/1/credits?api_key=c83a68923b7fe1d18733e8776bba59bb

这给我们这些数据:

{"cast": [{"character": "Madeline Westen","credit_id": "525749f519c29531db09b018","gender": 1,"id": 73177,"name": "Sharon Gless","order": 2,"profile_path": "/ul7dTg6MxIU72inhxXiMWEJH8MP.jpg"},{"character": "Michael Westen","credit_id": "525749f519c29531db09b04c","gender": 2,"id": 52886,"name": "Jeffrey Donovan","order": 0,"profile_path": "/5i47zZDpnAjLBtQdlqhg5AIYCuT.jpg"},{"character": "Fiona Glenanne","credit_id": "525749f519c29531db09afe4","gender": 1,"id": 5503,"name": "Gabrielle Anwar","order": 1,"profile_path": "/khnEDczzSy6UcbnqZ6Sb4lWxnkE.jpg"},{"character": "Sam Axe","credit_id": "525749f519c29531db09b080","gender": 2,"id": 11357,"name": "Bruce Campbell","order": 3,"profile_path": "/hZ2fW0gpPIBvXxT5suJzaPZQCz.jpg"}],"crew": [{"id": 20833,"credit_id": "525749d019c29531db098a72","name": "Jace Alexander","department": "Directing","job": "Director","profile_path": "/nkmQTpXAvsDjA9rt0hxtr1VnByF.jpg"},{"id": 1233032,"credit_id": "525749d019c29531db098a46","name": "Matt Nix","department": "Writing","job": "Writer","profile_path": null}],"guest_stars": [{"id": 6719,"name": "Ray Wise","credit_id": "525749cc19c29531db098912","character": "","order": 0,"profile_path": "/z1EXC8gYfFddC010e9YK5kI5NKC.jpg"},{"id": 92866,"name": "China Chow","credit_id": "525749cc19c29531db098942","character": "","order": 1,"profile_path": "/kUsfftCYQ7PoFL74wUNwwhPgxYK.jpg"},{"id": 17194,"name": "Chance Kelly","credit_id": "525749cc19c29531db09896c","character": "","order": 2,"profile_path": "/hUfIviyweiBZk4JKoCIKyuo6HGH.jpg"},{"id": 95796,"name": "Dan Martin","credit_id": "525749cd19c29531db098996","character": "","order": 3,"profile_path": "/u24mFuqwEE7kguXK32SS1UzIQzJ.jpg"},{"id": 173269,"name": "Dimitri Diatchenko","credit_id": "525749cd19c29531db0989c0","character": "","order": 4,"profile_path": "/vPScVMpccnmNQSsvYhdwGcReblD.jpg"},{"id": 22821,"name": "David Zayas","credit_id": "525749cd19c29531db0989ea","character": "","order": 5,"profile_path": "/eglTZ63x2lu9I2LiDmeyPxhgwc8.jpg"},{"id": 1233031,"name": "Nick Simmons","credit_id": "525749cf19c29531db098a17","character": "","order": 6,"profile_path": "/xsc2u2QQA6Nu7SvUYUPKFlGl9fw.jpg"}],"id": 223655
}

如你所见,它有一个crew和一个cast字段。我们把所有的东西都存储在credits 下,它再次将我们的继承权与远程存储库同步,因为我们的路径匹配。正如我上面提到的,如果您的远程存储无法以这种方式进行镜像,则您必须同时拥有实体的远程和本地标识。我们可以通过利用TMDb API的布局来避免这种情况——它很容易镜像我们正在做的部分,但我们必须确保我们的项目的本地和远程路径始终匹配。

无论如何,这是常规,它的工作方式几乎与我们遇到的最后一个一样,只是稍微简单一些:

void _EnsureFetchedCredits()
{var credits = GetField<IList<object>>("credits");if (null != credits) return;var json = Tmdb.Invoke(string.Concat("/", string.Join("/", PathIdentity), "/credits"));if (null != json)Json["credits"] = json;
}

了解Json布局

实际上,我们一直在本地镜像远程存储库中的地址,如果数据尚未存在则按需获取。然而,也可能不那么清楚的是,我们也一直在回收分支。

也就是说,您可以通过查询JSON数据从多种方式获取“Matt Nix”,但我们通过将所有这些分支固定到指向他的/person/1233032 “人员”节点上来最小化重复分支的数量。这是由于缓存实体在InitializeCache() 找到节点后修复其缓存的方式,因此会重复使用。这使得我们的数据存储和检索效率更高,因为我们最大限度地提高了缓存命中率并同时最大限度地减少了重复。

因此,我们的树不再是树了,它是一个图形,因为在一个节点中可以有一个以上的父节点,而不像树。

您必须非常小心地执行此操作,否则您将创建无休止的递归图形,这些图形在尝试序列化JSON时将失败。幸运的是,因为我们使用路径并且只使用我们的缓存实体进行分支回收,所以永远不会发生这种情况。另请注意,您无法通过序列化JSON来“查看”分支回收。随着JSON的编写,任何回收的分支都将在每个位置写出。这不是最好的事情,也不是我们想要的,但它不是一个节目的终结者。

就实体本身而言,你可以从这里拿出它。虽然它有助于玩弄它并修补代码。

兴趣点

在整个代码中,我们一直在调用变量Tmdb.Invoke()来处理发送实际的JSON / REST调用。它的作用是将API密钥附加到查询字符串,其Lang 变体也将语言附加到查询字符串。这些函数及其变体处理查询API的各个方面,如使用页面参数调用,以及在超出限制时处理请求限制。他们都调用给_Invoke(),使用JsonRpc.Invoke()建立实际的调用

static object _Invoke(string path, bool sendLang, IDictionary<string, object> args, IDictionary<string, object> payload, Func<object, object> fixupResult, Func<object, object> fixupError, string httpMethod)
{var url = _apiUrlBase;if (!path.StartsWith("/"))url = string.Concat(url, "/");url = string.Concat(url, path);if (null == args)args = new JsonObject();args["api_key"] = ApiKey;if (sendLang && !string.IsNullOrEmpty(Language))args["language"] = Language;object result = null;var retryCount = 0;while (null == result){++retryCount;try{var s = JsonRpc.GetInvocationUrl(url, args);System.Diagnostics.Debug.WriteLine("Requesting from " + s);result = JsonRpc.Invoke(s, null/*we already computed the url*/, payload, null, httpMethod, fixupResult, fixupError, Tmdb.CacheLevel);if (null == result)break;}catch (JsonRpcException rex){if (retryCount > 11){rex.Json.Add("retry_count_exceeded:", retryCount - 1);throw;}// are we over the request limit?if (25 == rex.ErrorCode){System.Diagnostics.Debug.WriteLine(rex.Message + ".. throttling " + url);// wait and try againThread.Sleep(RequestThrottleDelay);}else if (-39 == rex.ErrorCode)continue;//malformed or empty json, try againelsethrow;}}return result;
}

这就是web不可靠的本质,我们必须处理无效响应和限制之类的事情,所以上面的这个例程为我们处理所有这些。有时,这意味着在最坏的情况下会出现严重的延迟,但对于多级缓存,这不是什么大问题。

在Web服务器环境中使用此混乱

所以这很有趣:显然,Keep-Alive连接可以由请求的相同线程请求提供。这并不总是正确的,但由于我们的每线程Tmdb.Json实例的工作方式,它对我们有益。基本上,我们被允许将其保持为(不可靠)连接状态。这被称为“邪恶的黑客”,但它实际上只是一个间接的优化。这意味着页面到页面,我们将继续获得由此API支持的单个用户会话的缓存命中。这非常好,因为这意味着我们创建的JSON实例更少,以满足用户的请求,并且我们更少地访问远程存储库。如果做不到这一点,我们会严重依赖次要的“每个网址”缓存。在web服务器环境中,最好将Tmdb.CacheLevel设置为JsonRpcCacheLevel.Aggressive以使其有效。

使用基于JSON的实体在C#中缓存远程数据相关推荐

  1. php在数据流(内存)中操纵远程数据

    html5上传图片时可用php://input的数据流来运作. 例如: 1 if($in = fopen('php://input', "rb")) 2 while($buff = ...

  2. 清空memcached中缓存的数据的方法

    第一.连接:telnet 127.0.0.1 11211  第二.按回车键  第三.flush_all 后回车  控制台显示OK,表示操作成功 说明:  1.清空所有键值  flush_all  注: ...

  3. alibaba 实体转json_JAVA中使用alibaba fastjson实现JSONObject、Object、Json字符串的转换...

    Object转JSON字符串: String jsonStr = JSONObject.toJSONString(object); JSON字符串转JSONObject: JSONObject jso ...

  4. adf.test_在ADF 12.2.1.3中使用基于JSON的REST Web服务

    adf.test 以前,我曾发布过有关在ADF中使用基于ADF BC的REST Web服务的信息. 现在,本文讨论使用通用数据控件使用基于JSON的REST Web服务. 您还可以查看有关Web服务的 ...

  5. 在ADF 12.2.1.3中使用基于JSON的REST Web服务

    以前,我曾发布过有关在ADF中使用基于ADF BC的REST Web服务的信息. 现在,本文讨论使用通用数据控件使用基于JSON的REST Web服务. 您也可以查看有关Web服务的先前文章,以获取更 ...

  6. idea中json转实体类

    之前没觉得写个json的实体类有多麻烦,直到- 然后我就不敢再手写了- 这还只是一部分,所以以后千万别做这种无用功,效率是王道. 正文: 我们需要在idea中安装GsonFormat插件,上图 在这里 ...

  7. 《游戏AI开发指南(基于Lua的人工智能在游戏中的应用)》(Yanlz+Unity+SteamVR+5G+AI+VR云游戏+Lua+人机交互+沙箱+导航+决策树+影响力地图+立钻哥哥+==)

    <游戏AI开发指南(基于Lua的人工智能在游戏中的应用)> <游戏AI开发指南(基于Lua的人工智能在游戏中的应用)> 版本 作者 参与者 完成日期 备注 YanlzAI_Lu ...

  8. xml转json和实体类的两种方式

    本文为博主原创,未经允许不得转载: xml在http通信中具有较高的安全性和传输速度,所以应用比较广泛, 在项目中往往需要对xml,json和实体类进行相互转换,在这里总结一下自己所用到的一些方法: ...

  9. 基于JSON的高级AJAX开发技术

    一. 引言毫无疑问,AJAX已经成为当今Web开发中一种强有力的用户交互技术,但是它的许多可能性 应用仍然鲜为人知.在本文中,我们将来共同探讨如何使用JavaScript对象标志(JSON)和JSON ...

最新文章

  1. 数据蒋堂 | JOIN延伸 - 维度概念
  2. php 给图片增加背景平铺水印代码
  3. 利用JDBC连接数据库(MySQL)
  4. 版本名称GA的含义:SNAPSHOT-alpha-beta-release-GA
  5. upload-labs_pass12_文件名截断_URL要编码为%00_pass13_文件名截断_Hex修改为00
  6. 第十二届蓝桥杯Java省赛A组试题:异或数列
  7. 进程控制常用的一些操作
  8. 信息学奥赛一本通(2055:【例3.5】收费)
  9. 在Linux下写一个简单的驱动程序
  10. 【高德LBS开源组件大赛】iOS版地图选中Overlay功能组件
  11. 如何打造高质量的NLP数据集
  12. Pywin32操控Excel——2. 筛选与排序
  13. 关于Id returned 1exit status的解决办法
  14. Ubuntu无网络连接/无网络标识解决方法
  15. java输出罗马数字,【Java】【刷穿 LeetCode】13. 罗马数字转整数(简单)
  16. 前端es6 require动态引入图片报错Error: Cannot find module
  17. 无人机感知与规避技术综述
  18. 谷歌语音文本转换python代码_谷歌语音到文本API结果为空
  19. 怎么把视频转成mp3音频?
  20. 2023年,如何自学通过PMP?(含pmp资料)

热门文章

  1. java运行时异常与一般异常有何异同_JVM | 虚拟机运行时数据区域划分和使用详解...
  2. 圣诞海报模板|给设计师点灵感
  3. 唯美“光效”PNG免扣素材大集合,一眼爱上!
  4. 设计潮流趋势|背景图案素材,增加设计对比和补充前景元素
  5. 原创设计师如何提高影响力?到集设,让你的原创设计作品展示给世界
  6. 专属设计师的专业领域导航网站
  7. 「PPT模板」 商务UI风格
  8. Madagascar中的宏定义--圆周率PI
  9. Ubuntu16.04上安装SU(Seismic Unix)的基本步骤
  10. shell获取文件扩展名(前缀,后缀)