索引>查询>处理文档关联

处理文档关联

RavenDB坚持的一个设计原则就是文档是独立的,这就是说处理一个文档所需的所有数据都存储在文档本身之中。然而,这不是说对象之间不应存在联系。

在许多场景下我们都需要定义对象之间的关系。一般来说,我们主要面临一个主要问题:什么时候加载被包含的实体,我们打算由关联实体加载数据(除非我们对这些数据不感兴趣)。将实体关联的所有对象都存储在一个文档中的方式看起来成本很低,但其实这样做在数据库资源和网络流量方面成本很高。

RavenDB提供三种优雅的方式来解决这个问题。不同的场景会用到其中的一种或多种方式。如果使用恰当,可以答复提高性能、降低网络带宽占用并加速开发。

1. 反规范化

最简单的解决方式就是打破数据规范化,在文档中除了存储外键,也将关联实体的实际数据存储在文档中(或直接代替外键)。

以下面的JSON文档为例:

  1. // Order document with id: orders/1234
  2. { 
  3.     "Customer": {
  4.         "Name": "Itamar",
  5.         "Id": "customers/2345"
  6.     },
  7.     Items: [
  8.         { 
  9.         "Product": { 
  10.             "Id": "products/1234",
  11.             "Name": "Milk",
  12.             "Cost": 2.3
  13.             },
  14.         "Quantity": 3
  15.         }
  16.     ]
  17. }

正如你所见,Order文档包含来自Customer和Product文档的部分数据,后者在其他地方存储了完整版本。注意我们没有将customer的所有属性拷贝到Order中;相反我们只是拷贝了一部分在展示或处理订单时所关心的数据。这种方式称作反规范化引用(denormalized reference)。

这种反规范化的方式避免了跨文档查询,仅让必要的数据被传输,但这种方式让其他一些场景的处理变困难。如,考虑下面这些实体结构:

  1. public class Order
  2. {
  3.     public string CustomerId { get; set; }
  4.     public Guid[] SupplierIds { get; set; }
  5.     public Referral Refferal { get; set; }
  6.     public LineItem[] LineItems { get; set; }
  7.     public double TotalPrice { get; set; }
  8. }
  1. public class Customer
  2. {
  3.     public string Id { get; set; }
  4.     public string Name { get; set; }
  5. }

当我们由数据库加载一个Order时,我们需要知道客户的姓名和地址,我们决定创建一个反规范化的字段Order.Customer将客户的这些具体信息直接存储在Order对象中。显然,密码及其他不相关的信息不会包含在其中:

  1. public class DenormalizedCustomer
  2. {
  3.     public string Id { get; set; }
  4.     public string Name { get; set; }
  5.     public string Address { get; set; }
  6. }

Order和Customer之间没有直接关联。相反,Order包含一个DenormalizedCustomer,后者中包含我们在处理Order对象时所需的来自Customer的信息。

但当用户的地址改变时会发生什么呢?我们不得不执行一个级联操作来更新这个客户下的所有订单。但如果这个客户有很多订单并频繁的更新地址呢?保持这些信息的同步会给服务器带来很大的负担。如果另一个订单处理操作需要一套不同的客户属性呢?DenormalizedCustomer需要被扩充,直至客户记录的大部分字段被拷贝。

提示

反规范化对于极少变化的数据或当所关联的底层数据改变时需要保持不变的数据是一种有效的解决方案。

2. 包含

包含特性的目标正式反规范化的缺陷。包含方式中一个对象只需要持有另一个对象的引用而不是包含来自另一个对象属性的拷贝。可以在检索根对象时预加载相关联的对象。下面的代码展示了如何进行这个操作:

Session:

  1. Order order = session
  2.     .Include<Order>(=> x.CustomerId)
  3.     .Load("orders/1234");
  4.  
  5. // this will not require querying the server!
  6. Customer customer = session.Load<Customer>(order.CustomerId);

Commands:

  1. MultiLoadResult result = store
  2.     .DatabaseCommands
  3.     .Get(ids: new[] { "orders/1234" }, includes: new[] { "CustomerId" });
  4.  
  5. RavenJObject order = result.Results[0];
  6. RavenJObject customer = result.Includes[0];

上面代码中,我们检索key为"orders/1234"的Order,同时"包含"Order.CustomerId属性关联的Customer。  第二行代码中的Load()完全在客户端执行(换句话说,不会向RavenDB服务器发送第二次请求),因为相关的Customer对象已经被获取到(这是完整的Customer对象而不是像之前那种反规范化的版本)。

同时加载多个文档也是可行的。

Session:

  1. Order[] orders = session
  2.     .Include<Order>(=> x.CustomerId)
  3.     .Load("orders/1234", "orders/4321");
  4.  
  5. foreach (Order order in orders)
  6. {
  7.     // this will not require querying the server!
  8.     Customer customer = session.Load<Customer>(order.CustomerId);
  9. }

Commands:

  1. MultiLoadResult result = store
  2.     .DatabaseCommands
  3.     .Get(ids: new[] { "orders/1234", "orders/4321" }, includes: new[] { "CustomerId" });
  4.  
  5. List<RavenJObject> orders = result.Results;
  6. List<RavenJObject> customers = result.Includes;

Include也可以和Query一起使用:

Query:

  1. IList<Order> orders = session
  2.     .Query<Order, Orders_ByTotalPrice>()
  3.     .Customize(=> x.Include<Order>(=> o.CustomerId))
  4.     .Where(=> x.TotalPrice > 100)
  5.     .ToList();
  6.  
  7. foreach (Order order in orders)
  8. {
  9.     // this will not require querying the server!
  10.     Customer customer = session.Load<Customer>(order.CustomerId);
  11. }

DocumentQuery:

  1. IList<Order> orders = session
  2.     .Advanced
  3.     .DocumentQuery<Order, Orders_ByTotalPrice>()
  4.     .Include(=> x.CustomerId)
  5.     .WhereGreaterThan(=> x.TotalPrice, 100)
  6.     .ToList();
  7.  
  8. foreach (Order order in orders)
  9. {
  10.     // this will not require querying the server!
  11.     Customer customer = session.Load<Customer>(order.CustomerId);
  12. }

Commands:

  1. QueryResult result = store
  2.     .DatabaseCommands
  3.     .Query(
  4.         "Orders/ByTotalPrice",
  5.         new IndexQuery
  6.         {
  7.             Query = "TotalPrice_Range:{Ix100 TO NULL}"
  8.         },
  9.         includes: new[] { "CustomerId" });
  10.  
  11. List<RavenJObject> orders = result.Results;
  12. List<RavenJObject> customers = result.Includes;

Index:

  1. public class Orders_ByTotalPrice : AbstractIndexCreationTask<Order>
  2. {
  3.     public Orders_ByTotalPrice()
  4.     {
  5.         Map = orders => from order in orders
  6.                         select new
  7.                         {
  8.                             order.TotalPrice
  9.                         };
  10.     }
  11. }

RavenDB处理上面请求的底层工作方式是这样的,RavenDB通过2个通道来返回一个加载请求的结果。第一个通道是返回Load()方法检索的根对象的Results通道。第二个是返回其他包含文档的Includes通道。在客户端这一侧,包含文档并不通过Load()方法调用返回,而是被添加到session UoW(Unit of Work)中,后续对这些文档的加载请求将由session直接提供结果,而不需要额外的服务器查询。

一对多包含

包含可以用于一对多关联。在上面的类中,Order有一个Suppliers属性,其包含一系列对Supplier文档的引用。下面的代码会让这些供应商对象被预查询。

Session:

  1. Order order = session
  2.     .Include<Order>(=> x.SupplierIds)
  3.     .Load("orders/1234");
  4.  
  5. foreach (Guid supplierId in order.SupplierIds)
  6. {
  7.     // this will not require querying the server!
  8.     Supplier supplier = session.Load<Supplier>(supplierId);
  9. }

Commands:

  1. MultiLoadResult result = store
  2.     .DatabaseCommands
  3.     .Get(ids: new[] { "orders/1234" }, includes: new[] { "SupplierIds" });
  4.  
  5. RavenJObject order = result.Results[0];
  6. RavenJObject customer = result.Includes[0];

同样,foreach循环中的Load()调用不用请求服务器,因为Supplier对象已经被加载到session缓存中。

多个加载同样可行:

Session:

  1. Order[] orders = session
  2.     .Include<Order>(=> x.SupplierIds)
  3.     .Load("orders/1234", "orders/4321");
  4.  
  5. foreach (Order order in orders)
  6. {
  7.     foreach (Guid supplierId in order.SupplierIds)
  8.     {
  9.         // this will not require querying the server!
  10.         Supplier supplier = session.Load<Supplier>(supplierId);
  11.     }
  12. }

Commands:

  1. MultiLoadResult result = store
  2.     .DatabaseCommands
  3.     .Get(ids: new[] { "orders/1234", "orders/4321" }, includes: new[] { "SupplierIds" });
  4.  
  5. List<RavenJObject> orders = result.Results;
  6. List<RavenJObject> customers = result.Includes;

二级属性包含

包含不仅可用于加载文档中第一级的属性的值。还可以加载第二层属性的值。在上面的类中,Order包含一个Referral属性,类型如下:

  1. public class Referral
  2. {
  3.     public string CustomerId { get; set; }
  4.     public double CommissionPercentage { get; set; }
  5. }

这个类包含一个Customer的id。下面的代码将会包含二级属性关联的文档:

Session:

  1. Order order = session
  2.     .Include<Order>(=> x.Refferal.CustomerId)
  3.     .Load("orders/1234");
  4.  
  5. // this will not require querying the server!
  6. Customer customer = session.Load<Customer>(order.Refferal.CustomerId);

Commands:

  1. MultiLoadResult result = store
  2.     .DatabaseCommands
  3.     .Get(ids: new[] { "orders/1234" }, includes: new[] { "Refferal.CustomerId" });
  4.  
  5. RavenJObject order = result.Results[0];
  6. RavenJObject customer = result.Includes[0];

另一种方式是提供基于字符串的属性路径:

Session:

  1. Order order = session.Include("Refferal.CustomerId")
  2.     .Load<Order>("orders/1234");
  3.  
  4. // this will not require querying the server!
  5. Customer customer = session.Load<Customer>(order.Refferal.CustomerId);

Commands:

  1. MultiLoadResult result = store
  2.     .DatabaseCommands
  3.     .Get(ids: new[] { "orders/1234" }, includes: new[] { "Refferal.CustomerId" });
  4.  
  5. RavenJObject order = result.Results[0];
  6. RavenJObject customer = result.Includes[0];

二级属性包含对于集合属性中的二级属性同样适用。Order.LineItems属性是一个LineItem对象的集合,每个LineItem含有一个Product的引用:

  1. public class LineItem
  2. {
  3.     public Guid ProductId { get; set; }
  4.     public string Name { get; set; }
  5.     public int Quantity { get; set; }
  6. }

Product文档可以下面这样的语法来包含:

Session:

  1. Order order = session
  2.     .Include<Order>(=> x.LineItems.Select(li => li.ProductId))
  3.     .Load("orders/1234");
  4.  
  5. foreach (LineItem lineItem in order.LineItems)
  6. {
  7.     // this will not require querying the server!
  8.     Product product = session.Load<Product>(lineItem.ProductId);
  9. }

Commands:

  1. MultiLoadResult result = store
  2.     .DatabaseCommands
  3.     .Get(ids: new[] { "orders/1234" }, includes: new[] { "LineItems.,ProductId" });
  4.  
  5. RavenJObject order = result.Results[0];
  6. RavenJObject product = result.Includes[0];

当你想要加载多个文档。

Include方法中的Select()告诉RavenDB哪一个二级属性被用于关联。

约定

当像下面这样使用基于字符串的属性:

Session:

  1. Order order = session
  2.     .Include<Order>(=> x.Refferal.CustomerId)
  3.     .Load("orders/1234");
  4.  
  5. // this will not require querying the server!
  6. Customer customer = session.Load<Customer>(order.Refferal.CustomerId);

Commands:

  1. MultiLoadResult result = store
  2.     .DatabaseCommands
  3.     .Get(ids: new[] { "orders/1234" }, includes: new[] { "Refferal.CustomerId" });
  4.  
  5. RavenJObject order = result.Results[0];
  6. RavenJObject customer = result.Includes[0];

你必须记住提供的字符串路径要符合如下特定的规则:

  1. 用于分隔属性。如:上面例子中的"Referral.CustomerId"表示Order包含Referral属性,Referral属性包含另一个名为CustomerId的属性。

  2. 逗号用于指示属性是一个集合类型,如List。如果我们的Order有一个LineItems的列表,其中每个LineItem有一个ProductId属性,我们可以创建一个如下的字符串路径:"LineItems.,ProductId"。

  3. 前缀用于标识非字符串的文档标识符的id前缀。例如,如果CustomerId属性是一个整形,我们需要在其路径字符串"Referral.CustomerId"中添加customers/前缀,所以最终的字符串路径为"Referral.CustomerId(customers/)",集合的例子中如果ProductId是非字符串类型,路径应该是"LineItems.,ProductId(products/)"。

注意

对于字符串类型的标识属性,前缀是不需要的,因为他们已默认包含。

当使用HTTP API查询数据库时,学习字符串路径规则将很有帮助。

  1. curl -X GET "http://localhost:8080/databases/Northwind/queries/?include=LineItems.,ProductId(products/)&id=orders/1"

值类型标识符

上面的Include的示例假定用于解析引用的Id属性是一个字符串且其包含用于关联文档的完整标识(如CustomerId属性的值为"customers/5678")。包含也可以用于值类型的标识符。使用下面给出的实体:

  1. public class Order2
  2. {
  3.     public int CustomerId { get; set; }
  4.     public Guid[] SupplierIds { get; set; }
  5.     public Referral Refferal { get; set; }
  6.     public LineItem[] LineItems { get; set; }
  7.     public double TotalPrice { get; set; }
  8. }
  1. public class Customer2
  2. {
  3.     public int Id { get; set; }
  4.     public string Name { get; set; }
  5. }
  1. public class Referral2
  2. {
  3.     public int CustomerId { get; set; }
  4.     public double CommissionPercentage { get; set; }
  5. }

上面的例子可以重写为:

Session:

  1. Order2 order = session
  2.     .Include<Order2, Customer2>(=> x.CustomerId)
  3.     .Load("order2s/1234");
  4.  
  5. // this will not require querying the server!
  6. Customer2 customer = session.Load<Customer2>(order.CustomerId);

Commands:

  1. MultiLoadResult result = store
  2.     .DatabaseCommands
  3.     .Get(ids: new[] { "order2s/1234" }, includes: new[] { "CustomerId" });
  4.  
  5. RavenJObject order = result.Results[0];
  6. RavenJObject customer = result.Includes[0];

Query:

  1. IList<Order2> orders = session
  2.     .Query<Order2, Order2s_ByTotalPrice>()
  3.     .Customize(=> x.Include<Order2, Customer2>(=> o.CustomerId))
  4.     .Where(=> x.TotalPrice > 100)
  5.     .ToList();
  6.  
  7. foreach (Order2 order in orders)
  8. {
  9.     // this will not require querying the server!
  10.     Customer2 customer = session.Load<Customer2>(order.CustomerId);
  11. }

DocumentQuery:

  1. IList<Order2> orders = session
  2.     .Advanced
  3.     .DocumentQuery<Order2, Order2s_ByTotalPrice>()
  4.     .Include("CustomerId")
  5.     .WhereGreaterThan(=> x.TotalPrice, 100)
  6.     .ToList();
  7.  
  8. foreach (Order2 order in orders)
  9. {
  10.     // this will not require querying the server!
  11.     Customer2 customer = session.Load<Customer2>(order.CustomerId);
  12. }

Commands:

  1. QueryResult result = store
  2.     .DatabaseCommands
  3.     .Query(
  4.         "Order2s/ByTotalPrice",
  5.         new IndexQuery
  6.         {
  7.             Query = "TotalPrice_Range:{Ix100 TO NULL}"
  8.         },
  9.         includes: new[] { "CustomerId" });
  10.  
  11. List<RavenJObject> orders = result.Results;
  12. List<RavenJObject> customers = result.Includes;

Index:

  1. public class Order2s_ByTotalPrice : AbstractIndexCreationTask<Order2>
  2. {
  3.     public Order2s_ByTotalPrice()
  4.     {
  5.         Map = orders => from order in orders
  6.                         select new
  7.                         {
  8.                             order.TotalPrice
  9.                         };
  10.     }
  11. }

Session:

  1. Order2 order = session
  2.     .Include<Order2, Supplier>(=> x.SupplierIds)
  3.     .Load("order2s/1234");
  4.  
  5. foreach (Guid supplierId in order.SupplierIds)
  6. {
  7.     // this will not require querying the server!
  8.     Supplier supplier = session.Load<Supplier>(supplierId);
  9. }

Commands:

  1. MultiLoadResult result = store
  2.     .DatabaseCommands
  3.     .Get(ids: new[] { "order2s/1234" }, includes: new[] { "SupplierIds" });
  4.  
  5. RavenJObject order = result.Results[0];
  6. List<RavenJObject> suppliers = result.Includes;

Session:

  1. Order2 order = session
  2.     .Include<Order2, Customer2>(=> x.Refferal.CustomerId)
  3.     .Load("order2s/1234");
  4.  
  5. // this will not require querying the server!
  6. Customer2 customer = session.Load<Customer2>(order.Refferal.CustomerId);

Commands:

  1. MultiLoadResult result = store
  2.     .DatabaseCommands
  3.     .Get(ids: new[] { "order2s/1234" }, includes: new[] { "Refferal.CustomerId" });
  4.  
  5. RavenJObject order = result.Results[0];
  6. RavenJObject customer = result.Includes[0];

Session:

  1. Order2 order = session
  2.     .Include<Order2, Product>(=> x.LineItems.Select(li => li.ProductId))
  3.     .Load("orders/1234");
  4.  
  5. foreach (LineItem lineItem in order.LineItems)
  6. {
  7.     // this will not require querying the server!
  8.     Product product = session.Load<Product>(lineItem.ProductId);
  9. }

Commands:

  1. MultiLoadResult result = store
  2.     .DatabaseCommands
  3.     .Get(ids: new[] { "order2s/1234" }, includes: new[] { "LineItems.,ProductId" });
  4.  
  5. RavenJObject order = result.Results[0];
  6. List<RavenJObject> products = result.Includes;

Include<T, TInclude>中的第二个泛型参数指示关联属性指向的文档的类型。RavenDB会将关联文档的类型名称与关联属性的值合在一起的得到关联文档的完整标识符。例如,在第一个例子中,如果Order.CustomerId属性的值为56,客户端将使用customer2s/56作为键由数据库加载关联文档。Session.Load<Customer2>()
方法接收的参数为56,然后使用customer2s/56作为键由session缓存中查找并加载文档。

字典包含

在进行关联包含操作时,也可以使用字典的键和值。看下面这个例子:

  1. public class Person
  2. {
  3.     public string Id { get; set; }
  4.     public string Name { get; set; }
  5.     public Dictionary<string, string> Attributes { get; set; }
  6. }
  1. session.Store(
  2.     new Person
  3.     {
  4.         Id = "people/1",
  5.         Name = "John Doe",
  6.         Attributes = new Dictionary<string, string>
  7.         {
  8.             { "Mother", "people/2" },
  9.             { "Father", "people/3" }
  10.         }
  11.     });
  12.  
  13. session.Store(
  14.     new Person
  15.     {
  16.         Id = "people/2",
  17.         Name = "Helen Doe",
  18.         Attributes = new Dictionary<string, string>()
  19.     });
  20.  
  21. session.Store(
  22.     new Person
  23.     {
  24.         Id = "people/3",
  25.         Name = "George Doe",
  26.         Attributes = new Dictionary<string, string>()
  27.     });

我们可以包含字典值中关联的所有文档:

Session:

  1. var person = session
  2.     .Include<Person>(=> x.Attributes.Values)
  3.     .Load("people/1");
  4.  
  5. var mother = session.Load<Person>(person.Attributes["Mother"]);
  6. var father = session.Load<Person>(person.Attributes["Father"]);
  7.  
  8. Assert.Equal(1, session.Advanced.NumberOfRequests);

Commands:

  1. var result = store
  2.     .DatabaseCommands
  3.     .Get(new[] { "people/1" }, new[] { "Attributes.$Values" });
  4.  
  5. var include1 = result.Includes[0];
  6. var include2 = result.Includes[1];

当然也可以包含字典键关联的文档:

Session:

  1. var person = session
  2.     .Include<Person>(=> x.Attributes.Keys)
  3.     .Load("people/1");

Commands:

  1. var result = store
  2.     .DatabaseCommands
  3.     .Get(new[] { "people/1" }, new[] { "Attributes.$Keys" });

复合类型

如果字典中的值是复合类型,如:

  1. public class PersonWithAttribute
  2. {
  3.     public string Id { get; set; }
  4.     public string Name { get; set; }
  5.     public Dictionary<string, Attribute> Attributes { get; set; }
  6. }
  7.  
  8. public class Attribute
  9. {
  10.     public string Ref { get; set; }
  11. }
  1. session.Store(
  2. new PersonWithAttribute
  3. {
  4. Id = "people/1",
  5. Name = "John Doe",
  6. Attributes = new Dictionary<string, Attribute>
  7. {
  8. { "Mother", new Attribute { Ref = "people/2" } },
  9. { "Father", new Attribute { Ref = "people/3" } }
  10. }
  11. });session.Store(
  12. new Person
  13. {
  14. Id = "people/2",
  15. Name = "Helen Doe",
  16. Attributes = new Dictionary<string, string>()
  17. });session.Store(
  18. new Person
  19. {
  20. Id = "people/3",
  21. Name = "George Doe",
  22. Attributes = new Dictionary<string, string>()
  23. });

可以在指定的属性上进行包含操作:

Session:

  1. var person = session
  2.     .Include<PersonWithAttribute>(=> x.Attributes.Values.Select(=> v.Ref))
  3.     .Load("people/1");
  4.  
  5. var mother = session.Load<Person>(person.Attributes["Mother"].Ref);
  6. var father = session.Load<Person>(person.Attributes["Father"].Ref);
  7.  
  8. Assert.Equal(1, session.Advanced.NumberOfRequests);

Commands:

  1. var result = store
  2.     .DatabaseCommands
  3.     .Get(new[] { "people/1" }, new[] { "Attributes.$Values,Ref" });
  4.  
  5. var include1 = result.Includes[0];
  6. var include2 = result.Includes[1];

3. 两种方式组合

可以将之前介绍的两种技术结合。下面新的订单类中使用了前文中的DenormalizedCustomer类:

  1. public class Order3
  2. {
  3.     public DenormalizedCustomer Customer { get; set; }
  4.     public string[] SupplierIds { get; set; }
  5.     public Referral Refferal { get; set; }
  6.     public LineItem[] LineItems { get; set; }
  7.     public double TotalPrice { get; set; }
  8. }

我们得到反规范化的好处,一个可以被快速加载轻量级的Order及用于订单处理的相对固定的Customer详情。但是在必要时我们也可以简单高效的加载完整的Customer对象:

Session:

  1. Order3 order = session
  2.     .Include<Order3, Customer>(=> x.Customer.Id)
  3.     .Load("orders/1234");
  4.  
  5. // this will not require querying the server!
  6. Customer customer = session.Load<Customer>(order.Customer.Id);

Commands:

  1. MultiLoadResult result = store
  2.     .DatabaseCommands
  3.     .Get(ids: new[] { "orders/1234" }, includes: new[] { "Customer.Id" });
  4.  
  5. RavenJObject order = result.Results[0];
  6. RavenJObject customer = result.Includes[0];

反规范化与包含的结合同样可用于反规范化对象的列表。

包含也可以用于动态投影(Live Projection)的查询。包含在TransformResults执行完成后才执行。这给实现三次包含(Tertiary Includes)(如加载一个根文档关联的文档下关联的文档)提供了可能。

虽然RavenDB支持三次包含,但当你使用这个功能之前你应该重新评估你的文档模型。需要使用三次包含意味着你的文档设计走了“关系型”设计路线。

总结

什么时候选择何种方式没有一个严格的准则,但一般来说要给与充分考虑,评估每种方式带来的影响。

例如,一个电子商务应用中最好把商品名称和价格作为反规范化的数据放在一个订单明细对象中,因为你应该想让客户在订单历史中看到与下单时相同的价格和商品名称。但客户名称和地址最好使用引用而不是反规范化存于订单实体中。

在大部分反序列化不适用的场景,包含往往是可用的解决方案。

相关文章

索引:基础

查询:基础

RavenDB官网文档翻译系列第二的更多相关文章

  1. RavenDB官网文档翻译系列第一

    本系列文章主要翻译自RavenDB官方文档,有些地方做了删减,有些内容整合在一起.欢迎有需要的朋友阅读.毕竟还是中文读起来更亲切吗.下面进入正题. 起航 获取RavenDB RavenDB可以通过Nu ...

  2. Flink官网文档翻译

    http://ifeve.com/flink-quick-start/ http://vinoyang.com/2016/05/02/flink-concepts/ http://wuchong.me ...

  3. Quartz.NET快速上手第一课(官网文档翻译)

    Quartz.NET快速上手第一课(官网文档翻译) 原文链接 在你使用调度者(scheduler)之前,你需要对它进行实例化(谁能猜到这呢?).在实例化scheduler时候,你需要使用ISchedu ...

  4. Knockoutjs官网翻译系列(一)

    最近马上要开始一个新项目的研发,作为第一次mvvm应用的尝试,我决定使用knockoutjs框架.作为学习的开始就从官网的Document翻译开始吧,这样会增加印象并加入自己的思考,说是翻译也并不是纯 ...

  5. Knockout.Js官网学习(系列)

    1.Knockout.Js官网学习(简介) 2.Knockout.Js官网学习(监控属性Observables) Knockout.Js官网学习(数组observable) 3.Knockout.Js ...

  6. Nginx 官网文档翻译汇总

    Nginx 官网文档,各个模块的手册在这里. Nginx 中文文档 - 淘宝翻译 改版后的新 Nginx 官网文档 概述 新手指南 控制 Nginx 管理员指南 Admin Guide 安装 基本功能 ...

  7. Jenkins 官网文档翻译汇总

    Jenkins 官网地址 Jenkins 官网文档地址 用户手册 安装 Jenkins 使用 Jenkins 使用凭证 Pipeline 流水线 开始使用 Pipeline 使用 Jenkinsfil ...

  8. Knockoutjs官网翻译系列(三) 使用Computed Observables

    书接上回,前面谈到了在视图模型中可以定义普通的observable属性以及observableArray属性实现与UI元素的双向绑定,这一节我们继续探讨第三种可实现绑定的属性类型:computed o ...

  9. Docker 官网文档翻译汇总

    官方文档地址 Guide Docker 入门 Docker 入门教程 方向和设置 容器 服务 swarm 集群 stack 部署应用 概述 用 Docker 进行开发 在 Docker 上开发应用 应 ...

随机推荐

  1. nodejs进阶(6)—连接MySQL数据库

    1. 建库连库 连接MySQL数据库需要安装支持 npm install mysql 我们需要提前安装按mysql sever端 建一个数据库mydb1 mysql> CREATE DATABA ...

  2. 详解树莓派Model B+控制蜂鸣器演奏乐曲

    步进电机以及无源蜂鸣器这些都需要脉冲信号才能够驱动,这里将用GPIO的PWM接口驱动无源蜂鸣器弹奏乐曲,本文基于树莓派Mode B+,其他版本树莓派实现时需参照相关资料进行修改! 1 预备知识 1.1 ...

  3. 使用etree.HTML的编码问题

    title: 使用etree.HTML的编码问题 date: 2015-10-07 17:56:47 categories: [Python] tags: [Python, lxml, Xpath] ...

  4. jquery.Callbacks的实现

    前言 本人是一个热爱前端的菜鸟,一直喜欢学习js原生,对于jq这种js库,比较喜欢理解他的实现,虽然自己能力有限,水平很低,但是勉勉强强也算是能够懂一点吧,对于jq源码解读系列,博客园里有很多,推荐大 ...

  5. redis集成到Springmvc中及使用实例

    redis是现在主流的缓存工具了,因为使用简单.高效且对服务器要求较小,用于大数据量下的缓存 spring也提供了对redis的支持: org.springframework.data.redis.c ...

  6. 谈谈JS中的函数节流

    好吧,一直在秋招中,都没怎么写博客了...今天赶紧来补一补才行...我发现,在面试中,讲到函数节流好像可以加分,尽管这并不是特别高深的技术,下面就聊聊吧! ^_^ 备注:以下内容部分来自<Jav ...

  7. var和dynamic的区别

    1.var 1.均是声明动态类型的变量. 2.在编译阶段已经确定类型,在初始化的时候必须提供初始化的值. 3.无法作为方法参数类型,也无法作为返回值类型. 2.dynamic 1.均是声明动态类型的变 ...

  8. Could not evaluate expression

    VS15 调试变量不能显示值,提示:Could not evaluate expression 解决办法: 选择"在调试时显示运行以单击编辑器中的按钮"重启VS即可. 可参考:Vi ...

  9. enote笔记法使用范例(2)——指针(1)智能指针

    要知道什么是智能指针,首先了解什么称为 “资源分配即初始化” what RAII:RAII—Resource Acquisition Is Initialization,即“资源分配即初始化” 在&l ...

  10. ABAP单元测试最佳实践

    本文包含了我在开发项目中经历过的实用的ABAP单元测试指导方针.我把它们安排成为问答的风格,欢迎任何人添加更多的Q&A's,以完成这个列表. 在我的项目中,只使用传统的ABAP report. ...