Advanced Tutorials

Class to Relational Transformation

Master the classic Class2Relational transformation benchmark, a foundational example in model-driven engineering.

In this advanced tutorial, you’ll implement a complete transformation from UML-like class diagrams to relational database schemas. This demonstrates key ATL concepts including:

  • Multiple output elements per rule

  • Attribute type mapping

  • Primary and foreign key generation

  • Handling multi-valued attributes

45 mins Estimated Time

Section 1

Understanding the Metamodels

The Class2Relational transformation maps object-oriented class structures to relational tables. Understanding both metamodels is crucial for designing effective transformation rules.

The Class metamodel represents object-oriented concepts: packages, classes with attributes, data types, and inheritance.

The Relational metamodel represents database concepts: schemas, tables with columns, types, and primary/foreign keys.

Step 1

Examine the Class (source) metamodel.

The metamodel defines Package (container), Class (with attributes), Attribute (typed properties), DataType (primitive types), and Classifier (abstract base). Classes can have superTypes for inheritance.

class.ecore
1<?xml version="1.0" encoding="UTF-8"?>
2<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="Class" nsURI="http://www.example.org/class" nsPrefix="class">
4 <!-- NamedElement: Base class for all named elements -->
5 <eClassifiers xsi:type="ecore:EClass" name="NamedElement" abstract="true">
6 <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" lowerBound="1"
7 eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
8 </eClassifiers>
9 <!-- Package: Container for classifiers (Classes and DataTypes) -->
10 <eClassifiers xsi:type="ecore:EClass" name="Package" eSuperTypes="#//NamedElement">
11 <eStructuralFeatures xsi:type="ecore:EReference" name="classifiers" upperBound="-1"
12 eType="#//Classifier" containment="true" eOpposite="#//Classifier/package"/>
13 </eClassifiers>
14 <!-- Classifier: Abstract base for Class and DataType -->
15 <eClassifiers xsi:type="ecore:EClass" name="Classifier" abstract="true" eSuperTypes="#//NamedElement">
16 <eStructuralFeatures xsi:type="ecore:EReference" name="package" eType="#//Package"
17 eOpposite="#//Package/classifiers"/>
18 </eClassifiers>
19 <!-- DataType: Primitive data types (Integer, String, Boolean, etc.) -->
20 <eClassifiers xsi:type="ecore:EClass" name="DataType" eSuperTypes="#//Classifier"/>
21 <!-- Class: Classes with attributes and optional supertype -->
22 <eClassifiers xsi:type="ecore:EClass" name="Class" eSuperTypes="#//Classifier">
23 <eStructuralFeatures xsi:type="ecore:EAttribute" name="isAbstract" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EBoolean"
24 defaultValueLiteral="false"/>
25 <eStructuralFeatures xsi:type="ecore:EReference" name="superType" eType="#//Class"/>
26 <eStructuralFeatures xsi:type="ecore:EReference" name="attributes" upperBound="-1"
27 eType="#//Attribute" containment="true" eOpposite="#//Attribute/owner"/>
28 </eClassifiers>
29 <!-- Attribute: Class attributes with type and multiplicity -->
30 <eClassifiers xsi:type="ecore:EClass" name="Attribute" eSuperTypes="#//NamedElement">
31 <eStructuralFeatures xsi:type="ecore:EAttribute" name="multiValued" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EBoolean"
32 defaultValueLiteral="false"/>
33 <eStructuralFeatures xsi:type="ecore:EReference" name="type" lowerBound="1" eType="#//Classifier"/>
34 <eStructuralFeatures xsi:type="ecore:EReference" name="owner" lowerBound="1" eType="#//Class"
35 eOpposite="#//Class/attributes"/>
36 </eClassifiers>
37</ecore:EPackage>

Step 2

Examine the Relational (target) metamodel.

The metamodel defines Schema (container), Table (with columns), Column (with type reference), and Type. Tables have a key column for primary keys, and columns can reference other tables via keyOf.

relational.ecore
1<?xml version="1.0" encoding="UTF-8"?>
2<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="Relational" nsURI="http://www.example.org/relational" nsPrefix="relational">
4 <!-- Named: Base class for all named elements -->
5 <eClassifiers xsi:type="ecore:EClass" name="Named" abstract="true">
6 <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" lowerBound="1"
7 eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
8 </eClassifiers>
9 <!-- Schema: Database schema containing tables and types -->
10 <eClassifiers xsi:type="ecore:EClass" name="Schema" eSuperTypes="#//Named">
11 <eStructuralFeatures xsi:type="ecore:EReference" name="tables" upperBound="-1"
12 eType="#//Table" containment="true" eOpposite="#//Table/schema"/>
13 <eStructuralFeatures xsi:type="ecore:EReference" name="types" upperBound="-1"
14 eType="#//Type" containment="true"/>
15 </eClassifiers>
16 <!-- Table: Database table with columns and keys -->
17 <eClassifiers xsi:type="ecore:EClass" name="Table" eSuperTypes="#//Named">
18 <eStructuralFeatures xsi:type="ecore:EReference" name="schema" eType="#//Schema"
19 eOpposite="#//Schema/tables"/>
20 <eStructuralFeatures xsi:type="ecore:EReference" name="columns" upperBound="-1"
21 eType="#//Column" containment="true" eOpposite="#//Column/owner"/>
22 <eStructuralFeatures xsi:type="ecore:EReference" name="key" upperBound="-1"
23 eType="#//Column"/>
24 </eClassifiers>
25 <!-- Column: Table column with type reference -->
26 <eClassifiers xsi:type="ecore:EClass" name="Column" eSuperTypes="#//Named">
27 <eStructuralFeatures xsi:type="ecore:EReference" name="owner" eType="#//Table"
28 eOpposite="#//Table/columns"/>
29 <eStructuralFeatures xsi:type="ecore:EReference" name="keyOf" eType="#//Table"/>
30 <eStructuralFeatures xsi:type="ecore:EReference" name="type" lowerBound="1" eType="#//Type"/>
31 </eClassifiers>
32 <!-- Type: SQL data type (INTEGER, VARCHAR, etc.) -->
33 <eClassifiers xsi:type="ecore:EClass" name="Type" eSuperTypes="#//Named"/>
34</ecore:EPackage>

Section 2

Writing Helper Functions

Helpers encapsulate reusable logic for the transformation. We need helpers for:

  • Collecting all attributes (including inherited ones)

  • Determining if a classifier is a data type

  • Mapping data types to SQL type names

Step 1

Review the complete transformation with all helpers including allAttributes (recursive collection), isDataType (primitive check), and sqlTypeName (SQL type mapping).

class2relational.atl
1-- ATL Transformation: Class to Relational
2-- This transformation converts a simplified UML class model into a relational database schema
3-- Based on the classic TTC benchmark case
4-- @path Class=/Class2Relational/class.ecore
5-- @path Relational=/Class2Relational/relational.ecore
6
7module Class2Relational;
8create OUT: Relational from IN: Class;
9
10-- ===========================================================================
11-- Helper: Get all attributes including inherited ones
12-- ===========================================================================
13helper context Class!Class def: allAttributes: Sequence(Class!Attribute) =
14 if self.superType.oclIsUndefined() then
15 self.attributes->asSequence()
16 else
17 self.superType.allAttributes->union(self.attributes->asSequence())
18 endif;
19
20-- ===========================================================================
21-- Helper: Check if a classifier is a DataType (primitive type)
22-- ===========================================================================
23helper context Class!Classifier def: isDataType: Boolean =
24 self.oclIsTypeOf(Class!DataType);
25
26-- ===========================================================================
27-- Helper: Map class type to SQL type name
28-- ===========================================================================
29helper context Class!DataType def: sqlTypeName: String =
30 if self.name = 'Integer' then
31 'INTEGER'
32 else if self.name = 'Boolean' then
33 'BOOLEAN'
34 else if self.name = 'String' then
35 'VARCHAR(255)'
36 else if self.name = 'Double' or self.name = 'Float' then
37 'DECIMAL(10,2)'
38 else if self.name = 'Date' then
39 'DATE'
40 else
41 'VARCHAR(255)'
42 endif endif endif endif endif;
43
44-- ===========================================================================
45-- Rule: Package -> Schema
46-- Transforms a class package into a database schema
47-- ===========================================================================
48rule Package2Schema {
49 from
50 p: Class!Package
51 to
52 s: Relational!Schema (
53 name <- p.name,
54 tables <- p.classifiers->select(c | c.oclIsTypeOf(Class!Class)),
55 types <- p.classifiers->select(c | c.oclIsTypeOf(Class!DataType))
56 )
57}
58
59-- ===========================================================================
60-- Rule: DataType -> Type
61-- Maps primitive data types to SQL types
62-- ===========================================================================
63rule DataType2Type {
64 from
65 dt: Class!DataType
66 to
67 t: Relational!Type (
68 name <- dt.sqlTypeName
69 )
70}
71
72-- ===========================================================================
73-- Rule: Class -> Table
74-- Transforms a class into a database table with primary key
75-- ===========================================================================
76rule Class2Table {
77 from
78 c: Class!Class (not c.isAbstract)
79 to
80 t: Relational!Table (
81 name <- c.name,
82 columns <- Sequence{key}->union(
83 c.allAttributes->select(a | not a.multiValued and a.type.isDataType)
84 ),
85 key <- Sequence{key}
86 ),
87 key: Relational!Column (
88 name <- 'id',
89 type <- thisModule.resolveTemp(
90 Class!DataType.allInstances()->any(dt | dt.name = 'Integer'),
91 't'
92 ),
93 keyOf <- t
94 )
95}
96
97-- ===========================================================================
98-- Rule: SingleValuedAttribute -> Column
99-- Transforms single-valued primitive attributes to table columns
100-- ===========================================================================
101rule SingleValuedAttribute2Column {
102 from
103 a: Class!Attribute (
104 not a.multiValued and
105 a.type.oclIsTypeOf(Class!DataType)
106 )
107 to
108 c: Relational!Column (
109 name <- a.name,
110 type <- a.type
111 )
112}
113
114-- ===========================================================================
115-- Rule: MultiValuedAttribute -> Table
116-- Transforms multi-valued attributes to separate tables with foreign key
117-- ===========================================================================
118rule MultiValuedAttribute2Table {
119 from
120 a: Class!Attribute (
121 a.multiValued and
122 a.type.oclIsTypeOf(Class!DataType)
123 )
124 to
125 t: Relational!Table (
126 name <- a.owner.name + '_' + a.name,
127 columns <- Sequence{id, value}
128 ),
129 id: Relational!Column (
130 name <- a.owner.name.toLower() + '_id',
131 type <- thisModule.resolveTemp(
132 Class!DataType.allInstances()->any(dt | dt.name = 'Integer'),
133 't'
134 )
135 ),
136 value: Relational!Column (
137 name <- a.name,
138 type <- a.type
139 )
140}
141
142-- ===========================================================================
143-- Rule: ClassAttribute -> ForeignKey
144-- Transforms class-typed attributes to foreign key columns
145-- ===========================================================================
146rule ClassAttribute2ForeignKey {
147 from
148 a: Class!Attribute (
149 not a.multiValued and
150 a.type.oclIsTypeOf(Class!Class)
151 )
152 to
153 fk: Relational!Column (
154 name <- a.name + '_id',
155 type <- thisModule.resolveTemp(
156 Class!DataType.allInstances()->any(dt | dt.name = 'Integer'),
157 't'
158 )
159 )
160}

Section 3

Core Transformation Rules

The transformation requires several rules to handle different mapping scenarios:

  • Package to Schema (container mapping)

  • Class to Table (with primary key column)

  • Single-valued attributes to columns

  • Multi-valued attributes to separate tables

  • Class-typed attributes to foreign key columns

Step 1

The transformation defines rules for Package2Schema, DataType2Type, Class2Table (with primary keys), SingleValuedAttribute2Column, MultiValuedAttribute2Table, and ClassAttribute2ForeignKey mappings.

class2relational.atl
1-- ATL Transformation: Class to Relational
2-- This transformation converts a simplified UML class model into a relational database schema
3-- Based on the classic TTC benchmark case
4-- @path Class=/Class2Relational/class.ecore
5-- @path Relational=/Class2Relational/relational.ecore
6
7module Class2Relational;
8create OUT: Relational from IN: Class;
9
10-- ===========================================================================
11-- Helper: Get all attributes including inherited ones
12-- ===========================================================================
13helper context Class!Class def: allAttributes: Sequence(Class!Attribute) =
14 if self.superType.oclIsUndefined() then
15 self.attributes->asSequence()
16 else
17 self.superType.allAttributes->union(self.attributes->asSequence())
18 endif;
19
20-- ===========================================================================
21-- Helper: Check if a classifier is a DataType (primitive type)
22-- ===========================================================================
23helper context Class!Classifier def: isDataType: Boolean =
24 self.oclIsTypeOf(Class!DataType);
25
26-- ===========================================================================
27-- Helper: Map class type to SQL type name
28-- ===========================================================================
29helper context Class!DataType def: sqlTypeName: String =
30 if self.name = 'Integer' then
31 'INTEGER'
32 else if self.name = 'Boolean' then
33 'BOOLEAN'
34 else if self.name = 'String' then
35 'VARCHAR(255)'
36 else if self.name = 'Double' or self.name = 'Float' then
37 'DECIMAL(10,2)'
38 else if self.name = 'Date' then
39 'DATE'
40 else
41 'VARCHAR(255)'
42 endif endif endif endif endif;
43
44-- ===========================================================================
45-- Rule: Package -> Schema
46-- Transforms a class package into a database schema
47-- ===========================================================================
48rule Package2Schema {
49 from
50 p: Class!Package
51 to
52 s: Relational!Schema (
53 name <- p.name,
54 tables <- p.classifiers->select(c | c.oclIsTypeOf(Class!Class)),
55 types <- p.classifiers->select(c | c.oclIsTypeOf(Class!DataType))
56 )
57}
58
59-- ===========================================================================
60-- Rule: DataType -> Type
61-- Maps primitive data types to SQL types
62-- ===========================================================================
63rule DataType2Type {
64 from
65 dt: Class!DataType
66 to
67 t: Relational!Type (
68 name <- dt.sqlTypeName
69 )
70}
71
72-- ===========================================================================
73-- Rule: Class -> Table
74-- Transforms a class into a database table with primary key
75-- ===========================================================================
76rule Class2Table {
77 from
78 c: Class!Class (not c.isAbstract)
79 to
80 t: Relational!Table (
81 name <- c.name,
82 columns <- Sequence{key}->union(
83 c.allAttributes->select(a | not a.multiValued and a.type.isDataType)
84 ),
85 key <- Sequence{key}
86 ),
87 key: Relational!Column (
88 name <- 'id',
89 type <- thisModule.resolveTemp(
90 Class!DataType.allInstances()->any(dt | dt.name = 'Integer'),
91 't'
92 ),
93 keyOf <- t
94 )
95}
96
97-- ===========================================================================
98-- Rule: SingleValuedAttribute -> Column
99-- Transforms single-valued primitive attributes to table columns
100-- ===========================================================================
101rule SingleValuedAttribute2Column {
102 from
103 a: Class!Attribute (
104 not a.multiValued and
105 a.type.oclIsTypeOf(Class!DataType)
106 )
107 to
108 c: Relational!Column (
109 name <- a.name,
110 type <- a.type
111 )
112}
113
114-- ===========================================================================
115-- Rule: MultiValuedAttribute -> Table
116-- Transforms multi-valued attributes to separate tables with foreign key
117-- ===========================================================================
118rule MultiValuedAttribute2Table {
119 from
120 a: Class!Attribute (
121 a.multiValued and
122 a.type.oclIsTypeOf(Class!DataType)
123 )
124 to
125 t: Relational!Table (
126 name <- a.owner.name + '_' + a.name,
127 columns <- Sequence{id, value}
128 ),
129 id: Relational!Column (
130 name <- a.owner.name.toLower() + '_id',
131 type <- thisModule.resolveTemp(
132 Class!DataType.allInstances()->any(dt | dt.name = 'Integer'),
133 't'
134 )
135 ),
136 value: Relational!Column (
137 name <- a.name,
138 type <- a.type
139 )
140}
141
142-- ===========================================================================
143-- Rule: ClassAttribute -> ForeignKey
144-- Transforms class-typed attributes to foreign key columns
145-- ===========================================================================
146rule ClassAttribute2ForeignKey {
147 from
148 a: Class!Attribute (
149 not a.multiValued and
150 a.type.oclIsTypeOf(Class!Class)
151 )
152 to
153 fk: Relational!Column (
154 name <- a.name + '_id',
155 type <- thisModule.resolveTemp(
156 Class!DataType.allInstances()->any(dt | dt.name = 'Integer'),
157 't'
158 )
159 )
160}

Section 4

Running the Transformation

Test the transformation with a sample library management class model. The output should produce appropriate tables with proper key relationships.

Step 1

Create a sample class model.

This model represents a library system with Author, Book, Member, and Loan classes. Note the multi-valued ‘keywords’ and ‘borrowedBooks’ attributes.

sample-classes.xmi
1<?xml version="1.0" encoding="UTF-8"?>
2<xmi:XMI xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI"
3 xmlns:class="http://www.example.org/class">
4 <!-- Sample Class Model: Library Management System -->
5 <class:Package name="library">
6 <!-- Primitive DataTypes -->
7 <classifiers xsi:type="class:DataType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Integer"/>
8 <classifiers xsi:type="class:DataType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="String"/>
9 <classifiers xsi:type="class:DataType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Boolean"/>
10 <classifiers xsi:type="class:DataType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Date"/>
11
12 <!-- Author Class -->
13 <classifiers xsi:type="class:Class" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Author">
14 <attributes name="firstName" type="//@classifiers.1"/>
15 <attributes name="lastName" type="//@classifiers.1"/>
16 <attributes name="birthYear" type="//@classifiers.0"/>
17 </classifiers>
18
19 <!-- Book Class -->
20 <classifiers xsi:type="class:Class" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Book">
21 <attributes name="title" type="//@classifiers.1"/>
22 <attributes name="isbn" type="//@classifiers.1"/>
23 <attributes name="pageCount" type="//@classifiers.0"/>
24 <attributes name="available" type="//@classifiers.2"/>
25 <attributes name="author" type="//@classifiers.4"/>
26 <attributes name="keywords" multiValued="true" type="//@classifiers.1"/>
27 </classifiers>
28
29 <!-- Member Class -->
30 <classifiers xsi:type="class:Class" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Member">
31 <attributes name="memberNumber" type="//@classifiers.1"/>
32 <attributes name="name" type="//@classifiers.1"/>
33 <attributes name="email" type="//@classifiers.1"/>
34 <attributes name="joinDate" type="//@classifiers.3"/>
35 <attributes name="borrowedBooks" multiValued="true" type="//@classifiers.1"/>
36 </classifiers>
37
38 <!-- Loan Class -->
39 <classifiers xsi:type="class:Class" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Loan">
40 <attributes name="loanDate" type="//@classifiers.3"/>
41 <attributes name="dueDate" type="//@classifiers.3"/>
42 <attributes name="returned" type="//@classifiers.2"/>
43 <attributes name="book" type="//@classifiers.5"/>
44 <attributes name="member" type="//@classifiers.6"/>
45 </classifiers>
46 </class:Package>
47</xmi:XMI>

Step 2

Run the transformation.

Execute swift-atl with the transformation and input model. The output shows the generated relational schema.

Terminal
1# Run the Class2Relational transformation
2swift-atl transform \
3 --transformation class2relational.atl \
4 --source-metamodel class.ecore \
5 --target-metamodel relational.ecore \
6 --input sample-classes.xmi \
7 --output output-relational.xmi
8
9# Output:
10# Transformation complete.
11# Created schema 'library' with:
12# - 6 tables (Author, Book, Member, Loan, Book_keywords, Member_borrowedBooks)
13# - 4 SQL types (INTEGER, VARCHAR(255), BOOLEAN, DATE)
14# - Primary key columns for each main table
15# - Foreign key columns for class references

Step 3

Examine the output relational schema.

Notice how classes became tables with id columns, multi-valued attributes created separate tables (Book_keywords, Member_borrowedBooks), and class references became foreign key columns.

expected-output.xmi
1<?xml version="1.0" encoding="UTF-8"?>
2<xmi:XMI xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI"
3 xmlns:relational="http://www.example.org/relational">
4 <!-- Expected Output: Library Management Database Schema -->
5 <relational:Schema name="library">
6 <!-- SQL Types -->
7 <types name="INTEGER"/>
8 <types name="VARCHAR(255)"/>
9 <types name="BOOLEAN"/>
10 <types name="DATE"/>
11
12 <!-- Author Table -->
13 <tables name="Author">
14 <columns name="id" keyOf="//@tables.0" type="//@types.0"/>
15 <columns name="firstName" type="//@types.1"/>
16 <columns name="lastName" type="//@types.1"/>
17 <columns name="birthYear" type="//@types.0"/>
18 <key>//@tables.0/@columns.0</key>
19 </tables>
20
21 <!-- Book Table -->
22 <tables name="Book">
23 <columns name="id" keyOf="//@tables.1" type="//@types.0"/>
24 <columns name="title" type="//@types.1"/>
25 <columns name="isbn" type="//@types.1"/>
26 <columns name="pageCount" type="//@types.0"/>
27 <columns name="available" type="//@types.2"/>
28 <columns name="author_id" type="//@types.0"/>
29 <key>//@tables.1/@columns.0</key>
30 </tables>
31
32 <!-- Book_keywords Table (for multi-valued attribute) -->
33 <tables name="Book_keywords">
34 <columns name="book_id" type="//@types.0"/>
35 <columns name="keywords" type="//@types.1"/>
36 </tables>
37
38 <!-- Member Table -->
39 <tables name="Member">
40 <columns name="id" keyOf="//@tables.3" type="//@types.0"/>
41 <columns name="memberNumber" type="//@types.1"/>
42 <columns name="name" type="//@types.1"/>
43 <columns name="email" type="//@types.1"/>
44 <columns name="joinDate" type="//@types.3"/>
45 <key>//@tables.3/@columns.0</key>
46 </tables>
47
48 <!-- Member_borrowedBooks Table (for multi-valued attribute) -->
49 <tables name="Member_borrowedBooks">
50 <columns name="member_id" type="//@types.0"/>
51 <columns name="borrowedBooks" type="//@types.1"/>
52 </tables>
53
54 <!-- Loan Table -->
55 <tables name="Loan">
56 <columns name="id" keyOf="//@tables.5" type="//@types.0"/>
57 <columns name="loanDate" type="//@types.3"/>
58 <columns name="dueDate" type="//@types.3"/>
59 <columns name="returned" type="//@types.2"/>
60 <columns name="book_id" type="//@types.0"/>
61 <columns name="member_id" type="//@types.0"/>
62 <key>//@tables.5/@columns.0</key>
63 </tables>
64 </relational:Schema>
65</xmi:XMI>

Check Your Understanding

Question 1 of 3

Why does the Class2Table rule create two target elements (table and key column)?

Question 2 of 3

How are multi-valued attributes handled differently from single-valued ones?

Question 3 of 3

What is the purpose of the allAttributes helper?

UML to Swift Code Generation

Learn to generate Swift code from UML class diagrams using MTL templates.