65.9K
CodeProject 正在变化。 阅读更多。
Home

自适应分层知识管理 - 第二部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2021年4月6日

CPOL

22分钟阅读

viewsIcon

6202

downloadIcon

123

添加关系和关系实例管理 - 记录层次结构。

目录

引言

第一部分结论所述,第二部分将实现项目名称“自适应分层知识管理元模型”中的分层方面

新功能

  • 接下来要添加的是ParentChildMap实例和AllowableParentChildRelationship模型,这将通过创建实体关系的层次结构,使我们能够做更多有趣的事情。我将演示如何使用以下实体创建一本食谱书
    • 食谱创建者(一个Name实体)
    • 食谱配料(一个新的RecipeIngredient实体)
    • 食谱制作步骤(一个新的RecipeStep实体)
  • 我们将看到如何以不同的方式处理这些关系
    • 我们有什么食谱?
    • 我们有特定的人的哪些食谱?
    • 我们有哪些食谱使用了特定的配料?
  • 排序实体字段,以便我们可以重新排列记录网格列的默认顺序。

重构

  • 硬编码的实际表和表列名
  • 审查命名约定,因为我使用术语“实体”来指代用户在模型中定义的模型名称和集合名称。
  • 正确提供内置网页,而不是我目前实现的笨拙方法。
  • 服务应使用接口实现其public方法

约束

  • EntityField对于EntityIDNameDeleted应是唯一的
  • Entity对于NameDeleted应是唯一的
  • EntityInstance对于InstanceIDDeleted应是唯一的
  • EntityFieldValue对于InstanceIDEntityIDEntityFieldIDDeleted应是唯一的

索引

  • 需要为性能实现索引

关于下载

下载包含我用于此演示的 SQL Express 2017 数据库的备份,因此如果您恢复它,唯一需要做的就是在appsettings.json中设置连接字符串

"ConnectionStrings": {
  "DefaultConnection": "[your connection string]"
} 

重构

让我们先处理无聊的部分。我做的第一件事是删除了表和表列的硬编码字符串文字,除了在 SQL 语句中使用的地方。现在有一个新类

public static class Constants
{
  public const string ID = "ID";
  public const string VALUE = "Value";
  public const string NAME = "Name";
  public const string DELETED = "Deleted";
  public const string ENTITY = "Entity";
  public const string ENTITY_ID = "EntityID";
  public const string ENTITY_FIELD = "EntityField";
  public const string ENTITY_FIELD_ID = "EntityFieldID";
  public const string ENTITY_FIELD_VALUE = "EntityFieldValue";
  public const string ENTITY_FIELDS = "EntityFields";
  public const string ENTITY_INSTANCE = "EntityInstance";
  public const string ENTITY_INSTANCE_ID = "EntityInstanceID";
}

其次,EntityControllerEntityService分别重命名为RecordControllerRecordService,以帮助减少元模型表和实体实例记录集合之间的混淆。这也将内置记录编辑器路径从/entity更改为/record,以及一些其他次要的前端更改以支持/与后端重命名保持一致。

命名约定

展望未来,以下术语将具有以下含义。

元模型命名约定

  • 实体:元模型:可以为其创建记录的名称集合
  • 实体字段:元模型:与实体关联的字段集合
  • 实体关系:元模型:父实体到允许的子实体的集合

记录实例命名约定

  • 记录:实体的实例,并将关联的实体字段旋转为列
  • 实体实例:在数据库模型中,记录的 ID,它映射到特定的实体定义
  • 实体字段值:在数据库模型中,与元模型的实体名称、实体字段和实体实例关联的记录实例,包含一个值
  • 关系实例:父记录和子记录之间关系的实例。

正确提供网页

我将其推迟到第三部分。它似乎优先级很低!

约束

已添加以下约束。其副作用是,后端应检查正在添加的字段是否已被删除,如果是,则恢复该名称的已删除字段。这也适用于对实体字段的操作。

  • Entity对于NameDeleted应是唯一的。
    ALTER TABLE Entity ADD UNIQUE (Name, Deleted);
  • EntityField对于EntityIDNameDeleted应是唯一的。
    ALTER TABLE EntityField ADD UNIQUE (Name, EntityID, Deleted);
  • EntityFieldValue对于EntityInstanceIDEntityIDEntityFieldIDDeleted应是唯一的。
    ALTER TABLE EntityFieldValue ADD UNIQUE _
    (EntityInstanceID, EntityID, EntityFieldID, Deleted);

索引

已添加以下索引

CREATE NONCLUSTERED INDEX [IX_Entity] ON [dbo].[Entity] ([Name] ASC)

CREATE NONCLUSTERED INDEX [IX_EntityField] ON [dbo].[EntityField] ([EntityID] ASC, [Name] ASC)

CREATE NONCLUSTERED INDEX [IX_EntityFieldValue] ON [dbo].[EntityFieldValue] _
      ([EntityID] ASC, [EntityFieldID] ASC, [EntityInstanceID] ASC)

CREATE NONCLUSTERED INDEX [IX_EntityInstance] ON [dbo].[EntityInstance] ([EntityID] ASC)

服务应使用接口实现其公共方法

services.AddSingleton<ITableService, TableService>();
services.AddSingleton<IRecordService, RecordService>();
services.AddSingleton<IRelationshipService, RelationshipService>();

完成! 我不会用接口代码来烦扰你。

连接

此时,支持我们查询中的表连接是有用的。这样做的原因是,我们希望自动将父实体和子实体名称添加到关系查询中,以便客户端不需要实体集合,也不需要将父/子实体 ID 映射到实体集合以获取父/子实体名称。

首先,现在有一个Join定义类

public class Join
{
  public enum JoinType
  {
    Inner,
    Left,
    Right,
    Full,
  }

    public string Table { get; set; }
    public string TableField { get; set; }
    public string WithTable { get; set; }
    public FieldAliases WithTableFields { get; set; }
    public JoinType Type { get; set; }

    public Join(string table, string withTable, _
   FieldAliases withTableFields, JoinType type = JoinType.Inner, string tableField = null)
    {
      Table = table;
      WithTable = withTable;
      TableField = tableField;
      WithTableFields = withTableFields;
      Type = type;
  }
}

我们有一个Joins集合,允许我们获取joinsjoin字段

public string GetJoins()
{
  string ret = String.Empty;

  if (Count > 0)
  {
    List<string> joins = new List<string>();
    var joinAliases = GetJoinAliases();

    this.ForEachWithIndex((j, idx) =>
    {
      // Use the override if it exists, otherwise the default is the "with" table name + ID
      var tableFieldName = j.TableField ?? $"{j.WithTable}ID";
      var alias = joinAliases[idx].alias;
      var joinType = joinTemplate[j.Type];

      // Find the join table alias.
      // Currently this functionality is limited to one table with which we join other tables.
      // To fix this, the dictionary needs to have some "key" 
     // (such as the qualifier) that determines which of multiple joins
      // to continue joining with. Too complicated to set up right now.

      var joinTableAliases = joinAliases.Where(a => a.Value.table == j.Table).ToList();
      var joinTableAlias = j.Table;

      Assertion.IsTrue(joinTableAliases.Count <= 1, $"Unsupported: 
               Cannot join multiple instances of {j.Table} with other joins.");

      if (joinTableAliases.Count == 1)
      {
        joinTableAlias = joinTableAliases[0].Value.alias;
      }

      var join = $"{joinType} {j.WithTable} {alias} on {alias}.ID = 
                {joinTableAlias}.{tableFieldName} and {alias}.Deleted = 0";
      joins.Add(join);
    });

    ret = String.Join(Constants.CRLF, joins);
  }

  return ret;
}

public string GetJoinFields(string prepend = "")
{
  string ret = String.Empty;

  if (Count > 0)
  {
    List<string> joinFields = new List<string>();
    var joinAliases = GetJoinAliases();

    this.ForEachWithIndex((j, idx) =>
    {
      if (j.WithTableFields != null)
      {
        var joinTableAlias = joinAliases[idx].alias;

        j.WithTableFields.ForEach(f =>
        {
          joinFields.Add($"{joinTableAlias}.{f.Key} {f.Value}");
        });
      }
    });

    if (joinFields.Count > 0)
    {
      ret = prepend + String.Join(",", joinFields);
    }
  }

  return ret;
}

private Dictionary<int, (string alias, string table)> GetJoinAliases()
{
  Dictionary<int, (string alias, string table)> joinAliases = new Dictionary<int, 
                 (string alias, string table)>();

  this.ForEachWithIndex((j, idx) =>
  {
    var alias = $"{j.WithTable}{idx}";

    // If we have an alias for the WithTable because it's part of a join, use it.
    joinAliases.TryGetValue(idx, out (string alias, string table) tableAlias);
    tableAlias.alias ??= alias;
    tableAlias.table ??= j.WithTable;

    joinAliases[idx] = tableAlias;
  });

  return joinAliases;
}

以上是一段复杂的代码,它为复杂的连接计算表别名,例如,这是为关系实例查询生成的 SQL

select RelationshipInstance.* ,Entity1.Name Parent,Entity2.Name Child 
from RelationshipInstance 
JOIN EntityRelationship EntityRelationship0 on EntityRelationship0.ID = _
   RelationshipInstance.EntityRelationshipID and EntityRelationship0.Deleted = 0
JOIN Entity Entity1 on Entity1.ID = EntityRelationship0.ParentEntityID and Entity1.Deleted = 0
JOIN Entity Entity2 on Entity2.ID = EntityRelationship0.ChildEntityID and Entity2.Deleted = 0 
where RelationshipInstance.Deleted = 0

请注意我们如何在EntityRelationship(没有返回的字段)以及具有父实体和子实体名称的Entity记录上进行连接。目前,此功能存在限制,如注释和断言中所述。

这还需要修改“select”构建器,由于这对于SqlSelectBuilderSqlInsertSelectBuilder都是通用的,我创建了一个它们都可以调用的方法

private StringBuilder GetCoreSelect(string table, Joins joins = null)
{
  var joinFields = joins?.GetJoinFields(",") ?? "";
  var joinTables = joins?.GetJoins() ?? "";

  StringBuilder sb = new StringBuilder();
  sb.Append($"select {table}.* {joinFields} from {table} {joinTables} 
           where {table}.Deleted = 0");

  return sb;
}

稍后我会向您展示如何使用它。

实体与记录关系

记录之间的关系应该是动态的,特别是如果用户需要在两个实体之间建立新关系,则可以轻松创建这种关系。目前,我正在进行一个骨架实现,以使自适应分层记录管理的核心概念工作起来。我尚未实现的是

  1. 关系通常具有描述符,例如“配偶”、“丈夫”、“妻子”、“父母”、“子女”、“父亲”、“母亲”。
  2. 关系通常具有开始和结束时期。
  3. 关系可以是重复的,无论是固定间隔还是随机间隔——想想你在日历上每年安排的事情,与生活中发生的随机事情相比。

同样显而易见的是,特定记录可以与其他许多记录建立关系。

约束

  • 出于我们的目的,不支持循环关系实例——我们不支持您可以是自己的祖父母的时间旅行概念。
  • 记录不能与自身建立关系。

架构

添加了两张表

  1. EntityRelationship - 这描述了实体之间允许的关系。
  2. RelationshipInstance - 这描述了两个记录之间的特定关系。
CREATE TABLE [dbo].[EntityRelationship](
  [ID] [int] IDENTITY(1,1) NOT NULL,
  [ParentEntityID] [int] NOT NULL,
  [ChildEntityID] [int] NOT NULL,
  [Deleted] [bit] NOT NULL,
CONSTRAINT [PK_EntityRelationship] PRIMARY KEY CLUSTERED ...

ALTER TABLE [dbo].[EntityRelationship] _
WITH CHECK ADD CONSTRAINT [FK_EntityRelationship_Entity] FOREIGN KEY([ParentEntityID])
REFERENCES [dbo].[Entity] ([ID])

ALTER TABLE [dbo].[EntityRelationship] CHECK CONSTRAINT [FK_EntityRelationship_Entity]

ALTER TABLE [dbo].[EntityRelationship] WITH CHECK _
ADD CONSTRAINT [FK_EntityRelationship_Entity1] FOREIGN KEY([ChildEntityID])
REFERENCES [dbo].[Entity] ([ID])

CREATE TABLE [dbo].[RelationshipInstance](
  [ID] [int] IDENTITY(1,1) NOT NULL,
  [EntityRelationshipID] [int] NOT NULL,
  [ParentInstanceID] [int] NOT NULL,
  [ChildInstanceID] [int] NOT NULL,
  [Deleted] [bit] NOT NULL,
CONSTRAINT [PK_RelationshipInstance] PRIMARY KEY CLUSTERED 

ALTER TABLE [dbo].[RelationshipInstance] WITH CHECK ADD CONSTRAINT _
[FK_RelationshipInstance_EntityInstance] FOREIGN KEY([ParentEntityInstanceID])
REFERENCES [dbo].[EntityInstance] ([ID])

ALTER TABLE [dbo].[RelationshipInstance] WITH CHECK ADD CONSTRAINT _
[FK_RelationshipInstance_EntityInstance1] FOREIGN KEY([ChildEntityInstanceID])
REFERENCES [dbo].[EntityInstance] ([ID])

ALTER TABLE [dbo].[RelationshipInstance] WITH CHECK ADD CONSTRAINT _
[FK_RelationshipInstance_EntityRelationship] FOREIGN KEY([EntityRelationshipID])
REFERENCES [dbo].[EntityRelationship] ([ID])

我们的模式现在看起来像这样,其中顶部3个表用于管理记录(实例),底部3个表用于管理元模型定义。

关系控制器

此控制器管理实体关系定义和记录关系实例。

实体关系定义

管理关系定义的 API 端点是

/// <summary>
/// Returns all parent-child relationship definitions.
/// </summary>
[HttpGet("all")]
public object GetRelationshipDefinitions()
{
  // TODO: This needs to support pagination.
  var data = rs.GetAllDefinitions(cfg, ts);

  return data;
}

/// <summary>
/// Returns the definitions for children having a relationship 
/// with the specified parent, as parent-child records.
/// </summary>
[HttpGet("Parent/{parentEntityId}")]
public object GetRelationshipDefinitionsOfParent(int parentEntityId)
{
  var data = rs.GetRelationshipDefinitionsOfParent(cfg, ts, parentEntityId);

  return data;
}

/// <summary>
/// Returns the definitions for parents having a relationship 
/// with the specified child, as parent-child records.
/// </summary>
[HttpGet("Child/{childEntityId}")]
public object GetRelationshipDefinitionsOfChild(int childEntityId)
{
  var data = rs.GetRelationshipDefinitionsOfChild(cfg, ts, childEntityId);

  return data;
}

/// <summary>
/// Creates a parent-child relationship definition between two entities.
/// </summary>
[HttpPost()]
public object Insert(Parameters parms)
{
  var data = rs.InsertDefinition(cfg, ts, parms);

  return data;
}

/// <summary>
/// Deletes a specific parent-child relationship definition given the entity relationship ID.
/// </summary>
[HttpDelete("{entityRelationshipId}")]
public object DeleteParentChild(int entityRelationshipId)
{
  rs.DeleteDefinition(cfg, ts, entityRelationshipId);

  return NoContent();
}

/// <summary>
/// Deletes a specific parent-child relationship definition 
/// given the parent and child entity ID's.
/// </summary>
[HttpDelete("{parentEntityId}/{childEntityId}")]
public object DeleteParentChild(int parentEntityId, int childEntityId)
{
  rs.DeleteDefinition(cfg, ts, parentEntityId, childEntityId);

  return NoContent();
}

请注意,没有用于更新两个实体之间关系的端点。它要么存在,要么不存在。一旦存在,您就无法更改父或子,因为这会破坏该关系实例映射到其特定父/子记录的实体概念。

关系实例

可以查询关系实例以获取

  • 所有实例——这不是一个好主意,除非您正在过滤结果数据集,否则您可能会有数百万个实例。
  • 父级的子实例。
  • 子级的父实例。

并且可以删除关系实例。同样,允许更新关系实例。

关系实例的控制器代码

[HttpGet("instance/all")]
public object GetRelationshipInstances()
{
  // TODO: This REALLY needs to support pagination.
  var data = rs.GetAllInstances(cfg, ts);

  return data;
}

[HttpGet("instance/parent/{parentEntityId}")]
public object GetChildInstancesOfParent(int parentEntityId)
{
  var data = rs.GetChildInstancesOfParent(cfg, ts, parentEntityId);

  return data;
}

[HttpGet("instance/child/{childEntityId}")]
public object GetParentInstancesOfChild(int childEntityId)
{
  var data = rs.GetParentInstancesOfChild(cfg, ts, childEntityId);

  return data;
}

[HttpPost("instance")]
public object InsertInstance(Parameters parms)
{
  var data = rs.InsertInstance(cfg, ts, parms);

  return data;
}

[HttpDelete("instance/{entityRelationshipId}")]
public object DeleteInstance(int relationshipInstanceId)
{
  rs.DeleteInstance(cfg, ts, relationshipInstanceId);

  return NoContent();
}

[HttpDelete("instance/{parentInstanceId}/{childInstanceId}")]
public object DeleteInstance(int parentInstanceId, int childInstanceId)
{
  rs.DeleteInstance(cfg, ts, parentInstanceId, childInstanceId);

  return NoContent();
}

关系服务

实体关系定义

类似地,关系服务管理实体定义以及添加/删除关系实例。同样,没有update函数,因为更改现有关系实例没有意义,并且它可能会破坏父/子实例实体与实体关系定义的匹配。这段代码几乎不值得放在服务中,但重点是控制器应该只做很少的事情,即呈现对底层服务方法的调用工作流。在这种情况下,控制器中除了调用服务之外没有“工作流”。然而,重点是该服务可能会被另一种类型的控制器使用,我们希望通过将所有必要操作放在服务中而不是控制器中来减少复制粘贴的代码。顺便说一句,我们经常看到控制器方法实现得更像服务,而不是对于 Web API 项目来说,它们真正应该是什么:一个调用服务以返回所需结果的端点路由处理程序。你不能重用内置在控制器方法中的逻辑。你可以重用服务。

另请注意,返回的不仅仅是父/子 ID,还有实体名称。从 UI 的角度来看,这更有用,因为它不需要实体集合,也不需要额外的逻辑来在显示关系定义时填充实体名称。由于必要的join对于所有这些操作都是通用的,因此它们被静态定义为

private static Joins joinDefinition = new Joins()
{
  new Join(
    Constants.ENTITY_RELATIONSHIP,
    Constants.ENTITY,
    new FieldAliases() { { "Name", "Parent" } },
    tableField: Constants.PARENT_ENTITY_ID),

  new Join(
    Constants.ENTITY_RELATIONSHIP,
    Constants.ENTITY,
    new FieldAliases() { { "Name", "Child" } },
    tableField: Constants.CHILD_ENTITY_ID),
};

join定义在适当的TableService方法调用中传入。然后,RelationshipService的实现如下所示

public Records GetAllDefinitions(IConfiguration cfg, ITableService ts)
{
  var data = ts.GetAll(cfg, Constants.ENTITY_RELATIONSHIP, joins: joinDefinition);

  return data;
}

public Records GetRelationshipDefinitionsOfParent
(IConfiguration cfg, ITableService ts, int parentEntityId)
{
  var cond = new Conditions()
  {
    {new Condition(Constants.PARENT_ENTITY_ID, Condition.Op.Equals, parentEntityId) }
  };

  var data = ts.GetAll(cfg, Constants.ENTITY_RELATIONSHIP, cond, joinDefinition);

  return data;
}

public Records GetRelationshipDefinitionsOfChild
(IConfiguration cfg, ITableService ts, int childEntityId)
{
  var cond = new Conditions()
  {
    {new Condition(Constants.CHILD_ENTITY_ID, Condition.Op.Equals, childEntityId) }
  };

  var data = ts.GetAll(cfg, Constants.ENTITY_RELATIONSHIP, cond, joinDefinition);

  return data;
}

public Record InsertDefinition(IConfiguration cfg, ITableService ts, Parameters parms)
{
  var data = ts.Insert(cfg, Constants.ENTITY_RELATIONSHIP, parms, joinDefinition);

  return data;
}

public void DeleteDefinition(IConfiguration cfg, ITableService ts, int entityRelationshipId)
{
  ts.Delete(cfg, Constants.ENTITY_RELATIONSHIP, entityRelationshipId);
}

public void DeleteDefinition(IConfiguration cfg, 
ITableService ts, int parentEntityId, int childEntityId)
{
  var cond = new Conditions()
  {
    {new Condition(Constants.PARENT_ENTITY_ID, Condition.Op.Equals, parentEntityId) },
    {new Condition(Constants.CHILD_ENTITY_ID, Condition.Op.Equals, childEntityId) }
  };

  var data = ts.GetSingle(cfg, Constants.ENTITY_RELATIONSHIP, cond);
  ts.Delete(cfg, Constants.ENTITY_RELATIONSHIP, data[Constants.ID].ToInt());
}

使用示例

在这里,我们将再次使用 Postman 演示 API 端点。但是,正如我在本文开头提到的,我们将创建一个食谱应用程序。我们可以通过现有的/sysadmin UI 创建实体和实体字段定义

具有以下字段

请注意,我们可以使用特殊字符,例如“#”,单词之间可以有空格等,因为我们正在创建元模型而不是实际的表。

这里有一个与保留字段名相关的大红旗。我不得不将Recipe实体的字段“Name”更改为“Recipe Name”,否则我会因为“Name”已经是EntityField表中的一个字段而收到 pivot 语句错误!我将在第三部分的重构部分处理这个问题。

现在我们要创建以下关系

我们需要从数据库中获取实体的 ID

现在我们可以开始插入实体关系了

Recipe-名称

curl --location --request POST 'https://:8493/relationship' \
--header 'Content-Type: application/json' \
--data-raw '{"ParentEntityID": 13, "ChildEntityID": 11}'

响应

{
  "ID": 17,
  "ParentEntityID": 13,
  "ChildEntityID": 11,
  "Deleted": false,
  "Parent": "Recipe",
  "Child": "Name"
}

Recipe-配料

curl --location --request POST 'https://:8493/relationship' \
--header 'Content-Type: application/json' \
--data-raw '{"ParentEntityID": 13, "ChildEntityID": 14}'

响应

{
  "ID": 18,
  "ParentEntityID": 13,
  "ChildEntityID": 14,
  "Deleted": false,
  "Parent": "Recipe",
  "Child": "Recipe Ingredients"
}

Recipe-制作步骤

curl --location --request POST 'https://:8493/relationship' \
--header 'Content-Type: application/json' \
--data-raw '{"ParentEntityID": 13, "ChildEntityID": 15}'

响应

{
  "ID": 19,
  "ParentEntityID": 13,
  "ChildEntityID": 15,
  "Deleted": false,
  "Parent": "Recipe",
  "Child": "Recipe Directions"
}

查询所有实体关系,使用/relationship/all(和/relationship/parent/13,因为我们只定义了一个关系集),我们看到

[
  {
    "ID":17,
    "ParentEntityID":13,
    "ChildEntityID":11,
    "Deleted":false,
    "Parent":"Recipe",
    "Child":"Name"
  },
  {
    "ID":18,
    "ParentEntityID":13,
    "ChildEntityID":14,
    "Deleted":false,
    "Parent":"Recipe",
    "Child":"Recipe Ingredients"
  },
  {
    "ID":19,
    "ParentEntityID":13,
    "ChildEntityID":15,
    "Deleted":false,
    "Parent":"Recipe",
    "Child":"Recipe Directions"
 }
]

或使用/relationship/child/15,我们看到

[
  {
    "ID":19,
    "ParentEntityID":13,
    "ChildEntityID":15,
    "Deleted":false,
    "Parent":"Recipe",
    "Child":"Recipe Directions"
  }
]

关系实例

管理关系实例的关系服务方法

public Records GetAllInstances(IConfiguration cfg, ITableService ts)
{
  var data = ts.GetAll(cfg, Constants.RELATIONSHIP_INSTANCE, joins: joinInstance);

  return data;
}

public Records GetChildInstancesOfParent
(IConfiguration cfg, ITableService ts, int parentInstanceId)
{
  var cond = new Conditions()
  {
    {new Condition(Constants.PARENT_ENTITY_INSTANCE_ID, 
                  Condition.Op.Equals, parentInstanceId) }
  };

  var data = ts.GetAll(cfg, Constants.RELATIONSHIP_INSTANCE, cond, joinInstance);

  return data;
}

public Records GetParentInstancesOfChild
(IConfiguration cfg, ITableService ts, int childInstanceId)
{
  var cond = new Conditions()
  {
    {new Condition(Constants.CHILD_ENTITY_INSTANCE_ID, Condition.Op.Equals, childInstanceId) }
  };

  var data = ts.GetAll(cfg, Constants.RELATIONSHIP_INSTANCE, cond, joinInstance);

  return data;
}

public Record InsertInstance(IConfiguration cfg, ITableService ts, Parameters parms)
{
  var data = ts.Insert(cfg, Constants.RELATIONSHIP_INSTANCE, parms, joins: joinInstance);

  return data;
}

public void DeleteInstance(IConfiguration cfg, ITableService ts, int relationshipInstanceId)
{
  ts.Delete(cfg, Constants.RELATIONSHIP_INSTANCE, relationshipInstanceId);
}

public void DeleteInstance(IConfiguration cfg, 
ITableService ts, int parentInstanceId, int childInstanceId)
{
  var instRel = ts.GetSingle(cfg, Constants.RELATIONSHIP_INSTANCE, new Conditions()
  {
    new Condition(Constants.PARENT_ENTITY_INSTANCE_ID, Condition.Op.Equals, parentInstanceId),
    new Condition(Constants.CHILD_ENTITY_INSTANCE_ID, Condition.Op.Equals, childInstanceId)
  });

  var id = instRel[Constants.ID].ToInt();

  ts.Delete(cfg, Constants.RELATIONSHIP_INSTANCE, id);
}

请注意我们如何有一个用于实例的join

private static Joins joinInstance = new Joins()
{
  new Join(
    Constants.RELATIONSHIP_INSTANCE,
    Constants.ENTITY_RELATIONSHIP),

  new Join(
    Constants.ENTITY_RELATIONSHIP,
    Constants.ENTITY,
    new FieldAliases() { { "Name", "Parent" } },
    tableField: Constants.PARENT_ENTITY_ID),

  new Join(
    Constants.ENTITY_RELATIONSHIP,
    Constants.ENTITY,
    new FieldAliases() { { "Name", "Child" } },
    tableField: Constants.CHILD_ENTITY_ID),
};

这为我们提供了实体名称以及父/子实例。这是为了客户端的便利。

使用示例

首先,使用 /record 编辑器,因为我很懒,不想创建全套配料和制作步骤,我将创建两个食谱,每个食谱一个配料和一个制作步骤。所以这是我们在实例表中拥有的

现在,由于我还没有为此提供用户界面(接下来的部分,您将了解用户界面为何如此有用),这里有一个查询来了解我们正在做什么

select e.Name, ei.ID EntityIntanceID, ef.Name, efv.Value from EntityInstance ei
join Entity e on e.ID = ei.EntityID
join EntityFieldValue efv on efv.EntityInstanceID = ei.ID and _
    efv.Value is not null and efv.Value != ''
join EntityField ef on ef.ID = efv.EntityFieldID
where e.Name like 'Recipe%'
and ef.Name in ('Name', 'Recipe Name', 'Ingredient', 'Instruction')

这给我们带来了这些实例关系

我们之前知道我们的实体关系 ID

  • 18:食谱 - 食谱配料
  • 19:食谱 - 食谱制作步骤

所以现在我们可以使用 Postman 插入关系——我只展示一个例子

cURL

curl --location --request POST 'https://:8493/relationship/instance' \
--header 'Content-Type: application/json' \
--data-raw '{"EntityRelationshipID": 18, "ParentEntityInstanceID": 19, 
 "ChildEntityInstanceID": 21}'

响应

{
  "ID": 2,
  "EntityRelationshipID": 17,
  "ParentEntityInstanceID": 19,
  "ChildEntityInstanceID": 21,
  "Deleted": false,
  "Parent": "Recipe",
  "Child": "Recipe Ingredients"
}

我应该将实体命名为Recipe配料和Recipe制作步骤的单数形式!

完成!

现在,当我们执行 GET /relationship/instance/all

我们看到两个食谱,每个食谱都有一个配料和一个制作步骤。

[
  {"ID":6,"EntityRelationshipID":18,"ParentEntityInstanceID":19,
 "ChildEntityInstanceID":21,"Deleted":false,"Parent":"Recipe","Child":"Recipe Ingredient"},
  {"ID":7,"EntityRelationshipID":19,"ParentEntityInstanceID":19,
 "ChildEntityInstanceID":23,"Deleted":false,"Parent":"Recipe","Child":"Recipe Direction"},
  {"ID":8,"EntityRelationshipID":18,"ParentEntityInstanceID":20,
 "ChildEntityInstanceID":22,"Deleted":false,"Parent":"Recipe","Child":"Recipe Ingredient"},
  {"ID":9,"EntityRelationshipID":19,"ParentEntityInstanceID":20,
 "ChildEntityInstanceID":24,"Deleted":false,"Parent":"Recipe","Child":"Recipe Direction"}
]

我们可以通过 GET /relationship/instance/child/24 获取子元素的父元素,例如

[
  {"ID":9,"EntityRelationshipID":19,"ParentEntityInstanceID":20,
 "ChildEntityInstanceID":24,"Deleted":false,"Parent":"Recipe","Child":"Recipe Direction"}
]

或者通过 GET /relationship/instance/parent/20 获取父元素的子元素

[
  {"ID":8,"EntityRelationshipID":18,"ParentEntityInstanceID":20,
 "ChildEntityInstanceID":22,"Deleted":false,"Parent":"Recipe","Child":"Recipe Ingredient"},
  {"ID":9,"EntityRelationshipID":19,"ParentEntityInstanceID":20,
 "ChildEntityInstanceID":24,"Deleted":false,"Parent":"Recipe","Child":"Recipe Direction"}
]

定义实体关系的用户界面

正如第一部分所述,我不再费心向您展示 TypeScript 代码——您可以自己查看。它不是最优雅的东西,因为对于本系列文章的目的,UI 是次要的。

用户界面非常简单和“笨拙”——你在左侧选择实体作为“父级”,然后在右侧添加实体作为子级。子实体是一个下拉菜单

没有对重复的子名称、相同的父子名称或循环关系进行测试。

关系实例及其记录的用户界面

现在我们将使用现有的“记录”用户界面来引入显示实例实体与其父级和子级关系的递归概念。真正有趣的部分将是当我们实现选择层次结构中不同级别的记录时。

获取实体关系层次结构 API

有用的地方在于,我们可以有一个 API 端点,它返回实体的层次结构,无论我们从哪个实体开始。这样,我们可以识别顶层实体(以及当父实体有多个子实体时子实体的顺序,但这在这里没有实现)。因此,无论左侧选择了哪个实体

我们应该始终看到一个一致的层次结构。当然,在一个复杂的图中,可以有多个顶级实体。我避免使用“根”这个词,因为树的根在树的底部,而我们希望在 UI 的顶部显示“根”(即顶级实体)!另请注意,此时尚未充分测试各种用例!尽管我很高兴避免它,但此时,拥有一个 C# 模型实际上是有用的,这样我们就可以以可读的方式在代码中创建结构。为此,代码将放入RelationshipService中,因此它更适合集成测试。

public interface IEntityRelationship
{
  List<EntityRelationship> Children { get; set; }
}

public class TopLevelEntityRelationship : IEntityRelationship
{
  [JsonProperty(Order = 0)]
  public int ID { get; set; }

  [JsonProperty(Order = 1)]
  public string Parent { get; set; }

  [JsonProperty(Order = 2)]
  public int ParentEntityID { get; set; }

  [JsonProperty(Order = 3)]
  public List<EntityRelationship> Children { get; set; }

  public TopLevelEntityRelationship(EntityRelationship er)
  {
    ID = er.ID;
    Parent = er.Parent;
    ParentEntityID = er.ParentEntityID;
  }

  public TopLevelEntityRelationship Clone()
  {
    return MemberwiseClone() as TopLevelEntityRelationship;
  }
}

public class EntityRelationship : IEntityRelationship
{
  [JsonProperty(Order = 0)]
  public int ID { get; set; }

  [JsonProperty(Order = 1)]
  public string Parent { get; set; }

  [JsonProperty(Order = 2)]
  public string Child { get; set; }

  [JsonProperty(Order = 3)]
  public int ParentEntityID { get; set; }

  [JsonProperty(Order = 4)]
  public int ChildEntityID { get; set; }

  [JsonProperty(Order = 5)]
  public List<EntityRelationship> Children {get;set;}

  public EntityRelationship Clone()
  {
    return MemberwiseClone() as EntityRelationship;
  }
}

我们为什么要有一个克隆方法?原因是,如果我们不克隆EntityRelationship的实例,我们最终会在Children集合中拥有与它们的父级完全相同的对象引用。这实际上发生在顶级实体中,因为这些实际上将是没有父级的父子EnityRelationship实例,但是,我们希望它们出现在子列表中。如果我们不克隆实例,JSON 序列化器就会出现问题。首先,我们会注意到错误“Self referencing loop detected”,这非常合理。如果我们在序列化器中添加选项,例如

ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
PreserveReferencesHandling = PreserveReferencesHandling.Objects

那么我们得到了我们想要的东西,但是 JSON 中有$id$ref键,特别是$ref必须由客户端解码才能获取它所引用的对象的属性。因此,为了避免所有这些并向客户端提供合理的 JSON,我们克隆了子EntityRelationship实例,以便它们是新对象。

讨论

创建层次结构“图”具有一定的复杂性。

鉴于(请注意NameContactRecipe的子元素)

[
  {
    "ID": 17,
    "ParentEntityID": 13,
    "ChildEntityID": 11,
    "Deleted": false,
    "Parent": "Recipe",
    "Child": "Name"
  },
  {
    "ID": 18,
    "ParentEntityID": 13,
    "ChildEntityID": 14,
    "Deleted": false,
    "Parent": "Recipe",
    "Child": "Recipe Ingredient"
  },
  {
    "ID": 19,
    "ParentEntityID": 13,
    "ChildEntityID": 15,
    "Deleted": false,
    "Parent": "Recipe",
    "Child": "Recipe Direction"
  },
  {
    "ID": 22,
    "ParentEntityID": 12,
    "ChildEntityID": 11,
    "Deleted": false,
    "Parent": "Contact",
    "Child": "Name"
  }
]

这里有一些问题

  1. 如果我们从最顶层的元素开始,我们是否只递归显示子元素?
  2. 如果一个子元素有一个父元素,而这个父元素是通向该子实体的另一条路径怎么办?
  3. 通向不同顶级实体的路径是否也应该显示?
  4. 如果在此过程中,这些父实体有其他子分支,我们是否再次从问题 #2 开始解决它们?
  5. 如果我们从一个实体(例如Name)开始,它有两条路径(向上到 Recipe,向上到 Contact),我们是否都显示?
  6. 这是否取决于某种抽象,例如关系存在的“领域”?

这是我的回答

  1. 如果我们从最顶层的元素开始,我们是否只递归显示子元素?
  2. 如果一个子元素有一个父元素,而这个父元素是通向该子实体的另一条路径怎么办?目前,也显示该路径。
  3. 通向不同顶级实体的路径是否也应该显示?
  4. 如果在此过程中,这些父实体有其他子分支,我们是否再次从问题 #2 开始解决它们?
  5. 如果我们从一个实体(例如Name)开始,它有两条路径(向上到Recipe和向上到Contact),我们是否都显示?
  6. 这是否取决于某种抽象,例如关系存在的“领域”?未来功能

为了测试这个,我将添加另外两个实体定义,“Task”和“Subtask”,它们之间存在父子关系,这会添加到上面的 JSON 中

{
    "ID": 23,
    "ParentEntityID": 17,
    "ChildEntityID": 18,
    "Deleted": false,
    "Parent": "Task",
    "Child": "Subtask"
  }

不幸的是,我上述的答案导致了一个复杂的实现。

实现

public List<TopLevelEntityRelationship> GetHierarchy
(IConfiguration cfg, ITableService ts, int entityId)
{
  var models = GetAllDefinitions(cfg, ts).SerializeTo<EntityRelationship>();
  var hierarchy = FindTopLevelParents(models);
  var flattenedHierarchy = new List<int>[hierarchy.Count];
  var inUseHierarchies = new TopLevelEntityRelationship[hierarchy.Count];

  // Get the hierarchy for each top-level entity and the 
  // flattened collection of parent/child ID's.
  hierarchy.ForEachWithIndex((h, idx) =>
  {
    // We start with the top level parent ID and add the child ID's.
    var flatIds = new List<int>() { h.ParentEntityID };
    PopulateChildrenOfParent(h, models, h.ParentEntityID, flatIds);
    flattenedHierarchy[idx] = flatIds;
  });

  // Which hierarchies are in use given the entityID passed in?
  flattenedHierarchy.ForEachWithIndex((f, idx) =>
  {
    if (f.Any(id => id == entityId))
    {
      inUseHierarchies[idx] = hierarchy[idx];
    }
  });

  // Which heirarchies intersect the ones in use?
  // Once we add an intersected hierarchy, 
  // we have to also check what hierarchies it also intersects.
  inUseHierarchies.ForEachWithIndex((h, idx) => 
    GetIntersections(idx, hierarchy, flattenedHierarchy, inUseHierarchies), h => h != null);

  var actualInUse = inUseHierarchies.Where(h => h != null).ToList();

  return actualInUse;
}

private void GetIntersections(
                 int hidx, 
                 List<TopLevelEntityRelationship> hierarchy, 
                 List<int>[] flattenedHierarchy, 
                 TopLevelEntityRelationship[] inUseHierarchies)
{
  flattenedHierarchy.ForEachWithIndex((f, idx) =>
  {
    // Ignore self and already in-use hierarchies, 
   // so we only test a hierarchy not currently in use.
    if (idx != hidx && inUseHierarchies[idx] == null)
    {
      // If there are any intersections of entity ID's 
     // between an unused hierarchy and the one that is already in use...
      if (flattenedHierarchy[idx].Intersect(flattenedHierarchy[hidx]).Any())
      {
        // And we haven't already added it to the collection of in-use (object reference)...
        if (!inUseHierarchies.Where(h => h != null).Any(h => h == hierarchy[idx]))
        {
          // Then add this hierarchy, as it has an entity 
         // that intersects with an already in-use hierarchy.
          inUseHierarchies[idx] = hierarchy[idx];

          // Recurse, so we test the newly added hierarcy to see what intersections it may have.
          GetIntersections(idx, hierarchy, flattenedHierarchy, inUseHierarchies);
        }
      }
    }
  });
}

private List<TopLevelEntityRelationship> FindTopLevelParents(List<EntityRelationship> models)
{
  // Top level entities are those entities that do not occur as children in other relationships.
  var parentEntityIds = models.DistinctBy(m => m.ParentEntityID).ToList();
  var topLevelParents = parentEntityIds.NotIn
                       (models, p => p.ParentEntityID, m => m.ChildEntityID);
  
  // model.First because we can have multiple parent-child relationships 
 // with the top level entity as the parent, and we don't care which one.
  var topLevelEntities = topLevelParents.Select(p => 
    new TopLevelEntityRelationship(models.First
       (m => m.ParentEntityID == p.ParentEntityID))).ToList();

  return topLevelEntities;
}

private void PopulateChildrenOfParent(IEntityRelationship parent, 
       List<EntityRelationship> models, int parentId, List<int> flatIds)
{
  // Given a parent, find all the relationships where this parent is referenced.
  parent.Children = models.Where(m => m.ParentEntityID == parentId).Select
                   (m => m.Clone()).ToList();
  flatIds.AddRange(parent.Children.Select(c => c.ChildEntityID));

  // Recurse into grandchildren...
  parent.Children.ForEach(c => PopulateChildrenOfParent(c, models, c.ChildEntityID, flatIds));
}

请注意使用.Select(m => m.Clone())将子对象映射到克隆的子对象

现在,无论我们是请求已知顶层父级还是层次结构中的子级

GET /relationship/hierarchy/13
GET /relationship/hierarchy/11

我们创建的食谱模型的结果是

[
  {
    "ID": 17,
    "Parent": "Recipe",
    "ParentEntityID": 13,
    "Children": [
      {
        "ID": 17,
        "Parent": "Recipe",
        "Child": "Name",
        "ParentEntityID": 13,
        "ChildEntityID": 11,
        "Children": []
      },
      {
        "ID": 18,
        "Parent": "Recipe",
        "Child": "Recipe Ingredient",
        "ParentEntityID": 13,
        "ChildEntityID": 14,
        "Children": []
      },
      {
        "ID": 19,
        "Parent": "Recipe",
        "Child": "Recipe Direction",
        "ParentEntityID": 13,
        "ChildEntityID": 15,
        "Children": []
      }
    ]
  },
  {
    "ID": 22,
    "Parent": "Contact",
    "ParentEntityID": 12,
    "Children": [
      {
        "ID": 22,
        "Parent": "Contact",
        "Child": "Name",
        "ParentEntityID": 12,
        "ChildEntityID": 11,
        "Children": []
      }
    ]
  }
]

请注意,我们总是会得到Contact实体,因为Name实体在食谱名称和联系人名称之间存在交集。

反之,如果我们请求TaskSubtask的层次结构

GET /relationship/hierarchy/17
GET /relationship/hierarchy/18

我们只得到

[
  {
    "ID": 23,
    "Parent": "Task",
    "ParentEntityID": 17,
    "Children": [
      {
        "ID": 23,
        "Parent": "Task",
        "Child": "Subtask",
        "ParentEntityID": 17,
        "ChildEntityID": 18,
        "Children": []
      }
    ]
  }
]

Task/Subtask层次结构和RecipeContact层次结构中的实体之间没有交集。

返回特定父/子记录的子/父记录

在继续使用 UI 之前,我们需要一些额外的端点

  • 返回特定父级的子记录
  • 返回特定子级的父记录

我们需要这些端点的原因是

  • 当用户选择父记录时,我们可以显示每个子关系实体的所有关联子记录。
  • 当用户选择子记录时,我们可以显示每个父关系实体的关联(通常是一个)父记录。

两个端点都返回记录实例,作为预期的“枢轴”记录。实现并未针对性能进行优化!此初始通过只是为了让某些东西能够运行。

返回父级的所有子记录

/// <summary>
/// Returns the child records, grouped by child entity, of the specified parent instance ID.
/// </summary>
[HttpGet("instance/parent/{parentInstanceId}")]
public object GetChildRecordsOfParent(int parentInstanceId)
{
  var childRelationships = rls.GetChildInstancesOfParent(cfg, ts, parentInstanceId);
  var childRecords = new Dictionary<string, List<Record>>();

  // Create the lists for each unique child.
  childRelationships
    .Select(kvp => kvp[Constants.CHILD].ToString())
    .Distinct()
    .ForEach(child => childRecords[child] = new List<Record>());

  // This is not very efficient because we're doing a query per child ID.
  childRelationships.ForEach(rel =>
  {
    var childEntityName = rel[Constants.CHILD].ToString();
    var rec = rs.GetSingle(cfg, ts, childEntityName, 
             rel[Constants.CHILD_ENTITY_INSTANCE_ID].ToInt());
    childRecords[childEntityName].Add(rec);
    });

  return ser.Serialize(childRecords);
}

返回子级的所有父记录

这与之前的代码是镜像——可以避免代码重复,但是当我处理RelationshipInstance表与枢轴代码一起所需的连接时,两个 API 端点最终都会被重写。

/// <summary>
/// Returns the parent records, grouped by parent entity, of the specified child instance ID.
/// </summary>
[HttpGet("instance/child/{childInstanceId}")]
public object GetParentRecordsOfChild(int childInstanceId)
{
  var parentRelationships = rls.GetParentInstancesOfChild(cfg, ts, childInstanceId);
  var parentRecords = new Dictionary<string, List<Record>>();

  // Create the lists for each unique child.
  parentRelationships
    .Select(kvp => kvp[Constants.PARENT].ToString())
    .Distinct()
    .ForEach(parent => parentRecords[parent] = new List<Record>());

    // This is not very efficient because we're doing a query per child ID.
    parentRelationships.ForEach(rel =>
    {
      var parentEntityName = rel[Constants.PARENT].ToString();
      var rec = rs.GetSingle(cfg, ts, parentEntityName, 
               rel[Constants.PARENT_ENTITY_INSTANCE_ID].ToInt());
      parentRecords[parentEntityName].Add(rec);
    });

  return ser.Serialize(parentRecords);
}

使用示例

GET /RecordRelationship/instance/parent/19

这会返回 ID 为 19 的 Recipe 实例(鳄梨酱食谱)的子实例

{
  "Recipe Ingredient": [
    {
      "ID": 21,
      "Ingredient": "Avocado",
      "Quantity": "1"
    }
  ],
  "Recipe Direction": [
    {
      "ID": 23,
      "Step #": "1",
      "Instruction": "Mash avocado"
    }
  ]
}

GET /RecordRelationship/instance/child/22

这会返回鹰嘴豆泥食谱中配料“鹰嘴豆”的父实例(在本例中为一个)

{
  "Recipe": [
    {
      "ID": 20,
      "Recipe Name": "Hummos",
      "Culture": "Middle East",
      "Preparation Time": "",
      "Cooking Time": ""
    }
  ]
}

实现 UI

对于通用编辑工具,根据我们当前可用的信息,布局将动态创建为基于深度的“行”网格

  • 第 1 行包含顶级实体的网格。在上面的 JSON 示例中,这将只是“Recipe”。
  • 第 2 行包含子网格。在上面的 JSON 示例中,这将是“Name”、“食谱配料”和“食谱制作步骤”。
  • 第 3 行包含孙子网格。在上面的 JSON 示例中,没有孙子。
  • 等等。

我们将用于 UI 的端点是/recordRelationship。在左侧,我们可以选择要“开始”的实体,也就是说,我们希望看到这些实体中的哪些实体所有实例?请注意,尚未实现分页,因此不要尝试通过创建数百个实体实例来对系统进行压力测试!无论如何,这样做的目的是 UI 将显示相同的结构,但最初加载数据的网格取决于所选实体。

在下面的截图中,我填写了两个食谱,并且我还删除了与“Name”实体的关系,并创建了一个“Recipe Source”实体。我还删除了一些与本次演示无关的其他实体。所有编辑都是通过 UI 完成的——不再有幕后的 SQL 或 Postman 调用了!鹰嘴豆泥食谱是凭记忆制作的,所以不要在家尝试——我确信配料的量不对。首先,我们看到通常的实体列表

父子正向查找

点击Recipe,我们看到(我故意缩小了浏览器窗口的宽度,以尝试符合截图指南)

请注意我们的两个食谱是如何填充的。点击任何实体都会自动填充该实体的记录。另请记住,此 UI 完全是从实体关系定义自动生成的,并且列也由实体字段定义确定。

当我点击一个食谱时,我看到该食谱的配料、制作步骤和来源的记录自动填充(垂直截断)

父子反向查找

现在更有趣的是。让我们从配料开始。我们看到了所有食谱的配料

我们搜索“Garlic”,然后看到大蒜在两个地方使用

如果我点击其中一个,我就会看到它所用的食谱

而其他子元素的记录,因为此特定配料只有一个父元素,会自动填充。在第三部分中,我将更进一步引入“key”字段——其理念是您可以按唯一键字段值进行筛选,这样“Garlic”只出现一次,当您点击它时,所有引用该子实体实例(在本例中为配料)的父实体(在本例中为食谱)都会显示在父网格中。同样,您可以对特定来源的所有食谱进行反向查找,例如。

当前 UI 实现的一个警告是,我尚未测试它与多个父子关系的情况——换句话说,三个或更多级别关系未经测试,并且当我编写内置 UI 时,我认为目前存在一些限制。

附加功能

我在此迭代中做了一些额外的事情。

Swagger API 文档

此时,API 已经变得足够复杂,值得拥有一个 API 页面。使用微软的教程 ASP.NET Core Swashbuckle 入门,添加 Swagger 文档非常简单。所以现在,/swagger端点显示 API,您可以随意使用它。例如

我们看到

太棒了!

序列化服务

尽管 Swagger 在格式化 JSON 返回方面做得很好,但我仍然发现自己只是将GET端点放入 Chrome。用黑白显示 JSON,并且不必滚动,对我来说更容易看,所以我添加了一个序列化服务来格式化返回的 JSON。

public class PrettyFormat : ISerializationService
{
  public object Serialize(object data)
  {
    // For debugging, this is a lot easier to read.
    string json = JsonConvert.SerializeObject(data, Formatting.Indented);
    var cr = new ContentResult() 
            { Content = json, ContentType = "text/json", StatusCode = 200 };

    return cr;
  }
}

我猜这应该在自定义输出格式器中处理,类似于此处描述的,但我尚未实现services.AddMvc,所以这目前不是一个选项。然后,在 .NET Core 3 中,有三种新的实现方式,我的“天哪,这对于如此简单的事情来说太复杂了”的反应就出现了。但话说回来,也许这正是我所寻找的,让 ASP.NET Core 提供我内置的页面,而不是我目前使用的笨拙方法。提醒自己——AddControllers似乎是我想要的东西。

顺便说一句,将所有依赖注入服务添加到控制器中开始变得令人恼火!有一篇帖子处理“太多依赖”问题,但作者没有提出解决方案。

首页

目前有几个内置网页,因此拥有一个主页很有用

结论

此时,即使使用简单的网格和内置UI,我们现在也拥有了一个可用的通用、元数据驱动(即“自适应”和“分层”)的“知识管理”工具。根据Visual Studio的代码指标

这已在不到 600 行的后端可执行代码中完成。就个人而言,我发现这令人印象深刻。更不用说这一切都是通过六个数据库表完成的。

第三部分将包含什么

第三部分不会像第二部分那样快速发布!我在实际发布第一部分时已经写完了第二部分的大部分内容,但第三部分并非如此!我可能会根据内容填充情况将第三部分分成更小的片段。

Bug 修复

如果实体没有任何可用于枢轴的字段,则public Records GetAll(IConfiguration cfg, ITableService ts, string entity)将失败。

在记录UI中删除记录将导致记录关系UI崩溃,因为该记录的关系实例未被删除。

UI 可能无法处理深度 > 2 的关系。

我不得不将 Recipe 实体的字段“Name”更改为“Recipe Name”,否则我会因为“Name”已经是EntityField表中的一个字段而收到 pivot 语句错误!

重构

  • 我忽略了对*.html*/*.js*/*.ts*文件加载方式的重构,所以这应该在第三部分中完成。
  • 循环关系实例约束
  • 自关系约束
  • 异步而非同步端点处理程序
  • 当父级有多个子级时,子级的顺序
  • 数据分页
  • 性能:在RelationshipInstance表与枢轴代码结合时,找出必要的连接

新功能

在第三部分中,我计划专注于 UI 生成,特别是第一部分图表中描述的额外实体和实体字段元数据,这将是

  • UI 的实体字段标签
  • 离散控件而非所有内容的网格
  • 与实体字段关联的查找
  • 比 jsGrid 更好看的东西

这还将涉及 .NET Core 3 中的“可插拔”服务,可能如此处所述,因为上述某些方面,例如格式/插值、验证和范围/查找,应作为插件服务处理,因为这些开始涉及到特定于应用程序的要求,并且通常不是静态算法和数据,而是基于其他数据上下文的动态数据。因此,对我来说,开始研究这些难题部分是有意义的

遥远的未来

除了第一部分中光荣图的主要功能外,我还记录了这些笔记

  • 关系的描述符
  • 关系的时间维度
  • 父子序数:1或多
  • 没有对重复的子名称、相同的父子名称或循环关系进行测试。
  • 如果当前关系实例集合中只有一个记录,则自动向上选择父层次结构和向下选择子层次结构。
  • 实体的逻辑分组——域。实体能否/如何跨域?

暂时就这些了!

历史

  • 2021年4月6日:初始版本
© . All rights reserved.