Entity Engine
The Klyn Entity Engine provides one statically typed model for entities, identities, queries, relationships, schema management, and persistence. Relational databases are its most complete writable provider, but the same entity concepts also apply to XML, JSON, and in-memory sources.
An object-relational mapper associates application objects with relational tables and rows. The Entity Engine includes that role for SQL databases, then extends the same typed entity and query model to sources that have no table at all. An entity is therefore a mapped domain object, not a synonym for a database row.
| Provider | Entity Engine role |
|---|---|
| SQL | Tracked entities, transactions, CRUD, schema management, relationships, inheritance, and translated KQL. |
| XML | Read-only typed entities selected and mapped from one atomic XML document generation. |
| JSON | Read-only typed entities selected and mapped with JSONPath expressions. |
| Memory | Typed repositories over existing collections, with key validation and KQL execution. |
Provider capabilities remain explicit. A mapping or operation that the selected provider cannot honor raises a mapping, translation, or capability exception; Klyn never silently changes the execution model.
| Type | Responsibility |
|---|---|
ProviderConfiguration | Immutable source-specific connection or document settings. |
EntityManagerFactory | Thread-safe owner of the validated entity model, provider metadata, and schema manager. |
EntityManager | Short-lived, non-thread-safe persistence context with identity tracking and one unit of work. |
Repository<T, TId> | Typed query root. The identity type can be inferred from the effective mapping. |
Query<T> | Immutable lazy query executed by an explicit terminal operation. |
SchemaManager | Physical schema creation, removal, validation, comparison, and migration-plan application. |
A factory can be shared safely. An EntityManager must stay inside one request,
command, or worker and must never be shared between threads.
Provider-neutral annotations live in klyn.data.mapping. SQL bindings live in
klyn.data.sql. Matching property and column names are inferred, so annotations should
describe only identity, constraints, relationships, generation, or physical names that differ.
import klyn.data.mapping
import klyn.data.sql
@Entity
@Table(name="T_Articles")
class Article:
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
public property id as Int = 0
@Column(nullable=false, length=120)
public property name as String
@Column(nullable=false, precision=12, scale=2)
public property price as Double
public Article():
pass
public Article(name as String, price as Double):
this.name = name
this.price = price
The parameterless constructor lets the materializer instantiate the entity. The effective key
comes from @Id, @EmbeddedId, or @IdClass. Every mapped property
is validated against the selected provider before data access begins.
The configuration selects the provider. The entities argument separately defines the
complete and authoritative model visible to the factory. This explicit boundary prevents an
unrelated annotated class on the classpath from changing a schema or query unexpectedly.
import klyn.data
import klyn.data.sql
configuration = SQLConfiguration(
url="mariadb://localhost:3306/klyn",
login="klyn",
password="secret",
showSQL=false,
formatSQL=false
)
try factory = EntityManagerFactory(
configuration,
entities=[Blog, Post]
):
assert Blog.type in factory.mappedTypes
assert Post.type in factory.mappedTypes
A class name used as a value is its reflection Type, so [Blog, Post] is the
concise form of [Blog.type, Post.type]. The list is copied, validated, ordered
deterministically, and then kept immutable. Every relationship target must belong to this model.
SQLConfiguration argument | Meaning |
|---|---|
url | Database URL. MariaDB, PostgreSQL, and SQL Server select their dialect automatically. |
login / password | Credentials passed to the SQL driver. Never embed production secrets in source. |
dialect | Optional explicit SQL dialect; required when an ODBC URL cannot identify server semantics. |
showSQL | Logs generated SQL when enabled. Defaults to false. |
formatSQL | Formats logged SQL for readability. Defaults to false. |
queryTimeoutSeconds | Provider statement timeout in seconds; 0 means no explicit deadline. |
odbcConfiguration = SQLConfiguration(
url="odbc:DSN=Reporting",
login="reporter",
password="secret",
dialect=PostgreSQLDialect()
)
The factory-owned schema manager uses the same validated mappings as queries and persistence.
No-argument operations address the complete entity model; overloads receiving a Type
address one entity.
schema = factory.schema
if not schema.exists(Article.type):
schema.create(Article.type)
schema.validate(Article.type)
plan = schema.diff()
if not plan.empty:
print(plan.preview)
schema.apply(plan)
Existing structures are validated rather than altered silently. Inspect a schema plan before
applying it, especially when plan.destructive is true.
Entity mutations belong to the manager. Use try-with-resources, keep transactions short, flush
with saveChanges(), and roll back every exceptional path.
try factory = EntityManagerFactory(configuration, entities=[Article]),
em = factory.createEntityManager():
transaction = em.beginTransaction()
try:
article = Article("Mechanical keyboard", 129.90)
em.persist(article)
assert em.saveChanges() == 1
transaction.commit()
catch error as Exception:
if transaction.active:
transaction.rollback()
throw error
loaded = em.find<Article>(article.id)
loaded.price = 119.90
em.saveChanges()
em.remove(loaded)
em.saveChanges()
find<T>(id) first checks the identity map. persist() registers a new
entity, merge() copies detached state into a managed instance, remove()
schedules deletion, and refresh() reloads provider state.
For manager-backed entity work, prefer query(em):. The compiler resolves the mapped
entity, infers the exact result type, validates every member, and lowers the query to the
provider-neutral query representation before the SQL provider emits parameterized SQL.
minimum = 50.0
maximum = 150.0
selected = query(em):
from Article
where price >= minimum and price <= maximum
order by price asc
for article in selected.toList():
print(f"{article.name}: {article.price}")
The repository form remains useful for provider-independent functions and direct document repositories. See Entity Engine KQL for the complete query syntax and execution model.
Relational examples dominate this introduction because SQL currently exposes the complete writable lifecycle. The mapping model also supports XML entities, JSON entities, and in-memory entities. These pages document their source-specific annotations, validation, and capability boundaries.
samples/sql/Entities.kn builds a MariaDB article catalog, creates its schema, inserts
ten entities transactionally, and executes a typed price query from a desktop GUI.
klyn samples/sql/Entities.kn