Compare commits

..

2 Commits

Author SHA1 Message Date
darshanr0107
d32f6afd35 chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-01 17:28:33 +05:30
darshanr0107
5a167835cc feat: provide support for aggregation in ER diagram
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-01 17:07:13 +05:30
18 changed files with 608 additions and 37 deletions

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Render newlines as spaces in class diagrams

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
feat: Add support for aggregation relationships in ER diagram

View File

@@ -524,18 +524,5 @@ describe('Class diagram', () => {
`,
{}
);
it('should handle an empty class body with empty braces', () => {
imgSnapshotTest(
` classDiagram
class FooBase~T~ {}
class Bar {
+Zip
+Zap()
}
FooBase <|-- Ba
`,
{ flowchart: { defaultRenderer: 'elk' } }
);
});
});
});

View File

@@ -369,4 +369,94 @@ ORDER ||--|{ LINE-ITEM : contains
);
});
});
describe('Aggregation Relationships', () => {
it('should render basic aggregation relationships', () => {
imgSnapshotTest(
`
erDiagram
DEPARTMENT <> EMPLOYEE : contains
PROJECT <>.. TASK : manages
TEAM <> MEMBER : consists_of
`,
{ logLevel: 1 }
);
});
it('should render aggregation with entity attributes', () => {
imgSnapshotTest(
`
erDiagram
DEPARTMENT <> EMPLOYEE : contains
DEPARTMENT {
int id PK
string name
string location
}
EMPLOYEE {
int id PK
string name
int department_id FK
}
`,
{ logLevel: 1 }
);
});
it('should render aggregation with quoted labels', () => {
imgSnapshotTest(
`
erDiagram
UNIVERSITY <> COLLEGE : "has multiple"
COLLEGE <> DEPARTMENT : "contains"
DEPARTMENT <> FACULTY : "employs"
`,
{ logLevel: 1 }
);
});
it('should render mixed relationship types', () => {
imgSnapshotTest(
`
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ ORDER_ITEM : contains
PRODUCT <> ORDER_ITEM : "aggregated in"
WAREHOUSE <>.. PRODUCT : "stores"
`,
{ logLevel: 1 }
);
});
it('should render aggregation with entity aliases', () => {
imgSnapshotTest(
`
erDiagram
d[DEPARTMENT]
e[EMPLOYEE]
p[PROJECT]
t[TASK]
d <> e : contains
p <>.. t : manages
`,
{ logLevel: 1 }
);
});
it('should render complex aggregation scenarios', () => {
imgSnapshotTest(
`
erDiagram
COMPANY <> DEPARTMENT : owns
DEPARTMENT <> EMPLOYEE : contains
EMPLOYEE <> PROJECT : works_on
PROJECT <> TASK : consists_of
TASK <> SUBTASK : includes
`,
{ logLevel: 1 }
);
});
});
});

View File

@@ -169,6 +169,164 @@
</pre>
<hr />
<!-- Aggregation Examples -->
<h2>Aggregation Examples</h2>
<h3>Basic Aggregation (Solid Line)</h3>
<pre class="mermaid">
erDiagram
DEPARTMENT <> EMPLOYEE : contains
DEPARTMENT {
int id PK
string name
string location
}
EMPLOYEE {
int id PK
string name
int department_id FK
}
</pre>
<hr />
<h3>Dashed Aggregation</h3>
<pre class="mermaid">
erDiagram
PROJECT <>.. TASK : manages
PROJECT {
int id PK
string name
string description
}
TASK {
int id PK
string title
int project_id FK
}
</pre>
<hr />
<h3>Aggregation with Different Cardinalities</h3>
<pre class="mermaid">
erDiagram
COMPANY <> DEPARTMENT : owns
DEPARTMENT <> EMPLOYEE : contains
EMPLOYEE <> PROJECT : works_on
PROJECT <> TASK : consists_of
COMPANY {
int id PK
string name
}
DEPARTMENT {
int id PK
string name
int company_id FK
}
EMPLOYEE {
int id PK
string name
int department_id FK
}
PROJECT {
int id PK
string name
int employee_id FK
}
TASK {
int id PK
string title
int project_id FK
}
</pre>
<hr />
<h3>Two-way Aggregation</h3>
<pre class="mermaid">
erDiagram
TEAM <> MEMBER : consists_of
TEAM {
int id PK
string name
}
MEMBER {
int id PK
string name
int team_id FK
}
</pre>
<hr />
<h3>Complex Aggregation with Labels</h3>
<pre class="mermaid">
erDiagram
UNIVERSITY <> COLLEGE : "has multiple"
COLLEGE <> DEPARTMENT : "contains"
DEPARTMENT <> FACULTY : "employs"
FACULTY <> STUDENT : "teaches"
UNIVERSITY {
int id PK
string name
}
COLLEGE {
int id PK
string name
int university_id FK
}
DEPARTMENT {
int id PK
string name
int college_id FK
}
FACULTY {
int id PK
string name
int department_id FK
}
STUDENT {
int id PK
string name
int faculty_id FK
}
</pre>
<hr />
<h3>Mixed Relationship Types</h3>
<pre class="mermaid">
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ ORDER_ITEM : contains
PRODUCT <> ORDER_ITEM : "aggregated in"
WAREHOUSE <>.. PRODUCT : "stores"
CUSTOMER {
int id PK
string name
}
ORDER {
int id PK
int customer_id FK
date order_date
}
ORDER_ITEM {
int id PK
int order_id FK
int product_id FK
int quantity
}
PRODUCT {
int id PK
string name
int warehouse_id FK
}
WAREHOUSE {
int id PK
string name
}
</pre>
<hr />
<script type="module">
import mermaid from './mermaid.esm.mjs';
mermaid.initialize({

View File

@@ -6,7 +6,7 @@
# Frequently Asked Questions
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/1433#issuecomment-1991554712)
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217)
2. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785)
3. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621)
4. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)

View File

@@ -209,6 +209,42 @@ erDiagram
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
```
### Aggregation
Aggregation represents a "has-a" relationship where the part can exist independently of the whole. This is different from composition, where the part cannot exist without the whole. Aggregation relationships are rendered with hollow diamond markers at the endpoints.
| Value | Alias for | Description |
| :---: | :------------------: | ------------------------------ |
| <> | _aggregation_ | Basic aggregation (solid line) |
| <>.. | _aggregation-dashed_ | Dashed aggregation line |
**Examples:**
```mermaid-example
erDiagram
DEPARTMENT <> EMPLOYEE : contains
PROJECT <>.. TASK : manages
TEAM <> MEMBER : consists_of
```
```mermaid
erDiagram
DEPARTMENT <> EMPLOYEE : contains
PROJECT <>.. TASK : manages
TEAM <> MEMBER : consists_of
```
In these examples:
- `DEPARTMENT <> EMPLOYEE` shows that a department contains employees (aggregation)
- `PROJECT <>.. TASK` shows that a project manages tasks (dashed aggregation)
- `TEAM <> MEMBER` shows that a team consists of members (aggregation)
**Aggregation vs Association**
- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently
- **Association** (`||--`, `}o--`): General relationship between entities
### Attributes
Attributes can be defined for entities by specifying the entity name followed by a block containing multiple `type name` pairs, where a block is delimited by an opening `{` and a closing `}`. The attributes are rendered inside the entity boxes. For example:

View File

@@ -1070,14 +1070,6 @@ describe('given a class diagram with members and methods ', function () {
parser.parse(str);
});
it('should handle an empty class body with {}', function () {
const str = 'classDiagram\nclass EmptyClass {}';
parser.parse(str);
const actual = parser.yy.getClass('EmptyClass');
expect(actual.label).toBe('EmptyClass');
expect(actual.members.length).toBe(0);
expect(actual.methods.length).toBe(0);
});
});
});

View File

@@ -293,7 +293,6 @@ classStatement
: classIdentifier
| classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);}
| classIdentifier STRUCT_START members STRUCT_STOP {yy.addMembers($1,$3);}
| classIdentifier STRUCT_START STRUCT_STOP {}
| classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.setCssClass($1, $3);yy.addMembers($1,$5);}
;
@@ -302,15 +301,8 @@ classIdentifier
| CLASS className classLabel {$$=$2; yy.addClass($2);yy.setClassLabel($2, $3);}
;
emptyBody
:
| SPACE emptyBody
| NEWLINE emptyBody
;
annotationStatement
: ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); }
:ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); }
;
members

View File

@@ -2,6 +2,7 @@ import { log } from '../../logger.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import type { Edge, Node } from '../../rendering-util/types.js';
import type { EntityNode, Attribute, Relationship, EntityClass, RelSpec } from './erTypes.js';
import { AggregationType } from './erTypes.js';
import {
setAccTitle,
getAccTitle,
@@ -33,6 +34,11 @@ export class ErDB implements DiagramDB {
IDENTIFYING: 'IDENTIFYING',
};
private Aggregation = {
AGGREGATION: AggregationType.AGGREGATION,
AGGREGATION_DASHED: AggregationType.AGGREGATION_DASHED,
};
constructor() {
this.clear();
this.addEntity = this.addEntity.bind(this);
@@ -131,6 +137,31 @@ export class ErDB implements DiagramDB {
return this.relationships;
}
/**
* Validate aggregation relationship
* @param rSpec - The relationship specification to validate
* @returns boolean indicating if the aggregation relationship is valid
*/
public validateAggregationRelationship(rSpec: RelSpec): boolean {
const isAggregation =
rSpec.relType === this.Aggregation.AGGREGATION ||
rSpec.relType === this.Aggregation.AGGREGATION_DASHED;
if (!isAggregation) {
return false;
}
const validCardinalities = [
this.Cardinality.ZERO_OR_ONE,
this.Cardinality.ZERO_OR_MORE,
this.Cardinality.ONE_OR_MORE,
this.Cardinality.ONLY_ONE,
this.Cardinality.MD_PARENT,
];
return validCardinalities.includes(rSpec.cardA) && validCardinalities.includes(rSpec.cardB);
}
public getDirection() {
return this.direction;
}
@@ -248,4 +279,17 @@ export class ErDB implements DiagramDB {
public setDiagramTitle = setDiagramTitle;
public getDiagramTitle = getDiagramTitle;
public getConfig = () => getConfig().er;
// Getter methods for aggregation constants
public get AggregationConstants() {
return this.Aggregation;
}
public get CardinalityConstants() {
return this.Cardinality;
}
public get IdentificationConstants() {
return this.Identification;
}
}

View File

@@ -9,6 +9,10 @@ const ERMarkers = {
ZERO_OR_MORE_END: 'ZERO_OR_MORE_END',
MD_PARENT_END: 'MD_PARENT_END',
MD_PARENT_START: 'MD_PARENT_START',
AGGREGATION_START: 'AGGREGATION_START',
AGGREGATION_END: 'AGGREGATION_END',
AGGREGATION_DASHED_START: 'AGGREGATION_DASHED_START',
AGGREGATION_DASHED_END: 'AGGREGATION_DASHED_END',
};
/**
@@ -180,6 +184,66 @@ const insertMarkers = function (elem, conf) {
.attr('fill', 'none')
.attr('d', 'M21,18 Q39,0 57,18 Q39,36 21,18');
// Aggregation markers (hollow diamond)
elem
.append('defs')
.append('marker')
.attr('id', ERMarkers.AGGREGATION_START)
.attr('refX', 0)
.attr('refY', 9)
.attr('markerWidth', 20)
.attr('markerHeight', 18)
.attr('orient', 'auto')
.append('path')
.attr('stroke', conf.stroke)
.attr('fill', 'white')
.attr('d', 'M18,9 L9,0 L0,9 L9,18 Z');
elem
.append('defs')
.append('marker')
.attr('id', ERMarkers.AGGREGATION_END)
.attr('refX', 20)
.attr('refY', 9)
.attr('markerWidth', 20)
.attr('markerHeight', 18)
.attr('orient', 'auto')
.append('path')
.attr('stroke', conf.stroke)
.attr('fill', 'white')
.attr('d', 'M2,9 L11,0 L20,9 L11,18 Z');
// Dashed aggregation markers
elem
.append('defs')
.append('marker')
.attr('id', ERMarkers.AGGREGATION_DASHED_START)
.attr('refX', 0)
.attr('refY', 9)
.attr('markerWidth', 20)
.attr('markerHeight', 18)
.attr('orient', 'auto')
.append('path')
.attr('stroke', conf.stroke)
.attr('fill', 'white')
.attr('stroke-dasharray', '3,3')
.attr('d', 'M18,9 L9,0 L0,9 L9,18 Z');
elem
.append('defs')
.append('marker')
.attr('id', ERMarkers.AGGREGATION_DASHED_END)
.attr('refX', 20)
.attr('refY', 9)
.attr('markerWidth', 20)
.attr('markerHeight', 18)
.attr('orient', 'auto')
.append('path')
.attr('stroke', conf.stroke)
.attr('fill', 'white')
.attr('stroke-dasharray', '3,3')
.attr('d', 'M2,9 L11,0 L20,9 L11,18 Z');
return;
};

View File

@@ -448,6 +448,11 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
svgPath.attr('stroke-dasharray', '8,8');
}
// Handle aggregation relationship styling
if (rel.relSpec.relType === diagObj.db.Aggregation.AGGREGATION_DASHED) {
svgPath.attr('stroke-dasharray', '8,8');
}
// TODO: Understand this better
let url = '';
if (conf.arrowMarkerAbsolute) {
@@ -503,6 +508,15 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
break;
}
// Handle aggregation markers
if (
rel.relSpec.relType === diagObj.db.Aggregation.AGGREGATION ||
rel.relSpec.relType === diagObj.db.Aggregation.AGGREGATION_DASHED
) {
// Add aggregation marker at the start (entity B side)
svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.AGGREGATION_START + ')');
}
// Now label the relationship
// Find the half-way point

View File

@@ -35,3 +35,15 @@ export interface EntityClass {
styles: string[];
textStyles: string[];
}
// Aggregation relationship types
export const AggregationType = {
AGGREGATION: 'AGGREGATION',
AGGREGATION_DASHED: 'AGGREGATION_DASHED',
} as const;
// Line types for aggregation
export const AggregationLineType = {
SOLID: 'SOLID',
DASHED: 'DASHED',
} as const;

View File

@@ -72,6 +72,8 @@ o\| return 'ZERO_OR_ONE';
o\{ return 'ZERO_OR_MORE';
\|\{ return 'ONE_OR_MORE';
\s*u return 'MD_PARENT';
"<>.." return 'AGGREGATION_DASHED';
"<>" return 'AGGREGATION';
\.\. return 'NON_IDENTIFYING';
\-\- return 'IDENTIFYING';
"to" return 'IDENTIFYING';
@@ -167,6 +169,47 @@ statement
| entityName SQS entityName SQE STYLE_SEPARATOR idList BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); yy.setClass([$1], $6); }
| entityName SQS entityName SQE { yy.addEntity($1, $3); }
| entityName SQS entityName SQE STYLE_SEPARATOR idList { yy.addEntity($1, $3); yy.setClass([$1], $6); }
| entityName aggregationRelSpec entityName COLON role
{
yy.addEntity($1);
yy.addEntity($3);
yy.addRelationship($1, $5, $3, $2);
}
| entityName STYLE_SEPARATOR idList aggregationRelSpec entityName STYLE_SEPARATOR idList COLON role
{
yy.addEntity($1);
yy.addEntity($5);
yy.addRelationship($1, $9, $5, $4);
yy.setClass([$1], $3);
yy.setClass([$5], $7);
}
| entityName STYLE_SEPARATOR idList aggregationRelSpec entityName COLON role
{
yy.addEntity($1);
yy.addEntity($5);
yy.addRelationship($1, $7, $5, $4);
yy.setClass([$1], $3);
}
| entityName aggregationRelSpec entityName STYLE_SEPARATOR idList COLON role
{
yy.addEntity($1);
yy.addEntity($3);
yy.addRelationship($1, $7, $3, $2);
yy.setClass([$3], $5);
}
| entityName 'AGGREGATION' entityName COLON role
{
yy.addEntity($1);
yy.addEntity($3);
yy.addRelationship($1, $5, $3, { cardA: 'ZERO_OR_MORE', relType: 'AGGREGATION', cardB: 'ZERO_OR_MORE' });
}
| entityName 'AGGREGATION_DASHED' entityName COLON role
{
yy.addEntity($1);
yy.addEntity($3);
yy.addRelationship($1, $5, $3, { cardA: 'ZERO_OR_MORE', relType: 'AGGREGATION_DASHED', cardB: 'ZERO_OR_MORE' });
}
| title title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
@@ -272,6 +315,17 @@ relSpec
}
;
aggregationRelSpec
: 'AGGREGATION' cardinality cardinality
{
$$ = { cardA: $2, relType: $1, cardB: $3 };
}
| 'AGGREGATION_DASHED' cardinality cardinality
{
$$ = { cardA: $2, relType: $1, cardB: $3 };
}
;
cardinality
: 'ZERO_OR_ONE' { $$ = yy.Cardinality.ZERO_OR_ONE; }
| 'ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE; }
@@ -283,6 +337,8 @@ cardinality
relType
: 'NON_IDENTIFYING' { $$ = yy.Identification.NON_IDENTIFYING; }
| 'IDENTIFYING' { $$ = yy.Identification.IDENTIFYING; }
| 'AGGREGATION' { $$ = yy.Aggregation.AGGREGATION; }
| 'AGGREGATION_DASHED' { $$ = yy.Aggregation.AGGREGATION_DASHED; }
;
role

View File

@@ -1001,4 +1001,75 @@ describe('when parsing ER diagram it...', function () {
}
);
});
describe('aggregation relationships', function () {
it('should parse basic aggregation syntax', function () {
erDiagram.parser.parse('erDiagram\nDEPARTMENT <> EMPLOYEE : contains');
const rels = erDb.getRelationships();
expect(erDb.getEntities().size).toBe(2);
expect(rels.length).toBe(1);
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
expect(rels[0].roleA).toBe('contains');
});
it('should parse dashed aggregation syntax', function () {
erDiagram.parser.parse('erDiagram\nPROJECT <>.. TASK : manages');
const rels = erDb.getRelationships();
expect(erDb.getEntities().size).toBe(2);
expect(rels.length).toBe(1);
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION_DASHED);
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
expect(rels[0].roleA).toBe('manages');
});
it('should parse aggregation with quoted labels', function () {
erDiagram.parser.parse('erDiagram\nUNIVERSITY <> COLLEGE : "has multiple"');
const rels = erDb.getRelationships();
expect(erDb.getEntities().size).toBe(2);
expect(rels.length).toBe(1);
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
expect(rels[0].roleA).toBe('has multiple');
});
it('should parse multiple aggregation relationships', function () {
erDiagram.parser.parse(
'erDiagram\nDEPARTMENT <> EMPLOYEE : contains\nPROJECT <>.. TASK : manages'
);
const rels = erDb.getRelationships();
expect(erDb.getEntities().size).toBe(4);
expect(rels.length).toBe(2);
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
expect(rels[1].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION_DASHED);
});
it('should parse aggregation with entity aliases', function () {
erDiagram.parser.parse('erDiagram\nd[DEPARTMENT]\ne[EMPLOYEE]\nd <> e : contains');
const rels = erDb.getRelationships();
expect(erDb.getEntities().size).toBe(2);
expect(rels.length).toBe(1);
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
expect(erDb.getEntity('d').alias).toBe('DEPARTMENT');
expect(erDb.getEntity('e').alias).toBe('EMPLOYEE');
});
it('should validate aggregation relationships', function () {
erDiagram.parser.parse('erDiagram\nDEPARTMENT <> EMPLOYEE : contains');
const rels = erDb.getRelationships();
expect(erDb.validateAggregationRelationship(rels[0].relSpec)).toBe(true);
});
it('should handle mixed relationship types', function () {
erDiagram.parser.parse(
'erDiagram\nCUSTOMER ||--o{ ORDER : places\nPRODUCT <> ORDER_ITEM : "aggregated in"'
);
const rels = erDb.getRelationships();
expect(erDb.getEntities().size).toBe(4);
expect(rels.length).toBe(2);
expect(rels[0].relSpec.relType).toBe(erDb.Identification.IDENTIFYING);
expect(rels[1].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
});
});
});

View File

@@ -68,6 +68,32 @@ const getStyles = (options: FlowChartStyleOptions) =>
stroke: ${options.lineColor} !important;
stroke-width: 1;
}
.aggregation {
stroke: ${options.lineColor};
stroke-width: 1;
fill: white;
}
.aggregation-dashed {
stroke: ${options.lineColor};
stroke-width: 1;
stroke-dasharray: 8,8;
fill: white;
}
.aggregation-marker {
fill: white !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}
.aggregation-marker-dashed {
fill: white !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
stroke-dasharray: 3,3;
}
`;
export default getStyles;

View File

@@ -1,6 +1,6 @@
# Frequently Asked Questions
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/1433#issuecomment-1991554712)
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217)
1. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785)
1. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621)
1. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)

View File

@@ -151,6 +151,35 @@ erDiagram
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
```
### Aggregation
Aggregation represents a "has-a" relationship where the part can exist independently of the whole. This is different from composition, where the part cannot exist without the whole. Aggregation relationships are rendered with hollow diamond markers at the endpoints.
| Value | Alias for | Description |
| :---: | :------------------: | ------------------------------ |
| <> | _aggregation_ | Basic aggregation (solid line) |
| <>.. | _aggregation-dashed_ | Dashed aggregation line |
**Examples:**
```mermaid-example
erDiagram
DEPARTMENT <> EMPLOYEE : contains
PROJECT <>.. TASK : manages
TEAM <> MEMBER : consists_of
```
In these examples:
- `DEPARTMENT <> EMPLOYEE` shows that a department contains employees (aggregation)
- `PROJECT <>.. TASK` shows that a project manages tasks (dashed aggregation)
- `TEAM <> MEMBER` shows that a team consists of members (aggregation)
**Aggregation vs Association**
- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently
- **Association** (`||--`, `}o--`): General relationship between entities
### Attributes
Attributes can be defined for entities by specifying the entity name followed by a block containing multiple `type name` pairs, where a block is delimited by an opening `{` and a closing `}`. The attributes are rendered inside the entity boxes. For example: