sirius-framework-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/README.md +160 -0
- package/dist/grammars/tree-sitter-java.wasm +0 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +247 -0
- package/dist/index.js.map +1 -0
- package/dist/java-parser.d.ts +25 -0
- package/dist/java-parser.js +281 -0
- package/dist/java-parser.js.map +1 -0
- package/dist/prompts/index.d.ts +9 -0
- package/dist/prompts/index.js +15 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/workflows.d.ts +27 -0
- package/dist/prompts/workflows.js +317 -0
- package/dist/prompts/workflows.js.map +1 -0
- package/dist/resources/biz/analytics.md +157 -0
- package/dist/resources/biz/biz-controller.md +151 -0
- package/dist/resources/biz/codelists.md +154 -0
- package/dist/resources/biz/entity-triple.md +142 -0
- package/dist/resources/biz/importer.md +153 -0
- package/dist/resources/biz/isenguard.md +156 -0
- package/dist/resources/biz/jobs.md +145 -0
- package/dist/resources/biz/processes.md +155 -0
- package/dist/resources/biz/storage.md +149 -0
- package/dist/resources/biz/tenants.md +159 -0
- package/dist/resources/biz/testing.md +127 -0
- package/dist/resources/db/composites.md +145 -0
- package/dist/resources/db/entities.md +156 -0
- package/dist/resources/db/mixing.md +176 -0
- package/dist/resources/db/queries.md +178 -0
- package/dist/resources/db/refs.md +135 -0
- package/dist/resources/index.d.ts +27 -0
- package/dist/resources/index.js +68 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/kernel/async.md +189 -0
- package/dist/resources/kernel/commons.md +203 -0
- package/dist/resources/kernel/config.md +155 -0
- package/dist/resources/kernel/di.md +138 -0
- package/dist/resources/kernel/lifecycle.md +146 -0
- package/dist/resources/loader.d.ts +9 -0
- package/dist/resources/loader.js +17 -0
- package/dist/resources/loader.js.map +1 -0
- package/dist/resources/web/controllers.md +151 -0
- package/dist/resources/web/services.md +136 -0
- package/dist/resources/web/templates.md +162 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/introspection.d.ts +55 -0
- package/dist/tools/introspection.js +233 -0
- package/dist/tools/introspection.js.map +1 -0
- package/dist/tools/scaffold.d.ts +64 -0
- package/dist/tools/scaffold.js +505 -0
- package/dist/tools/scaffold.js.map +1 -0
- package/dist/workspace.d.ts +37 -0
- package/dist/workspace.js +185 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
Tests in sirius-biz are written in Kotlin using JUnit 5 with the `SiriusExtension`.
|
|
4
|
+
This extension boots the full Sirius framework (DI, databases, config) before tests
|
|
5
|
+
run, giving you access to real services and database connections.
|
|
6
|
+
|
|
7
|
+
## Test Structure
|
|
8
|
+
|
|
9
|
+
- **Test files:** `src/test/kotlin/sirius/biz/**/*Test.kt` (Kotlin)
|
|
10
|
+
- **Test entities:** `src/test/java/sirius/biz/**/*.java` (Java, for test-only entities)
|
|
11
|
+
- **Test config:** `src/test/resources/test.conf`
|
|
12
|
+
- **Test suite:** `TestSuite.java` using `ScenarioSuite`
|
|
13
|
+
|
|
14
|
+
## SiriusExtension and DI
|
|
15
|
+
|
|
16
|
+
Every test class must use `@ExtendWith(SiriusExtension::class)`. This boots the
|
|
17
|
+
Sirius framework once per test run (shared across all test classes).
|
|
18
|
+
|
|
19
|
+
Use `@Part` in a `companion object` with `@JvmStatic` to inject services.
|
|
20
|
+
Always wait for database readiness in `@BeforeAll`:
|
|
21
|
+
|
|
22
|
+
```kotlin
|
|
23
|
+
@BeforeAll
|
|
24
|
+
@JvmStatic
|
|
25
|
+
fun setup() {
|
|
26
|
+
oma.readyFuture.await(Duration.ofSeconds(60))
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Important:** `@Part` fields must be in the `companion object` (static), not on
|
|
31
|
+
the test instance. Sirius DI only injects static fields.
|
|
32
|
+
|
|
33
|
+
## Test Naming
|
|
34
|
+
|
|
35
|
+
Use Kotlin backtick syntax for descriptive test names. This produces readable test
|
|
36
|
+
output while being valid Kotlin method names.
|
|
37
|
+
|
|
38
|
+
## Complete Example
|
|
39
|
+
|
|
40
|
+
```kotlin
|
|
41
|
+
@ExtendWith(SiriusExtension::class)
|
|
42
|
+
class TenantsTest {
|
|
43
|
+
|
|
44
|
+
companion object {
|
|
45
|
+
@Part
|
|
46
|
+
@JvmStatic
|
|
47
|
+
private lateinit var oma: OMA
|
|
48
|
+
|
|
49
|
+
@BeforeAll
|
|
50
|
+
@JvmStatic
|
|
51
|
+
fun setup() {
|
|
52
|
+
oma.readyFuture.await(Duration.ofSeconds(60))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Test
|
|
57
|
+
fun `installTestTenant works`() {
|
|
58
|
+
TenantsHelper.installTestTenant()
|
|
59
|
+
assertTrue {
|
|
60
|
+
UserContext.get().getUser().hasPermission(UserInfo.PERMISSION_LOGGED_IN)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Test Entities
|
|
67
|
+
|
|
68
|
+
Test-only entities live in `src/test/java/` (not `src/main/java/`). They are
|
|
69
|
+
written in Java (not Kotlin) because the Mixing ORM annotation processor requires
|
|
70
|
+
Java source files:
|
|
71
|
+
|
|
72
|
+
```java
|
|
73
|
+
// src/test/java/sirius/biz/mymodule/TestProduct.java
|
|
74
|
+
@Framework("test")
|
|
75
|
+
public class TestProduct extends BizEntity {
|
|
76
|
+
@Autoloaded
|
|
77
|
+
@Length(100)
|
|
78
|
+
private String name;
|
|
79
|
+
|
|
80
|
+
// getters and setters
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Enable the test framework in `test.conf`:
|
|
85
|
+
|
|
86
|
+
```hocon
|
|
87
|
+
sirius.frameworks {
|
|
88
|
+
test = true
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Test Configuration
|
|
93
|
+
|
|
94
|
+
`src/test/resources/test.conf` enables the frameworks needed for testing:
|
|
95
|
+
|
|
96
|
+
```hocon
|
|
97
|
+
sirius.frameworks {
|
|
98
|
+
biz.tenants = true
|
|
99
|
+
biz.tenants-jdbc = true
|
|
100
|
+
biz.storage = true
|
|
101
|
+
biz.storage-blob-jdbc = true
|
|
102
|
+
biz.isenguard = true
|
|
103
|
+
biz.processes = true
|
|
104
|
+
biz.locks = true
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Docker services (MariaDB, MongoDB, Elasticsearch, Redis) must be running for
|
|
109
|
+
integration tests. Use `docker-compose up -d` in the project root.
|
|
110
|
+
|
|
111
|
+
## Common Mistakes
|
|
112
|
+
|
|
113
|
+
1. **`@Part` on instance fields** — DI injection only works on static fields.
|
|
114
|
+
Place `@Part` fields in the `companion object` with `@JvmStatic`.
|
|
115
|
+
|
|
116
|
+
2. **Missing `@BeforeAll` database wait** — Tests that run before the schema is
|
|
117
|
+
ready will fail intermittently. Always await `oma.readyFuture`.
|
|
118
|
+
|
|
119
|
+
3. **Test entities in Kotlin** — The Mixing ORM requires Java source files for
|
|
120
|
+
entity classes. Test entities must be `.java` files even though tests are Kotlin.
|
|
121
|
+
|
|
122
|
+
4. **Not cleaning up test data** — Tests share the same database. Create unique
|
|
123
|
+
test data (e.g., with random suffixes) or clean up in `@AfterEach` to avoid
|
|
124
|
+
interference between tests.
|
|
125
|
+
|
|
126
|
+
5. **Forgetting `@JvmStatic`** — Without `@JvmStatic` on companion object fields,
|
|
127
|
+
Sirius DI cannot find and populate the fields.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Composites
|
|
2
|
+
|
|
3
|
+
A `Composite` is a reusable group of fields that can be embedded into any entity
|
|
4
|
+
or other composite. It lives in `sirius.db.mixing.Composite` and extends `Mixable`,
|
|
5
|
+
meaning it can itself be extended via mixins.
|
|
6
|
+
|
|
7
|
+
## How Composites Work
|
|
8
|
+
|
|
9
|
+
When a composite is declared as a field in an entity, all of the composite's fields
|
|
10
|
+
become properties of the entity. The field name of the composite is **prepended**
|
|
11
|
+
to each property name, separated by `_`:
|
|
12
|
+
|
|
13
|
+
```java
|
|
14
|
+
public class Customer extends SQLTenantAware {
|
|
15
|
+
private final PersonData person = new PersonData();
|
|
16
|
+
// Creates columns: person_title, person_firstname, person_lastname, etc.
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This prefixing means the same composite class can appear multiple times in one entity
|
|
21
|
+
under different field names without column conflicts:
|
|
22
|
+
|
|
23
|
+
```java
|
|
24
|
+
public class Order extends SQLTenantAware {
|
|
25
|
+
private final AddressData billingAddress = new AddressData();
|
|
26
|
+
private final AddressData shippingAddress = new AddressData();
|
|
27
|
+
// billingAddress_street, billingAddress_city, ...
|
|
28
|
+
// shippingAddress_street, shippingAddress_city, ...
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Mapping Constants
|
|
33
|
+
|
|
34
|
+
Every composite field should declare a `Mapping` constant to enable type-safe
|
|
35
|
+
references in queries and templates:
|
|
36
|
+
|
|
37
|
+
```java
|
|
38
|
+
public class PersonData extends Composite {
|
|
39
|
+
|
|
40
|
+
public static final Mapping FIRSTNAME = Mapping.named("firstname");
|
|
41
|
+
@Length(150)
|
|
42
|
+
@Trim
|
|
43
|
+
@Autoloaded
|
|
44
|
+
@NullAllowed
|
|
45
|
+
@AutoImport
|
|
46
|
+
private String firstname;
|
|
47
|
+
|
|
48
|
+
public static final Mapping LASTNAME = Mapping.named("lastname");
|
|
49
|
+
@Length(150)
|
|
50
|
+
@Trim
|
|
51
|
+
@Autoloaded
|
|
52
|
+
@NullAllowed
|
|
53
|
+
@AutoImport
|
|
54
|
+
private String lastname;
|
|
55
|
+
|
|
56
|
+
// getters/setters...
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Accessing Nested Fields with Mapping.inner()
|
|
61
|
+
|
|
62
|
+
When querying or referencing a composite field from the entity level, use
|
|
63
|
+
`Mapping.inner()` to build the prefixed path:
|
|
64
|
+
|
|
65
|
+
```java
|
|
66
|
+
// In a query on the Customer entity:
|
|
67
|
+
oma.select(Customer.class)
|
|
68
|
+
.eq(Customer.PERSON.inner(PersonData.LASTNAME), "Smith")
|
|
69
|
+
.queryList();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This resolves to the column `person_lastname` in the database.
|
|
73
|
+
|
|
74
|
+
## Standard Annotations
|
|
75
|
+
|
|
76
|
+
These annotations can be placed on fields in composites (and entities):
|
|
77
|
+
|
|
78
|
+
- `@Length(n)` — Maximum column/field length (required for strings).
|
|
79
|
+
- `@Trim` — Automatically trims whitespace on load/save.
|
|
80
|
+
- `@NullAllowed` — Permits null values. Without this, nulls cause validation errors.
|
|
81
|
+
- `@Unique` — Enforces uniqueness. Supports `within` parameter for scoped uniqueness.
|
|
82
|
+
- `@Transient` — Excludes the field from persistence entirely.
|
|
83
|
+
- `@Autoloaded` — Auto-populates the field from web request parameters.
|
|
84
|
+
- `@AutoImport` — Auto-populates the field during data import.
|
|
85
|
+
|
|
86
|
+
## Lifecycle Annotations
|
|
87
|
+
|
|
88
|
+
These method-level annotations on the containing entity (or composite) hook into
|
|
89
|
+
the persistence lifecycle:
|
|
90
|
+
|
|
91
|
+
- `@BeforeSave` — Called before the entity is written to the database. Use for
|
|
92
|
+
computed fields, normalization, or throwing exceptions to abort the save.
|
|
93
|
+
- `@OnValidate` — Called during validation. The annotated method receives a
|
|
94
|
+
`Consumer<String>` to collect warning messages without aborting.
|
|
95
|
+
- `@ValidatedBy(ValidatorClass.class)` — Delegates field validation to an external
|
|
96
|
+
validator class.
|
|
97
|
+
|
|
98
|
+
```java
|
|
99
|
+
public class InvoiceData extends Composite {
|
|
100
|
+
|
|
101
|
+
public static final Mapping INVOICE_NUMBER = Mapping.named("invoiceNumber");
|
|
102
|
+
@Length(50)
|
|
103
|
+
@NullAllowed
|
|
104
|
+
private String invoiceNumber;
|
|
105
|
+
|
|
106
|
+
@BeforeSave
|
|
107
|
+
protected void generateInvoiceNumber() {
|
|
108
|
+
if (Strings.isEmpty(invoiceNumber)) {
|
|
109
|
+
invoiceNumber = sequences.generateId("invoices");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@OnValidate
|
|
114
|
+
protected void validate(Consumer<String> validationConsumer) {
|
|
115
|
+
if (Strings.isEmpty(invoiceNumber)) {
|
|
116
|
+
validationConsumer.accept("Invoice number is required.");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## PersonData — Standard Example
|
|
123
|
+
|
|
124
|
+
`PersonData` in `sirius.biz.model` is the canonical composite example. It stores
|
|
125
|
+
title, salutation, firstname, and lastname. It provides helper methods like
|
|
126
|
+
`getAddressableName()` and `getShortName()`, and validation helpers
|
|
127
|
+
(`verifySalutation()`, `validateSalutation()`) that the containing entity can
|
|
128
|
+
call from its own `@BeforeSave` or `@OnValidate` methods.
|
|
129
|
+
|
|
130
|
+
## Common Mistakes
|
|
131
|
+
|
|
132
|
+
1. **Forgetting the prefix in queries** — A field `firstname` inside a composite
|
|
133
|
+
field `person` becomes `person_firstname` in the database. Always use
|
|
134
|
+
`Mapping.inner()` to construct the correct path.
|
|
135
|
+
|
|
136
|
+
2. **Not declaring Mapping constants** — Without `Mapping.named()` constants,
|
|
137
|
+
queries require raw strings, which are error-prone and not refactoring-safe.
|
|
138
|
+
|
|
139
|
+
3. **Mutable composite instances** — Composite fields should be declared `final`
|
|
140
|
+
in the entity. The composite object is created once and its internal fields are
|
|
141
|
+
mutated, but the composite reference itself should not change.
|
|
142
|
+
|
|
143
|
+
4. **Missing @NullAllowed** — String fields default to non-null. If a field
|
|
144
|
+
legitimately can be empty, annotate it with `@NullAllowed` or provide a
|
|
145
|
+
`@DefaultValue`.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Entities
|
|
2
|
+
|
|
3
|
+
Sirius-db provides three entity base classes, one per supported database. All share
|
|
4
|
+
a common ancestor: `BaseEntity<I>` (in `sirius.db.mixing`), which extends `Mixable`
|
|
5
|
+
and implements `Entity`. The type parameter `I` is the ID type.
|
|
6
|
+
|
|
7
|
+
## SQLEntity — JDBC / SQL Databases
|
|
8
|
+
|
|
9
|
+
`SQLEntity` uses `Long` IDs auto-generated by the database. It supports optimistic
|
|
10
|
+
locking via a `version` field that is automatically incremented on each update.
|
|
11
|
+
|
|
12
|
+
```java
|
|
13
|
+
public abstract class SQLEntity extends BaseEntity<Long> {
|
|
14
|
+
// id: long, auto-assigned by the database
|
|
15
|
+
// version: int, used for optimistic locking
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
An entity is considered new (not yet persisted) when `id < 0`. The sentinel value
|
|
20
|
+
is `SQLEntity.NON_PERSISTENT_ENTITY_ID` (-1).
|
|
21
|
+
|
|
22
|
+
**Mapper:** `OMA` (Object Mapper for rdbms Access)
|
|
23
|
+
|
|
24
|
+
## MongoEntity — MongoDB
|
|
25
|
+
|
|
26
|
+
`MongoEntity` uses `String` IDs generated by `KeyGenerator` -- NOT MongoDB's native
|
|
27
|
+
ObjectId. This produces short, URL-safe string identifiers. Optimistic locking is
|
|
28
|
+
supported via a `version` field.
|
|
29
|
+
|
|
30
|
+
```java
|
|
31
|
+
@Index(name = "id", columns = "id", columnSettings = Mango.INDEX_ASCENDING, unique = true)
|
|
32
|
+
public abstract class MongoEntity extends BaseEntity<String> {
|
|
33
|
+
// id: String, generated by KeyGenerator before insert
|
|
34
|
+
// version: int, used for optimistic locking
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
An entity is new when `id == null`. The ID is generated in `generateId()` during
|
|
39
|
+
the create operation, after before-save checks have executed.
|
|
40
|
+
|
|
41
|
+
**Mapper:** `Mango`
|
|
42
|
+
|
|
43
|
+
## ElasticEntity — Elasticsearch
|
|
44
|
+
|
|
45
|
+
`ElasticEntity` uses `String` IDs auto-generated by Elasticsearch. It uses
|
|
46
|
+
`primaryTerm` and `seqNo` for optimistic concurrency control instead of a simple
|
|
47
|
+
version counter.
|
|
48
|
+
|
|
49
|
+
```java
|
|
50
|
+
public abstract class ElasticEntity extends BaseEntity<String> {
|
|
51
|
+
// id: String, auto-generated by Elasticsearch
|
|
52
|
+
// primaryTerm, seqNo: used for optimistic concurrency
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For performance, annotate a field with `@RoutedBy` to enable custom routing:
|
|
57
|
+
|
|
58
|
+
```java
|
|
59
|
+
@RoutedBy(tenantRef)
|
|
60
|
+
public class EventLog extends ElasticEntity { ... }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Mapper:** `Elastic`
|
|
64
|
+
|
|
65
|
+
## Hierarchy in sirius-biz
|
|
66
|
+
|
|
67
|
+
sirius-biz extends these base classes to add tracing, journaling, and tenant
|
|
68
|
+
awareness. The full hierarchies are:
|
|
69
|
+
|
|
70
|
+
**SQL path:**
|
|
71
|
+
```
|
|
72
|
+
BaseEntity<Long>
|
|
73
|
+
+-- SQLEntity
|
|
74
|
+
+-- BizEntity (adds TraceData)
|
|
75
|
+
+-- SQLTenantAware (adds tenant reference)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**MongoDB path:**
|
|
79
|
+
```
|
|
80
|
+
BaseEntity<String>
|
|
81
|
+
+-- MongoEntity
|
|
82
|
+
+-- PrefixSearchableEntity (adds search prefix support)
|
|
83
|
+
+-- MongoBizEntity (adds TraceData)
|
|
84
|
+
+-- MongoTenantAware (adds tenant reference)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
When creating a tenant-aware entity, extend `SQLTenantAware` or `MongoTenantAware`.
|
|
88
|
+
When creating a non-tenant entity with tracing, extend `BizEntity` or `MongoBizEntity`.
|
|
89
|
+
|
|
90
|
+
## Entity Registration
|
|
91
|
+
|
|
92
|
+
Entities are discovered automatically at startup. Use `@Framework` to gate an entity
|
|
93
|
+
behind a framework flag:
|
|
94
|
+
|
|
95
|
+
```java
|
|
96
|
+
@Framework("biz.tenants-jdbc")
|
|
97
|
+
public class SQLTenant extends SQLTenantAware implements Tenant<Long> { ... }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The entity will only be registered (and its table/collection created) when the
|
|
101
|
+
framework flag `biz.tenants-jdbc` is enabled in the config.
|
|
102
|
+
|
|
103
|
+
## Concrete SQL Example
|
|
104
|
+
|
|
105
|
+
```java
|
|
106
|
+
@Framework("myapp.products")
|
|
107
|
+
public class Product extends SQLTenantAware {
|
|
108
|
+
|
|
109
|
+
public static final Mapping NAME = Mapping.named("name");
|
|
110
|
+
@Length(255)
|
|
111
|
+
@Trim
|
|
112
|
+
@Autoloaded
|
|
113
|
+
private String name;
|
|
114
|
+
|
|
115
|
+
public static final Mapping PRICE = Mapping.named("price");
|
|
116
|
+
@Autoloaded
|
|
117
|
+
private int price;
|
|
118
|
+
|
|
119
|
+
// getters/setters...
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Concrete Mongo Example
|
|
124
|
+
|
|
125
|
+
```java
|
|
126
|
+
@Framework("myapp.products")
|
|
127
|
+
public class MongoProduct extends MongoTenantAware {
|
|
128
|
+
|
|
129
|
+
public static final Mapping NAME = Mapping.named("name");
|
|
130
|
+
@Length(255)
|
|
131
|
+
@Trim
|
|
132
|
+
@Autoloaded
|
|
133
|
+
private String name;
|
|
134
|
+
|
|
135
|
+
// getters/setters...
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Common Mistakes
|
|
140
|
+
|
|
141
|
+
1. **Using MongoDB ObjectId** — MongoEntity does NOT use ObjectId. IDs are generated
|
|
142
|
+
as strings by `KeyGenerator`. Do not try to parse them as ObjectId.
|
|
143
|
+
|
|
144
|
+
2. **Setting the SQL ID manually** — The `id` field is managed by the database.
|
|
145
|
+
Never assign a value to `setId()` unless you are doing a low-level data migration.
|
|
146
|
+
|
|
147
|
+
3. **Forgetting @Framework** — Without a framework annotation, the entity is always
|
|
148
|
+
registered. This can cause table creation in databases where it is not wanted.
|
|
149
|
+
|
|
150
|
+
4. **Extending concrete entity classes** — Superclasses of entities should always
|
|
151
|
+
be `abstract`. The framework does not support merging distinct subclasses into
|
|
152
|
+
one table.
|
|
153
|
+
|
|
154
|
+
5. **Ignoring optimistic locking** — If a concurrent update causes a version
|
|
155
|
+
mismatch, an `OptimisticLockException` is thrown. Catch and handle it
|
|
156
|
+
(e.g., reload and retry) rather than letting it propagate as a 500 error.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Mixing ORM
|
|
2
|
+
|
|
3
|
+
Mixing is the core ORM (Object-Relational/Document Mapping) layer in sirius-db.
|
|
4
|
+
It provides a unified abstraction over SQL, MongoDB, and Elasticsearch, handling
|
|
5
|
+
entity discovery, schema management, property mapping, and validation.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
The Mixing system consists of three main components:
|
|
10
|
+
|
|
11
|
+
1. **`Mixing`** — The central registry. Discovers all entity classes at startup,
|
|
12
|
+
creates their descriptors, and provides lookup by class or name.
|
|
13
|
+
2. **`EntityDescriptor`** — Describes a single entity type: its properties,
|
|
14
|
+
lifecycle handlers, relation name, and validation rules.
|
|
15
|
+
3. **`Property`** — Maps a single Java field to a database column/field. Handles
|
|
16
|
+
type conversion, validation, and access.
|
|
17
|
+
|
|
18
|
+
## Mixing — The Registry
|
|
19
|
+
|
|
20
|
+
`Mixing` is a singleton (`@Register`) that initializes at startup by:
|
|
21
|
+
|
|
22
|
+
1. Scanning for all `BaseEntity` subclasses (via `EntityLoadAction`)
|
|
23
|
+
2. Creating an `EntityDescriptor` for each
|
|
24
|
+
3. Linking cross-references between descriptors
|
|
25
|
+
4. Optionally executing schema updates
|
|
26
|
+
|
|
27
|
+
Inject it with `@Part`:
|
|
28
|
+
|
|
29
|
+
```java
|
|
30
|
+
@Part
|
|
31
|
+
private Mixing mixing;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Key methods:
|
|
35
|
+
- `mixing.getDescriptor(Class)` — returns the descriptor for an entity class
|
|
36
|
+
- `mixing.getDescriptor(String)` — returns the descriptor by type name
|
|
37
|
+
(upper-cased simple class name, e.g., `"PRODUCT"`)
|
|
38
|
+
- `mixing.findDescriptor(Class)` — returns Optional (no exception if missing)
|
|
39
|
+
- `mixing.getDescriptors()` — returns all known descriptors
|
|
40
|
+
|
|
41
|
+
## EntityDescriptor
|
|
42
|
+
|
|
43
|
+
Each entity class has exactly one `EntityDescriptor`. It holds:
|
|
44
|
+
|
|
45
|
+
- **Properties** — the list of `Property` objects for each mapped field
|
|
46
|
+
- **Relation name** — the table/collection/index name in the database
|
|
47
|
+
- **Realm** — which database instance to use (via `@Realm` annotation)
|
|
48
|
+
- **Lifecycle handlers** — methods annotated with `@BeforeSave`, `@AfterSave`,
|
|
49
|
+
`@BeforeDelete`, `@AfterDelete`, `@OnValidate`
|
|
50
|
+
- **Version flag** — whether optimistic locking is enabled (`@Versioned`)
|
|
51
|
+
|
|
52
|
+
```java
|
|
53
|
+
EntityDescriptor descriptor = mixing.getDescriptor(Product.class);
|
|
54
|
+
descriptor.getRelationName(); // e.g., "product"
|
|
55
|
+
descriptor.getRealm(); // e.g., "mixing" (default)
|
|
56
|
+
descriptor.getProperties(); // all Property objects
|
|
57
|
+
descriptor.isVersioned(); // true if @Versioned
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Change detection is built into the descriptor:
|
|
61
|
+
|
|
62
|
+
```java
|
|
63
|
+
descriptor.isChanged(entity, property); // checks if a field was modified
|
|
64
|
+
entity.isChanged(Product.NAME); // convenience on the entity itself
|
|
65
|
+
entity.isAnyMappingChanged(); // any field changed at all
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Property Abstraction
|
|
69
|
+
|
|
70
|
+
A `Property` bridges a Java field and its database representation:
|
|
71
|
+
|
|
72
|
+
- `getName()` — the effective property name (prefixed for composites/mixins)
|
|
73
|
+
- `getPropertyName()` — the column/field name in the database
|
|
74
|
+
- `getValue(entity)` — reads the current value from the entity
|
|
75
|
+
- `setValue(entity, value)` — writes a value to the entity
|
|
76
|
+
- `getValueForDatasource(mapperClass, entity)` — converts the value for storage
|
|
77
|
+
- `parseValue(entity, Value)` — converts a raw value back into the Java type
|
|
78
|
+
|
|
79
|
+
Properties are created by `PropertyFactory` implementations. Each Java type
|
|
80
|
+
(String, int, LocalDateTime, EntityRef, Composite, etc.) has a corresponding
|
|
81
|
+
property factory that knows how to map it.
|
|
82
|
+
|
|
83
|
+
## Entity Discovery and @Framework
|
|
84
|
+
|
|
85
|
+
Entities are discovered by scanning for all concrete subclasses of `BaseEntity`.
|
|
86
|
+
To conditionally include an entity, use `@Framework`:
|
|
87
|
+
|
|
88
|
+
```java
|
|
89
|
+
@Framework("myapp.products")
|
|
90
|
+
public class Product extends SQLTenantAware { ... }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If the framework flag `myapp.products` is not enabled in `sirius.frameworks`,
|
|
94
|
+
the entity class is not loaded, its table is not created, and its descriptor
|
|
95
|
+
does not exist in the `Mixing` registry.
|
|
96
|
+
|
|
97
|
+
## Mapper Hierarchy
|
|
98
|
+
|
|
99
|
+
Each database backend has its own mapper that extends `BaseMapper`:
|
|
100
|
+
|
|
101
|
+
| Mapper | Entity Base | Query Type | ID Type |
|
|
102
|
+
|--------|-------------|------------|---------|
|
|
103
|
+
| `OMA` | `SQLEntity` | `SmartQuery` | `Long` |
|
|
104
|
+
| `Mango` | `MongoEntity` | `MongoQuery` | `String` |
|
|
105
|
+
| `Elastic` | `ElasticEntity` | `ElasticQuery` | `String` |
|
|
106
|
+
|
|
107
|
+
All mappers provide the same core operations: `find()`, `select()`, `update()`,
|
|
108
|
+
`delete()`, `tryUpdate()`, `tryDelete()`.
|
|
109
|
+
|
|
110
|
+
## Descriptor-Based Validation
|
|
111
|
+
|
|
112
|
+
Validation runs automatically before save via the descriptor. Sources of
|
|
113
|
+
validation rules include:
|
|
114
|
+
|
|
115
|
+
- **@Length** — property length check
|
|
116
|
+
- **@NullAllowed** — null check (fields are non-null by default)
|
|
117
|
+
- **@Unique** — uniqueness check via database query
|
|
118
|
+
- **@OnValidate** — custom validation methods on the entity or composite
|
|
119
|
+
- **@ValidatedBy** — external validator class
|
|
120
|
+
- **@BeforeSave** — pre-save hooks that can throw to abort
|
|
121
|
+
|
|
122
|
+
```java
|
|
123
|
+
@OnValidate
|
|
124
|
+
protected void validate(Consumer<String> validationConsumer) {
|
|
125
|
+
if (Strings.isEmpty(getName())) {
|
|
126
|
+
validationConsumer.accept("Name is required.");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Validation messages collected by `@OnValidate` are warnings. To hard-fail,
|
|
132
|
+
throw an exception in a `@BeforeSave` handler instead.
|
|
133
|
+
|
|
134
|
+
## Schema Synchronization
|
|
135
|
+
|
|
136
|
+
Mixing can automatically update database schemas at startup. The behavior is
|
|
137
|
+
controlled by `mixing.autoUpdateSchema` in the config:
|
|
138
|
+
|
|
139
|
+
- `"safe"` — executes non-destructive changes (add columns, create tables)
|
|
140
|
+
- `"all"` — executes all changes including potentially destructive ones
|
|
141
|
+
- `"off"` — no automatic schema changes
|
|
142
|
+
|
|
143
|
+
## Mixins
|
|
144
|
+
|
|
145
|
+
Mixins add fields to existing entities without modifying them. A mixin targets
|
|
146
|
+
a specific entity type via `@Mixin`:
|
|
147
|
+
|
|
148
|
+
```java
|
|
149
|
+
@Mixin(Product.class)
|
|
150
|
+
public class ProductExtension extends Mixable {
|
|
151
|
+
public static final Mapping EXTERNAL_ID = Mapping.named("externalId");
|
|
152
|
+
@Length(100) @NullAllowed
|
|
153
|
+
private String externalId;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
This adds an `externalId` column to the `Product` table. Useful for extending
|
|
158
|
+
framework-provided entities from application code.
|
|
159
|
+
|
|
160
|
+
## Common Mistakes
|
|
161
|
+
|
|
162
|
+
1. **Querying before Mixing is ready** — At startup, `Mixing.initialize()` must
|
|
163
|
+
complete before any database operations. In tests, await `oma.readyFuture`
|
|
164
|
+
or `mango.readyFuture`.
|
|
165
|
+
|
|
166
|
+
2. **Using getDescriptor() for unregistered classes** — If the entity's framework
|
|
167
|
+
flag is disabled, `getDescriptor()` throws. Use `findDescriptor()` when the
|
|
168
|
+
entity may not exist.
|
|
169
|
+
|
|
170
|
+
3. **Confusing property name and field name** — A composite field `person` with
|
|
171
|
+
a sub-field `firstname` produces property name `person_firstname`. The Java
|
|
172
|
+
field name is just `firstname`.
|
|
173
|
+
|
|
174
|
+
4. **Ignoring the realm** — Entities default to the `"mixing"` realm. If your
|
|
175
|
+
application uses multiple databases, annotate entities with `@Realm("other")`
|
|
176
|
+
to direct them to the correct database.
|