Ran Wei/MDE 系列
English
基于 Eclipse Epsilon 的模型驱动工程 — Ran Wei

综合项目:MiniJDL

端到端构建一个 JHipster 风格的实体语言:元模型、模型、验证、生成与转换。

4 小时4 个时段9 个可运行示例Epsilon Playground

完成后你能够

  • 用 Emfatic 元模型定义小型实体领域语言。
  • 从模型构建、验证并生成三种格式的产物。
  • 将模型转换为关系模型并评估结果。

开始之前

建议先学习第 1 至 9 模块。除特别说明外,示例均可在 Playground 中运行。

下载本模块全部示例(zip)

目录

学习计划

4 小时

时间为估计值,包含运行示例的时间。进度保存在当前浏览器,中英文版本共享。

时段 160 分钟
MiniJDL 项目:定义语言
时段 260 分钟
构建、验证并生成 SQL、Java 与 HTML
时段 360 分钟
转换并评估关系模型
时段 460 分钟
练习
1

MiniJDL 项目说明

结课项目要求你设计一种小型语言,用于描述包含实体、字段和有向关系的应用。从同一个模型出发,检查领域规则,生成 SQL DDL、Java 数据类和 HTML 文档,再把实体转换成关系模型。这里的 MiniJDL 是教学语言,并非 JHipster 的 JDL;它生成的 Java 类是简单的数据声明,而不是可运行的 Web 应用。

从实体描述生成多个相互协调的产物这一思路改编自 Dimitris Kolovos 的 Minimal JHipster JDL Monolith Example(2021)。本节的 MiniJDL 元模型、商店模型和 Epsilon 程序均为教程编写。你可以在本地使用第 10 模块下载包,或在 Playground 中打开各示例。

目标是建立可重复的流程:元模型定义语言,模型记录应用,EVL 拒绝不良数据,EGL/EGX 生成可读产物,ETL 通过明确的映射创建另一种模型。每一步都留下可检查的证据,再决定是否信任下一步。

2

定义语言

打开第一个示例中的 minijdl.emf。Application 包含 Entity;Entity 包含 Field 和 Relation。字段的 FieldType 可以是 STRING、INTEGER 或 BOOLEAN。关系引用目标实体。val 明确所有权,ref 表示关系指向一个已有实体,而不是拥有它的副本。

Flexmi 模型描述 Shop:Customer 有 email 和 active,Product 有 title 和 price,Purchase 有 quantity,并与另外两个实体建立关系。EOL 查询打印数量,便于确认模型按预期加载。

示例检查 MiniJDL 语言与模型在 Playground 中打开 ↗
inspect.eol
var app = Application.all.first();
("Application: " + app.name).println();
for (e in app.entities) {
  var fieldNoun = "fields";
  if (e.fields.size() = 1) { fieldNoun = "field"; }
  var relationNoun = "relations";
  if (e.relations.size() = 1) { relationNoun = "relation"; }
  (e.name + ": " + e.fields.size() + " " + fieldNoun + ", " + e.relations.size() + " " + relationNoun).println();
}
minijdl.emf
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
  attr String[1] name;
  val Entity[*] entities;
}
class Entity {
  attr String[1] name;
  val Field[*] fields;
  val Relation[*] relations;
}
class Field {
  attr String[1] name;
  attr FieldType type;
}
class Relation {
  attr String[1] name;
  ref Entity target;
}
shop.flexmi
<?nsuri minijdl?>
<application name="Shop">
  <entity name="Customer">
    <field name="email" type="STRING"/>
    <field name="active" type="BOOLEAN"/>
  </entity>
  <entity name="Product">
    <field name="title" type="STRING"/>
    <field name="price" type="INTEGER"/>
  </entity>
  <entity name="Purchase">
    <field name="quantity" type="INTEGER"/>
    <relation name="customer" target="Shop.Customer"/>
    <relation name="product" target="Shop.Product"/>
  </entity>
</application>
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
Application: Shop
Customer: 2 fields, 0 relations
Product: 2 fields, 0 relations
Purchase: 1 field, 2 relations

元模型能表达结构,却不能表达所有业务规则。例如,它并不阻止同一实体中的两个字段同名。该规则应放在验证中。若以后加入新的字段类型,就必须同时更新元模型中的枚举和各生成器中的类型映射。

3

构建并验证模型

下一个模型故意包含两个 Customer.email 字段。EVL 检查实体名称唯一、每个实体内部的字段名称唯一,并检查关系有目标。重复字段产生两条结果,因为约束会对两个违规字段分别执行。这是验证结果,而不是解析错误。

示例检测重复字段在 Playground 中打开 ↗
rules.evl
context Entity {
  constraint UniqueEntityName {
    check: Entity.all.select(e | e.name = self.name).size() = 1
    message: "Duplicate entity: " + self.name
  }
}
context Field {
  constraint UniqueFieldName {
    check: self.eContainer().fields.select(f | f.name = self.name).size() = 1
    message: "Duplicate field: " + self.eContainer().name + "." + self.name
  }
}
context Relation {
  constraint HasTarget {
    check: not self.target.isUndefined()
    message: "Missing relation target: " + self.name
  }
}
其他文件: shop.flexmi, minijdl.emf
shop.flexmi
<?nsuri minijdl?>
<application name="Shop">
  <entity name="Customer">
    <field name="email" type="STRING"/>
    <field name="email" type="BOOLEAN"/>
  </entity>
  <entity name="Product">
    <field name="title" type="STRING"/>
    <field name="price" type="INTEGER"/>
  </entity>
  <entity name="Purchase">
    <field name="quantity" type="INTEGER"/>
    <relation name="customer" target="Shop.Customer"/>
    <relation name="product" target="Shop.Product"/>
  </entity>
</application>
minijdl.emf
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
  attr String[1] name;
  val Entity[*] entities;
}
class Entity {
  attr String[1] name;
  val Field[*] fields;
  val Relation[*] relations;
}
class Field {
  attr String[1] name;
  attr FieldType type;
}
class Relation {
  attr String[1] name;
  ref Entity target;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
Error [UniqueFieldName]: Duplicate field: Customer.email
Error [UniqueFieldName]: Duplicate field: Customer.email

生成交付产物前先修复重复字段。练习 4 的参考解答在修正后的模型上运行相同规则。真实项目还应酌情检查标识符语法、目标语言保留字、关系名与字段名冲突、必填字段和循环。通过这组简短规则,并不代表其他策略也已得到保证。

4

生成 SQL、Java 与 HTML

EGL 把商店模型映射为 SQL DDL。每个实体变成一张带 id 列的表;字段映射为 SQL 列类型;关系变成 _id 列和外键声明。检查生成的 SQL,确认 Purchase 按名称引用 Customer 和 Product。模板使用简单的通用 SQL 风格;生产环境必须明确选择数据库专属类型和命名规则。

示例生成 SQL DDL在 Playground 中打开 ↗
generate.egl
[%
var ddl = "-- MiniJDL schema for " + Application.all.first().name + "\n";
for (e in Entity.all) {
  ddl += "CREATE TABLE " + e.name + " (\n  id INTEGER PRIMARY KEY";
  for (f in e.fields) {
    var sqlType = "INTEGER";
    if (f.type.toString() = "STRING") { sqlType = "VARCHAR(255)"; }
    else if (f.type.toString() = "BOOLEAN") { sqlType = "BOOLEAN"; }
    ddl += ",\n  " + f.name + " " + sqlType;
  }
  for (r in e.relations) {
    ddl += ",\n  " + r.name + "_id INTEGER";
  }
  for (r in e.relations) {
    ddl += ",\n  FOREIGN KEY (" + r.name + "_id) REFERENCES " + r.target.name + "(id)";
  }
  ddl += "\n);\n";
}
%][%=ddl%]
其他文件: shop.flexmi, minijdl.emf
shop.flexmi
<?nsuri minijdl?>
<application name="Shop">
  <entity name="Customer">
    <field name="email" type="STRING"/>
    <field name="active" type="BOOLEAN"/>
  </entity>
  <entity name="Product">
    <field name="title" type="STRING"/>
    <field name="price" type="INTEGER"/>
  </entity>
  <entity name="Purchase">
    <field name="quantity" type="INTEGER"/>
    <relation name="customer" target="Shop.Customer"/>
    <relation name="product" target="Shop.Product"/>
  </entity>
</application>
minijdl.emf
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
  attr String[1] name;
  val Entity[*] entities;
}
class Entity {
  attr String[1] name;
  val Field[*] fields;
  val Relation[*] relations;
}
class Field {
  attr String[1] name;
  attr FieldType type;
}
class Relation {
  attr String[1] name;
  ref Entity target;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
-- MiniJDL schema for Shop
CREATE TABLE Customer (
  id INTEGER PRIMARY KEY,
  email VARCHAR(255),
  active BOOLEAN
);
CREATE TABLE Product (
  id INTEGER PRIMARY KEY,
  title VARCHAR(255),
  price INTEGER
);
CREATE TABLE Purchase (
  id INTEGER PRIMARY KEY,
  quantity INTEGER,
  customer_id INTEGER,
  product_id INTEGER,
  FOREIGN KEY (customer_id) REFERENCES Customer(id),
  FOREIGN KEY (product_id) REFERENCES Product(id)
);

EGX 针对每个实体调用一次 EGL 模板,得到 Customer.java、Product.java 和 Purchase.java。模板将字段类型映射到 Java 类型,将关系映射为有类型的引用。这些文件展示生成过程;Playground 不会编译它们,关系持久化也不在此小示例范围内。

示例生成 Java 实体类在 Playground 中打开 ↗
generate.egx
rule Entity2Java
  transform e : Entity {
  template: "template.egl"
  target: e.name + ".java"
}
template.egl
public class [%=e.name%] {
  public int id;
[% for (f in e.fields) { %]  public [% if (f.type.toString() = "STRING") { %]String[% } else if (f.type.toString() = "BOOLEAN") { %]boolean[% } else { %]int[% } %] [%=f.name%];
[% } %][% for (r in e.relations) { %]  public [%=r.target.name%] [%=r.name%];
[% } %]}
其他文件: shop.flexmi, minijdl.emf
shop.flexmi
<?nsuri minijdl?>
<application name="Shop">
  <entity name="Customer">
    <field name="email" type="STRING"/>
    <field name="active" type="BOOLEAN"/>
  </entity>
  <entity name="Product">
    <field name="title" type="STRING"/>
    <field name="price" type="INTEGER"/>
  </entity>
  <entity name="Purchase">
    <field name="quantity" type="INTEGER"/>
    <relation name="customer" target="Shop.Customer"/>
    <relation name="product" target="Shop.Product"/>
  </entity>
</application>
minijdl.emf
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
  attr String[1] name;
  val Entity[*] entities;
}
class Entity {
  attr String[1] name;
  val Field[*] fields;
  val Relation[*] relations;
}
class Field {
  attr String[1] name;
  attr FieldType type;
}
class Relation {
  attr String[1] name;
  ref Entity target;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
--- Customer.java ---
public class Customer {
  public int id;
  public String email;
  public boolean active;
}
--- Product.java ---
public class Product {
  public int id;
  public String title;
  public int price;
}
--- Purchase.java ---
public class Purchase {
  public int id;
  public int quantity;
  public Customer customer;
  public Product product;
}

HTML 模板提供同一模型的第三种视图,列出实体、字段和关系数量。打开生成输出,把名称与 SQL、Java 产物比较。如果某个生成器漏掉实体,共同的源模型有助于发现差异。

示例生成 HTML 文档在 Playground 中打开 ↗
generate.egl
<h1>[%=Application.all.first().name%] entities</h1>
[% for (e in Entity.all) { %]<section><h2>[%=e.name%]</h2><ul>
[% for (f in e.fields) { %]<li>[%=f.name%]: [%=f.type%]</li>
[% } %]</ul><p>Relations: [%=e.relations.size()%]</p></section>
[% } %]
其他文件: shop.flexmi, minijdl.emf
shop.flexmi
<?nsuri minijdl?>
<application name="Shop">
  <entity name="Customer">
    <field name="email" type="STRING"/>
    <field name="active" type="BOOLEAN"/>
  </entity>
  <entity name="Product">
    <field name="title" type="STRING"/>
    <field name="price" type="INTEGER"/>
  </entity>
  <entity name="Purchase">
    <field name="quantity" type="INTEGER"/>
    <relation name="customer" target="Shop.Customer"/>
    <relation name="product" target="Shop.Product"/>
  </entity>
</application>
minijdl.emf
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
  attr String[1] name;
  val Entity[*] entities;
}
class Entity {
  attr String[1] name;
  val Field[*] fields;
  val Relation[*] relations;
}
class Field {
  attr String[1] name;
  attr FieldType type;
}
class Relation {
  attr String[1] name;
  ref Entity target;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
<h1>Shop entities</h1>
<section><h2>Customer</h2><ul>
<li>email: STRING</li>
<li>active: BOOLEAN</li>
</ul><p>Relations: 0</p></section>
<section><h2>Product</h2><ul>
<li>title: STRING</li>
<li>price: INTEGER</li>
</ul><p>Relations: 0</p></section>
<section><h2>Purchase</h2><ul>
<li>quantity: INTEGER</li>
</ul><p>Relations: 2</p></section>
5

转换为关系模型

文本生成不是唯一途径。ETL 程序把 Application 映射为 Schema,Entity 映射为 Table,Field 映射为 Column。目标遵循自己的 relational.emf 元模型,因此可以继续验证或交给其他生成器。初始映射故意省略关系,便于观察信息损失。

示例将实体映射到关系表在 Playground 中打开 ↗
transform.etl
rule Application2Schema
  transform a : Source!Application
  to s : Target!Schema {
  s.name = a.name;
  s.tables ::= a.entities;
}
rule Entity2Table
  transform e : Source!Entity
  to t : Target!Table {
  t.name = e.name;
  var id = new Target!Column;
  id.name = "id";
  id.sqlType = "INTEGER";
  t.columns ::= e.fields;
  t.columns.add(id);
}
rule Field2Column
  transform f : Source!Field
  to c : Target!Column {
  c.name = f.name;
  if (f.type.toString() = "STRING") { c.sqlType = "VARCHAR(255)"; }
  else if (f.type.toString() = "BOOLEAN") { c.sqlType = "BOOLEAN"; }
  else { c.sqlType = "INTEGER"; }
}
relational.emf
@namespace(uri="relational", prefix="relational")
package relational;
class Schema {
  attr String name;
  val Table[*] tables;
}
class Table {
  attr String name;
  val Column[*] columns;
  val ForeignKey[*] foreignKeys;
}
class Column {
  attr String name;
  attr String sqlType;
}
class ForeignKey {
  attr String name;
  attr String column;
  ref Table target;
}
其他文件: shop.flexmi, minijdl.emf
shop.flexmi
<?nsuri minijdl?>
<application name="Shop">
  <entity name="Customer">
    <field name="email" type="STRING"/>
    <field name="active" type="BOOLEAN"/>
  </entity>
  <entity name="Product">
    <field name="title" type="STRING"/>
    <field name="price" type="INTEGER"/>
  </entity>
  <entity name="Purchase">
    <field name="quantity" type="INTEGER"/>
    <relation name="customer" target="Shop.Customer"/>
    <relation name="product" target="Shop.Product"/>
  </entity>
</application>
minijdl.emf
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
  attr String[1] name;
  val Entity[*] entities;
}
class Entity {
  attr String[1] name;
  val Field[*] fields;
  val Relation[*] relations;
}
class Field {
  attr String[1] name;
  attr FieldType type;
}
class Relation {
  attr String[1] name;
  ref Entity target;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
Schema name="Shop"
  Table name="Customer"
    Column name="email" sqlType="VARCHAR(255)"
    Column name="active" sqlType="BOOLEAN"
    Column name="id" sqlType="INTEGER"
  Table name="Product"
    Column name="title" sqlType="VARCHAR(255)"
    Column name="price" sqlType="INTEGER"
    Column name="id" sqlType="INTEGER"
  Table name="Purchase"
    Column name="quantity" sqlType="INTEGER"
    Column name="id" sqlType="INTEGER"

将目标树与源模型比较:三张表包含五个已声明字段的列,以及三个生成的 id 列,却没有外键。ETL 规则在 t.columns ::= e.fields 后添加 id,因为该赋值会替换先前的列集合。练习 6 加入关系键列和 Relation2ForeignKey 规则,并将其结果挂到正确的表下。转换成功运行不代表映射完整;应先确定目标必须满足的不变量,再判断工作是否完成。

6

评估并扩展解答

用下面的清单评估自己的 MiniJDL 变体:

  • 语言: 每个类、包含关系和引用是否都有用途?模型元素的所有权是否如预期?
  • 模型: 所有按名称编写的引用都能解析吗?不修改元模型,能否描述第二个不同的应用?
  • 验证: 无效名称和关系是否产生清楚的结果?修正后的模型是否通过?还有哪些规则尚未实现?
  • SQL: 所有实体、字段和关系键是否存在?输出对所选数据库是否有效?
  • Java: 预期的文件和类型是否存在?如需可编译代码,是否已编译并处理包名与导入?
  • HTML: 文档是否描述了与模型一致的实体和关系?
  • ETL: 表和列数量是否正确?每个外键是否指向预期目标表?目标模型能否验证?
  • 可重复性: 修改模型后,能否重新运行流程、比较输出,而不手工修补产物?

参考示例提供了小型端到端解答,并不生成完整的 JHipster 应用。加入重数、约束、应用配置和持久化选项,将成为新的设计练习。

7

练习

练习 1★★★概念5 分钟

追踪所有权。 为什么 Entity 包含自己的 Field,而 Relation.target 是引用?

查看解答

字段归声明它的实体所有。关系指向应用中已有的另一个实体;若包含该目标,就会复制或移动它。

练习 2★★★概念5 分钟

追踪元素。 在 SQL、Java 和 HTML 示例中追踪 Purchase.customer。哪些表示包含目标名称?HTML 示例省略了什么?

查看解答

SQL 声明 customer_id 和指向 Customer 的外键;Java 声明 Customer customer 字段。HTML 示例只报告 Purchase 有两个关系,未列出关系名称和目标,因此它只是摘要,不是完整的模式说明。

练习 3★★★概念5 分钟

查找信息损失。 第一个关系模型目标省略了什么?即使 ETL 已结束,为什么还要检查?

查看解答

它省略了 Purchase 的两条关系,因此目标中没有外键。ETL 执行成功只能说明规则运行过,不能证明所有必需的源概念都已映射。

练习 4★★★编程10 分钟

修复模型。 将重复的 Customer.email 字段改为 active: BOOLEAN,然后运行相同的 EVL 规则。

查看解答

修正后的模型通过这组参考规则中的全部三个约束:

参考解答验证修复后的模型在 Playground 中打开 ↗
rules.evl
context Entity {
  constraint UniqueEntityName {
    check: Entity.all.select(e | e.name = self.name).size() = 1
    message: "Duplicate entity: " + self.name
  }
}
context Field {
  constraint UniqueFieldName {
    check: self.eContainer().fields.select(f | f.name = self.name).size() = 1
    message: "Duplicate field: " + self.eContainer().name + "." + self.name
  }
}
context Relation {
  constraint HasTarget {
    check: not self.target.isUndefined()
    message: "Missing relation target: " + self.name
  }
}
其他文件: shop.flexmi, minijdl.emf
shop.flexmi
<?nsuri minijdl?>
<application name="Shop">
  <entity name="Customer">
    <field name="email" type="STRING"/>
    <field name="active" type="BOOLEAN"/>
  </entity>
  <entity name="Product">
    <field name="title" type="STRING"/>
    <field name="price" type="INTEGER"/>
  </entity>
  <entity name="Purchase">
    <field name="quantity" type="INTEGER"/>
    <relation name="customer" target="Shop.Customer"/>
    <relation name="product" target="Shop.Product"/>
  </entity>
</application>
minijdl.emf
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
  attr String[1] name;
  val Entity[*] entities;
}
class Entity {
  attr String[1] name;
  val Field[*] fields;
  val Relation[*] relations;
}
class Field {
  attr String[1] name;
  attr FieldType type;
}
class Relation {
  attr String[1] name;
  ref Entity target;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
All constraints are satisfied.
练习 5★★★编程10 分钟

扩展 Java 生成。 在每个生成的 Java 类中加入返回实体名称的 entityName() 方法。

查看解答

EGX 规则仍为每个实体创建一个文件;EGL 模板新增该方法:

参考解答生成包含关系目标的 Java 类在 Playground 中打开 ↗
generate.egx
rule Entity2Java
  transform e : Entity {
  template: "template.egl"
  target: e.name + ".java"
}
template.egl
public class [%=e.name%] {
  public int id;
[% for (f in e.fields) { %]  public [% if (f.type.toString() = "STRING") { %]String[% } else if (f.type.toString() = "BOOLEAN") { %]boolean[% } else { %]int[% } %] [%=f.name%];
[% } %][% for (r in e.relations) { %]  public [%=r.target.name%] [%=r.name%];
[% } %]  public String entityName() { return "[%=e.name%]"; }
}
其他文件: shop.flexmi, minijdl.emf
shop.flexmi
<?nsuri minijdl?>
<application name="Shop">
  <entity name="Customer">
    <field name="email" type="STRING"/>
    <field name="active" type="BOOLEAN"/>
  </entity>
  <entity name="Product">
    <field name="title" type="STRING"/>
    <field name="price" type="INTEGER"/>
  </entity>
  <entity name="Purchase">
    <field name="quantity" type="INTEGER"/>
    <relation name="customer" target="Shop.Customer"/>
    <relation name="product" target="Shop.Product"/>
  </entity>
</application>
minijdl.emf
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
  attr String[1] name;
  val Entity[*] entities;
}
class Entity {
  attr String[1] name;
  val Field[*] fields;
  val Relation[*] relations;
}
class Field {
  attr String[1] name;
  attr FieldType type;
}
class Relation {
  attr String[1] name;
  ref Entity target;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
--- Customer.java ---
public class Customer {
  public int id;
  public String email;
  public boolean active;
  public String entityName() { return "Customer"; }
}
--- Product.java ---
public class Product {
  public int id;
  public String title;
  public int price;
  public String entityName() { return "Product"; }
}
--- Purchase.java ---
public class Purchase {
  public int id;
  public int quantity;
  public Customer customer;
  public Product product;
  public String entityName() { return "Purchase"; }
}
练习 6★★★编程15 分钟

完成关系映射。 加入 ETL 规则,为每条关系创建外键,并通过转换轨迹解析目标表。

查看解答

Entity2Table 添加每个 _id 列并挂载转换后的关系;Relation2ForeignKey 将关系目标实体映射到相应的表。目标树现在于 Purchase 下包含 customer 和 product 外键:

参考解答将关系映射为外键在 Playground 中打开 ↗
transform.etl
rule Application2Schema
  transform a : Source!Application
  to s : Target!Schema {
  s.name = a.name;
  s.tables ::= a.entities;
}
rule Entity2Table
  transform e : Source!Entity
  to t : Target!Table {
  t.name = e.name;
  var id = new Target!Column;
  id.name = "id";
  id.sqlType = "INTEGER";
  t.columns ::= e.fields;
  t.columns.add(id);
  for (r in e.relations) {
    var column = new Target!Column;
    column.name = r.name + "_id";
    column.sqlType = "INTEGER";
    t.columns.add(column);
  }
  t.foreignKeys ::= e.relations;
}
rule Field2Column
  transform f : Source!Field
  to c : Target!Column {
  c.name = f.name;
  if (f.type.toString() = "STRING") { c.sqlType = "VARCHAR(255)"; }
  else if (f.type.toString() = "BOOLEAN") { c.sqlType = "BOOLEAN"; }
  else { c.sqlType = "INTEGER"; }
}
rule Relation2ForeignKey
  transform r : Source!Relation
  to fk : Target!ForeignKey {
  fk.name = r.name;
  fk.column = r.name + "_id";
  fk.target ::= r.target;
}
relational.emf
@namespace(uri="relational", prefix="relational")
package relational;
class Schema {
  attr String name;
  val Table[*] tables;
}
class Table {
  attr String name;
  val Column[*] columns;
  val ForeignKey[*] foreignKeys;
}
class Column {
  attr String name;
  attr String sqlType;
}
class ForeignKey {
  attr String name;
  attr String column;
  ref Table target;
}
其他文件: shop.flexmi, minijdl.emf
shop.flexmi
<?nsuri minijdl?>
<application name="Shop">
  <entity name="Customer">
    <field name="email" type="STRING"/>
    <field name="active" type="BOOLEAN"/>
  </entity>
  <entity name="Product">
    <field name="title" type="STRING"/>
    <field name="price" type="INTEGER"/>
  </entity>
  <entity name="Purchase">
    <field name="quantity" type="INTEGER"/>
    <relation name="customer" target="Shop.Customer"/>
    <relation name="product" target="Shop.Product"/>
  </entity>
</application>
minijdl.emf
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
  attr String[1] name;
  val Entity[*] entities;
}
class Entity {
  attr String[1] name;
  val Field[*] fields;
  val Relation[*] relations;
}
class Field {
  attr String[1] name;
  attr FieldType type;
}
class Relation {
  attr String[1] name;
  ref Entity target;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
Schema name="Shop"
  Table name="Customer"
    Column name="email" sqlType="VARCHAR(255)"
    Column name="active" sqlType="BOOLEAN"
    Column name="id" sqlType="INTEGER"
  Table name="Product"
    Column name="title" sqlType="VARCHAR(255)"
    Column name="price" sqlType="INTEGER"
    Column name="id" sqlType="INTEGER"
  Table name="Purchase"
    Column name="quantity" sqlType="INTEGER"
    Column name="id" sqlType="INTEGER"
    Column name="customer_id" sqlType="INTEGER"
    Column name="product_id" sqlType="INTEGER"
    ForeignKey name="customer" column="customer_id" target->Customer
    ForeignKey name="product" column="product_id" target->Product
8

自测

1
本模块的 MiniJDL 是什么?
2
哪个特征拥有 Field?
3
为什么无效模型产生验证结果?
4
SQL 生成器为每条关系增加什么?
5
EGX 在 Java 生成中负责什么?
6
第一个关系模型目标缺少什么?
7
Relation2ForeignKey 如何找到目标表?
8
在声称 Java 产物可用于生产前还需要哪项检查?
9

延伸阅读