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

面向关系编程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (20投票s)

2011年12月12日

CPOL

14分钟阅读

viewsIcon

61291

downloadIcon

829

为《罗密欧与朱丽叶》元模型建模。

引言

在我上一篇文章《罗密欧与朱丽叶——让关系成为一等公民》中,我编写了一个应用程序,用于开发莎士比亚戏剧《罗密欧与朱丽叶》中角色和事件的实体关系元模型。在本文中,我将使用完全相同的应用程序来创建一个模式,用于描述如何对该模型进行建模。该模式当然是通用的,可用于任何信息的元模型。作为一个模式,它可以由物理数据库和/或 .NET DataSet 支持,这两者您都将在这里看到。我还创建了一个“超级用户”UI,用于输入元模型数据、导航关系,当然,我还会继续使用 GraphViz 来阐释模式以及元模型实例。希望在本文结束时,您能欣赏我这种独特的疯狂形式,即将表、字段和外键等概念抽象为实体、属性和关系等通用概念,并且您将了解到这样一个动态环境如何用于建模、数据挖掘和关系构建!

架构

关于实现,我将做一个有争议的决定——我们只需要两个主集合:EntityInstanceRelationshipInstance。是的,还有一些支持类,如 AttributeAudit,但我想指出的是,将所有实体类型(人、姓名、动作、地点等)放入单个集合的架构决策。另一种选择是创建单独的集合。后一种方法在代码生成、性能、索引等方面肯定有其优势。然而,为了简化起见,我想暂时搁置这些考虑,只使用两个集合,一个用于实体实例,一个用于关系实例。其缺点是,类型信息必须在每条记录中明确说明,而不是由集合类型(列表、表等)隐式确定。理想情况下,实现应与表示(模型 vs. 视图模型)解耦。我可能会在未来的文章中探讨使用类型化集合作为替代实现。

一条 EntityInstance 记录需要

  • 一个唯一的 ID
  • 对实体类型的引用

一条 RelationshipInstance 记录需要

  • 一个唯一的 ID
  • 对关系类型的引用
  • “实体 A”的 ID
  • “实体 B”的 ID

一条 EntityAttributeInstance 记录需要

  • 一个唯一的 ID
  • 对属性类型的引用
  • 对实体实例的引用
  • 属性值

一条 AttributeAudit 记录需要

  • 一个唯一的 ID
  • 对正在更改的 AttributeInstance 记录的引用
  • 谁创建/更改的
  • 何时创建/更改的
  • 之前的值(如果已更改)

使用同一个工具来可视化《罗密欧与朱丽叶》中的错综复杂的关系,我们也可以可视化上述模型

删除实体

通常,实体和关系永远不会被删除。关系会过期,这会影响关系的“状态”(死亡、宣告无罪、销毁等)。显然,在编辑数据时,能够执行硬删除是很有用的:输入了错别字、存在完全错误的记录、有意销毁信息等等。

  • 只有当实体满足以下条件时,用户才被允许删除它
    • 未在关系中被引用
    • 除了“创建”之外没有其他审计跟踪。
  • 如果实体在关系中被引用,用户必须确认这些关系也将被删除。用户可能还会被给予一个选项,以删除那些关系中引用的相关实体。
  • 如果实体有除“创建”以外的审计记录,用户必须确认该实体及其审计跟踪将被删除。
  • 删除实体可能需要“主管”权限或类似权限。

删除关系

关系实例仅在该关系中的实体存在时才存在。删除关系需要确认,因为我们正在破坏两个实体之间的关系。

更改关系实体引用

一旦创建,更改关系所引用的实体需要确认。这不会在审计中被跟踪。

孤立实体

这些是不存在于任何关系中的实体。查询所有未被关系引用的实体以获得此列表很简单。

创建数据库

我不喜欢手动做事,当我能编写一些自动化程序来完成我想要做的事情时。因此,沿着这个思路,给定上述实体关系模型,为必要的表创建数据库是直截了当的。另外,因为我想让这项工作尽可能具有可移植性,我在这些文章中使用的是 SQLite 数据库。添加对您喜欢的数据库提供程序的支持也相当容易。

在实例的属性中,很明显缺少了两样东西:定义外键关系所需的模式信息,以及数据类型。在 Attribute 类和 Relationship 类中添加必要的属性很容易,我们还需要能够为实体类型命名。

有了这个(现在是一个更完整的模型),就可以生成用于创建数据库的 SQL。此外,一个支持关系模式中所有属性集的完整模型是

完整尺寸的版本在此处。(哦,看,关系名称现在用大括号括起来了,而且我找到了如何在 Graphviz 标签中强制换行的方法:你必须指定“\\n”!)。

请注意包含了 ListPair 集合以及 RelationshipDescribedWith 属性——我不确定是否真的会使用该属性,但暂时先放在那里,其想法是关系本身可能具有属性,这些属性通过将关系类型与实体关联来定义。

创建表

SQLite 不支持在 “alter table” 命令中添加约束,例如外键。这很烦人,因为它意味着表必须以特定的顺序创建,以便 FK 可以被解析。此外,阅读文档后发现,除非您使用 “pragma” 命令显式启用它们,否则外键约束不会被强制执行。因此,我们稍后会处理这个问题,暂时依赖 .NET 的 DataSet 在用户编辑数据时强制执行约束。

不带约束地创建表是直截了当的。我实现了一个 IDataProvider 接口,这样我们就可以在不同的数据库实现中创建表,或者,正如您将在下面看到的,在内存中的 DataSet 中创建。与 SQLite 相关的关键代码是

public StringBuilder GetCreateTableSql(Entity entity)
{
  string comma = string.Empty;
  StringBuilder sb = new StringBuilder();
  sb.Append("create table ");
  sb.Append(entity.Name.Brace());
  sb.Append("(");

  entity.EntityAttributes.ForEach(a =>
  {
    sb.Append(comma);
    Attribute attr=Schema.Instance.GetAttribute(a.Name);
    sb.Append(a.FieldNameOrName.Brace());
    sb.Append(" ");
    sb.Append(dataTypeMap[attr.DataType]);

    if (a.IsPrimaryKey)
    {
      sb.Append(" PRIMARY KEY AUTOINCREMENT NOT NULL");
    }

    comma = ", ";
  });

  sb.Append(")");

  return sb;
}

创建 DataSet

为了初步填充数据,我想创建一个 DataSet 和一个“超级用户”UI,至少可以让我们开始填充数据。我的 IDataProvider 接口的一个单独实现创建了一个 DataSet

namespace DataSetDataProvider
{
  public class DataSetProvider : IDataProvider
  {
    protected DataSet dataSet;
    protected Dictionary<DataType, System.Type> 
              dataTypeMap = new Dictionary<DataType, System.Type>()
    {
      {DataType.Bool, typeof(bool)},
      {DataType.DateTime, typeof(System.DateTime)},
      {DataType.Int, typeof(int)},
      {DataType.String, typeof(string)},
    };

    public DataSet DataSet { get { return dataSet; } }

    public DataSetProvider()
    {
    }

    public void CreateDatabase(string name, bool replaceExisting)
    {
      dataSet = new DataSet(name);
    }

    public void CreateTables()
    {
      Schema.Instance.EntitiesContainer.ForEach(t => t.Entities.ForEach(e =>
      {
        DataTable dt = new DataTable(e.Name);
        DataColumn primaryKey = null;

        e.EntityAttributes.ForEach(attr =>
        {
          Attribute ropAttr = Schema.Instance.GetAttribute(attr.Name);
          DataColumn dc = new DataColumn(attr.FieldNameOrName, 
                                         dataTypeMap[ropAttr.DataType]);

          if (attr.IsPrimaryKey)
          {
            primaryKey = dc;
          }

          dt.Columns.Add(dc);
        });

        dt.PrimaryKey = new DataColumn[] { primaryKey };
        dataSet.Tables.Add(dt);
      }));
    }

    public void CreateConstraints()
    {
      Schema.Instance.RelationshipsContainer.ForEach(r=>r.Relationships.ForEach(rel=>
      {
        Validation.Validate(!string.IsNullOrEmpty(rel.ForeignKeyAttribute), 
          "A relationship requires designating the foreign key field for " + rel.EntityA);
        EntityAttribute attr = Schema.Instance.GetEntityAttribute(
                                      rel.EntityA, rel.ForeignKeyAttribute);
        string fkFieldName = attr.FieldNameOrName;
        DataColumn fkColumn = dataSet.Tables[rel.EntityA].Columns[fkFieldName];
        Validation.Validate(dataSet.Tables[rel.EntityB].PrimaryKey.Length > 0, 
          "No primary key has been set for the entity " + rel.EntityB);
        DataColumn pkColumn=dataSet.Tables[rel.EntityB].PrimaryKey[0];
        DataRelation dr = new DataRelation(rel.Name, pkColumn, fkColumn);
        dataSet.Relations.Add(dr);
      }));
    }
  }
}

开发用户界面

首先,我们将创建一个简单的用户界面,让我们选择任何实体作为编辑和导航父子关系的起点

在这个基本 UI 中需要添加三样东西

  1. 用于显示对外键 ID 更友好的查找功能(组合框)
  2. 导航到子级或父级选项
  3. 导航“栏”,显示我们如何在树中移动

第一项,显示用户友好的外键数据,有一个特殊的细微之处,由 RelationshipInstance 实体所说明,即该表引用了一个本身没有可供用户显示字段的表 (EntityInstance)——它仅由 ID 组成。为了解决这个问题,我创建了一个“Comment”字段,可用于向开发人员提供有关模型的信息,尽管具体的数据值本身存储在数据库中。这是我放入模型中的抽象级别所产生的副作用,“Comment”字段是支持该抽象的一种变通方法。

导航

我们可以很容易地填充主组合框和明细组合框,从而允许我们导航到主表和明细表。

获取主实体列表

/// <summary>
/// For the given entity, returns the master entities, the ones
/// referenced by FK's in the current entity. Duplicates are ignored.
/// </summary>
public List<string> GetMasterEntities(string entityName)
{
  List<string> ret = new List<string>();

  RelationshipsContainer.ForEach(r => r.Relationships.ForEach(rel =>
  {
    if (rel.EntityA == entityName)
    {
      // Does EntityA have an FK field to B?
      if (!String.IsNullOrEmpty(rel.ForeignKeyAttribute))
      {
        string masterEntity = rel.EntityB;

        if (!ret.Contains(masterEntity))
        {
          ret.Add(masterEntity);
        }
      }
    }
  }));

  return ret;
}

在上面的代码中,我们将 EntityA 视为主表,因此,如果存在一个到 EntityB 的关系,其中指定了外键字段(在 EntityA 中),那么我们就有了对从表的引用。您会注意到,我没有创建实体实例列表,而是创建了一个实体名称列表。我对此没有什么异议——并非所有时候都需要有对象到处飞。

获取明细实体列表

/// <summary>
/// For the given entity, returns the detail entities, the ones that
/// reference the current entity as FK's. Duplicates are ignored.
/// </summary>
public List<string> GetDetailEntities(string entityName)
{
  List<string> ret = new List<string>();

  RelationshipsContainer.ForEach(r => r.Relationships.ForEach(rel =>
  {
    if (rel.EntityB == entityName)
    {
        // Does EntityA have an FK to field B?
        if (!String.IsNullOrEmpty(rel.ForeignKeyAttribute))
        {
          string detailEntity = rel.EntityA;

          if (!ret.Contains(detailEntity))
          {
            ret.Add(detailEntity);
          }
        }
      }
    }));

  return ret;
}

这里情况正好相反:如果当 EntityB(被视为主表)与所需名称匹配时,关系为 EntityA 定义了一个外键字段,那么 EntityA 就是明细表。也许 EntityAEntityB 更好的名称实际上是“Detail”和“Master”!

追踪我们的导航

接下来,我希望能够追踪我在导航中的路径,并提供回溯的能力。在视觉上,我们将用户的导航显示如下

这都是 UI 行为,我们仍然需要实现核心功能。但首先,让网格显示组合框,这是我们核心功能所需的“佐料”。

外键查找

首先,我们需要创建一些数据,所以让我们从 EntityType 数据开始,使用我们的《罗密欧与朱丽叶》模式

在填充 AttributeType 表之后,我们现在可以填充 EntityAttributeTypes 表(请参考《罗密欧与朱丽叶》文章中的模式)

这是由以下代码处理的。请注意使用 DataView 对主表的显示字段值进行排序,并注意显示字段本身是由实体定义中的模式属性确定的。

protected void InitializeColumns()
{
  dgvModel.Columns.Clear();
  DataTable dt = dataSet.Tables[dgvModel.DataMember];

  foreach (DataColumn dc in dt.Columns)
  {
    bool isLookup = false;

    // Look at all the parent relations
    foreach (DataRelation dr in dt.ParentRelations)
    {
      // If the current column has a parent relation...
      if (dr.ChildColumns.Contains(dc))
      {
        DataView dvParent = new DataView(dr.ParentTable);
        string displayField = 
          Schema.Instance.GetEntity(dr.ParentTable.TableName).DisplayField;
        dvParent.Sort = displayField;

        DataGridViewComboBoxColumn col = new DataGridViewComboBoxColumn();
        col.DataSource = dvParent;
        col.ValueMember = dr.ParentTable.PrimaryKey[0].ColumnName;
        col.DisplayMember = displayField;

        col.HeaderText = dc.ColumnName;
        col.DataPropertyName = dc.ColumnName;
        col.ReadOnly = false;
        col.Width = 120;
        dgvModel.Columns.Add(col);

        isLookup = true;
        break;
      }
    }

    if (!isLookup)
    {
      DataGridViewTextBoxColumn col = 
              new DataGridViewTextBoxColumn();
      col.HeaderText = dc.ColumnName;
      col.DataPropertyName = dc.ColumnName;
      dgvModel.Columns.Add(col);
    }
  }
}

导航到选定的明细记录

现在,我们准备添加更多花哨的功能,以便可以导航到选定的明细记录。例如,从“实体类型”我们想要导航到“实体类型属性”,显示实体类型“Name”的所有属性

改为

请注意,导航文本框现在显示了限定符“[EntityTypeID=2]”。相关的代码如下

private void btnSelectedDetails_Click(object sender, EventArgs e)
{
  DataGridViewSelectedRowCollection selectedRows = dgvModel.SelectedRows;
  string orRow = String.Empty;
  string currentEntity = dgvModel.DataMember;
  string navToEntity = cbDetailTables.SelectedItem.ToString();
  StringBuilder qualifier = new StringBuilder();

  if (selectedRows.Count > 0)
  {
    foreach(DataGridViewRow gridRow in selectedRows)
    {
      string orRelationship = String.Empty;

      // Build qualifier: "[FK]=[value]"
      foreach (DataRelation dr in dataSet.Relations)
      {
        if ((dr.ChildTable.TableName == navToEntity) && 
                (dr.ParentTable.TableName == currentEntity))
        {
          qualifier.Append(orRow);
          qualifier.Append(orRelationship);
          qualifier.Append(dr.ChildColumns[0].ColumnName);
          qualifier.Append("=");
          string pkField = dr.ParentTable.PrimaryKey[0].ColumnName;
          DataRow row = ((DataRowView)gridRow.DataBoundItem).Row;
          qualifier.Append(row[pkField].ToString());
          orRelationship = " or ";
          orRow = String.Empty;
        }
      }

      orRow = " or ";
    }

    UpdateGrid(navToEntity);
    SetRowFilter(navToEntity, qualifier.ToString());
    ShowNavigateToDetail(navToEntity, qualifier.ToString());
  }
}

上述代码构造了一个限定符,其中明细表的外键 = 主表的主键值。请注意,上述代码处理了两个重要情况

  1. 用户在主表中选择了多行。
  2. 明细表有多个字段与主表存在关系。一个例子是 RelationshipInstance 表,它有三个独立的字段与 EntityInstance 表存在关系:EntityAID、EntityBID 和 DescribedWithEntityID。

一个同时涉及到这两种情况的例子是,从 EntityInstance 的多个选择导航到 RelationshipInstance 表,一个示例限定符看起来像这样:“<-- RelationshipInstance [EntityAID=2 or EntityBID=2 or DescribedWithEntityID=2 or EntityAID=1 or EntityBID=1 or DescribedWithEntityID=1]”。

我费了九牛二虎之力才弄清楚如何设置正确的默认 DataView。我尝试了这两个选项(我在各种网站上看到的)

// Doesn't work. Not the right view.
// dataSet.Tables[navToEntity].DefaultView.RowFilter = qualifier.ToString();

// Doesn't work. Not the right view.
// dataSet.DefaultViewManager.DataViewSettings[navToEntity].RowFilter = qualifier.ToString();

两者都不起作用,最后偶然发现了这个解决方案

protected void SetRowFilter(string entity, string qualifier)
{
  DataView dv = (DataView)((CurrencyManager)BindingContext[dataSet, entity]).List;
  dv.RowFilter = qualifier.ToString();
}

导航到选定的主记录

这本质上是反向过程,其中限定符被构造为主表的主键 = 明细表的外键值。

private void btnSelectedMasters_Click(object sender, EventArgs e)
{
  string currentEntity = dgvModel.DataMember;
  string navToEntity = cbMasterTables.SelectedItem.ToString();
  DataGridViewSelectedRowCollection selectedRows = dgvModel.SelectedRows;
  StringBuilder qualifier = new StringBuilder();
  string orRow = String.Empty;

  if (selectedRows.Count > 0)
  {
    foreach (DataGridViewRow gridRow in selectedRows)
    {
      string orRelationship = String.Empty;

      // Build qualifier: "[PK]=[value]"
      foreach (DataRelation dr in dataSet.Relations)
      {
        if ( (dr.ParentTable.TableName == navToEntity) && 
                  (dr.ChildTable.TableName==currentEntity))
        {
          qualifier.Append(orRow);
          qualifier.Append(orRelationship);
          string pkField = dr.ParentTable.PrimaryKey[0].ColumnName;
          qualifier.Append(pkField);
          qualifier.Append("=");
          DataRow row = ((DataRowView)gridRow.DataBoundItem).Row;
          string fkField = dr.ChildColumns[0].ColumnName;
          qualifier.Append(row[fkField].ToString());
          orRelationship = " or ";
          orRow = String.Empty;
        }
      }

      orRow = " or ";
    }

    UpdateGrid(navToEntity);
    SetRowFilter(navToEntity, qualifier.ToString());
    ShowNavigateToMaster(navToEntity, qualifier.ToString());
  }
}

向后导航

通过导航堆栈向后导航,只需解析导航记录并设置行过滤器

private void btnBack_Click(object sender, EventArgs e)
{
  string qualifier;
  string navItem = PopNavigation(out qualifier);
  UpdateGrid(navItem);
  SetRowFilter(navItem, qualifier);
}

现在来点真正时髦的东西

用一点了解我们模型的代码

protected void OnGraphRelationshipInstances(object sender, EventArgs args)
{
  if (dsp == null)
  {
    CreateDataSet();
  }

  DataSet dataSet = ((DataSetProvider)dsp).DataSet;

  if (dataSet.Tables["RelationshipInstance"].Rows.Count == 0)
  {
    OpenFileDialog ofd = new OpenFileDialog();
    ofd.Filter = "DataSet Files (.dataset)|*.dataset";
    ofd.RestoreDirectory = true;
    DialogResult ret = ofd.ShowDialog();

    if (ret == DialogResult.OK)
    {
      dataSet.Clear();
      dataSet.ReadXml(ofd.FileName, XmlReadMode.IgnoreSchema);
    }
  }

  StringBuilder sb = new StringBuilder();
  sb.AppendLine("digraph G {");
  List<string> entityInstanceList = new List<string>();

  foreach (DataRow row in dataSet.Tables["RelationshipInstance"].Rows)
  {
    int entityAID = Convert.ToInt32(row["EntityAID"]);
    int entityBID = Convert.ToInt32(row["EntityBID"]);
    DataRow entityAInstanceRow = dataSet.Tables["EntityInstance"].Rows.Find(entityAID);
    DataRow entityBInstanceRow = dataSet.Tables["EntityInstance"].Rows.Find(entityBID);
    string entityADescr = entityAInstanceRow["Comment"].ToString();
    string entityBDescr = entityBInstanceRow["Comment"].ToString();
    int entityATypeID = Convert.ToInt32(entityAInstanceRow["EntityTypeID"]);
    int entityBTypeID = Convert.ToInt32(entityBInstanceRow["EntityTypeID"]);
    DataRow entityATypeRow = dataSet.Tables["EntityType"].Rows.Find(entityATypeID);
    DataRow entityBTypeRow = dataSet.Tables["EntityType"].Rows.Find(entityBTypeID);

    // Ignore the Name entity for simplicity.
    if ((entityATypeRow["Name"].ToString() != "Name") &&
       (entityBTypeRow["Name"].ToString() != "Name"))
    {
      string label = String.Empty;

      if (!entityInstanceList.Contains(entityADescr))
      {
        entityInstanceList.Add(entityADescr);
        sb.Append(entityADescr.Quote() + "\r\n");
      }

      if (!entityInstanceList.Contains(entityBDescr))
      {
        entityInstanceList.Add(entityBDescr);
        sb.Append(entityBDescr.Quote() + "\r\n");
      }

      if (row["PairItemID"] != DBNull.Value)
      {
        int pairItemID = Convert.ToInt32(row["PairItemID"]);
        label = dataSet.Tables["PairItem"].Rows.Find(pairItemID)["Comment"].ToString();
      }

      sb.Append(entityADescr.Quote() + " -> " + entityBDescr.Quote() + 
               " [label=" + label.Quote() + "]\r\n");
    }
  }

  sb.AppendLine("}");
  Generate(sb);
}

我们可以生成一个*实例*关系模型。这让我们能够可视化数据实例相对于它们的关系

这只是整个实例图的一个子集,您可以在这里查看。

另一件时髦的事

我们还可以通过筛选特定的实体实例来检查实体之间的关系。例如,通过选择仆人之间的打斗

我们得到了这张可爱的图表

或者,通过选择罗密欧

希望您能看到这个架构的用处!

那么……

数据库呢?

我最初的想法是将模型数据持久化到数据库中,因此提到了 SQLite。但事实证明,直接持久化 DataSet 本身是微不足道的,所以我目前甚至没有使用数据库!我将把收尾工作留到以后再做。

DevExpress?

让我们面对现实吧,.NET 的 DataGrid 很丑。我最初也曾考虑利用 GridControl 的下钻功能,这样我就不必处理导航的细节。但事实证明,这并不是我想要的 UI 行为。此外,RelationshipInstance 表导致了一个特别奇怪的问题。因为有三个到 EntityInstance 的关系,GridControl 的下钻功能创建了三个标签页,每个关系一个。这真的不是我想要的——我想要一个 RelationshipInstance 记录的单一视图,无论对所选 EntityInstance 的引用出现在 EntityAID、EntityBID 还是 DescribedWithEntityID 字段中。因此,在重新思考了 UI 的目标以及如何实现最佳方案后,我选择了一个单一的网格,并回归到 .NET 的网格控件,从而避免了强迫下载代码的人也必须拥有 DevExpress。在我看来,这是一个在各方面都双赢的局面!

自定义 UI?

目前,我有一个“超级用户”UI,这完全不是我想呈现给最终用户的界面。最终用户的 UI 应该看起来和感觉上像是对模型的直接实现,而不是我现在所实现的元模型 UI。但是,这是实现最终用户界面的必要步骤,并且也是在元模型级别上细查数据的宝贵工具。

审计跟踪?

那将在我做自定义 UI 时实现。

我想改进的地方?

有几件事让我感到困扰。

  1. 首先是那些只管理关系的实体中的“Comment”字段——它们只包含外键。理想情况下,我希望能够指定一个 Person 实体的显示值由其与 Name 表的关系以及 Name 字段中的值来确定。就目前而言,很容易忘记创建一个 Name 实例以及 Person、Place 或 Action 实例与 Name 实例之间的关系。
  2. 如果能组合显示字段就好了。例如,在 PairItem 表中,我希望能够从 EntityAValue 和 EntityBValue 字段自动组合显示值。
  3. 查找——PairItemID 字段,渲染为组合框,实际上应该由特定关系类型允许的 Pair 类型来限定。还有其他类似的情况。

为什么?

你可能会问自己,这有什么用?毕竟,我正在摧毁传统的数据库开发方法,将表、字段和外键关系的概念抽象出来。原因在于:通过实现这种级别的抽象,我可以动态地创建与新数据的新关系,从而使应用程序能够在不必回头修改应用程序的“硬编码”方面(如实体模型、数据库模式、用户界面等)的情况下进行开发。举个例子(有些牵强),假设你有一个患者数据库,其中包含已开给患者的药物列表。在某个时候,你希望能够轻松地连接到一个药物数据库,以便可以轻松查找药物的用途、与其他处方药的相互作用等等。理论上,有了这样的模型,你只需添加必要的类型、属性和实例,并创建“已开药物”和“处方药信息”之间的关系,瞧,你仅仅通过添加新数据和新关系就增强了应用程序。现在,假设你还想包含草药疗法和处方药之间的相互作用。过程相同。这实现了我最初设定的目标——让关系成为一等公民,换句话说,关系被视为与其他任何数据一样。

运行演示

要探索演示的功能,请启动它并加载文件“rop.model

请注意,您不能使用模型功能(在“Model”菜单下)来编辑“romeo_and_juliet.model”模型。“Model”菜单下的所有功能都假定您正在处理一个数据库模式定义,其中关系中的主键字段和外键字段都已标识。

因此,加载定义了模式的“rop.model”后,从“Model”菜单中选择“View Data”。在此对话框中,打开文件 romeo_and_juliet.dataset。然后您可以探索我放入模型中的数据。RelationshipInstance 实体是一个很好的起点

更改模型

如果您在第一个屏幕截图中编辑模型

  • 关闭模型数据查看器(如果已打开)。
  • 从“Model”菜单中,选择“Create Data Set”。这将使用新模型更新 DataSet
  • 选择 View Data 并重新加载数据集。

参考文献

© . All rights reserved.