Entity Engine & XML
The XML provider maps elements and attributes to statically typed entities, validates their
identities, reconstructs mapped relationships, and executes KQL over one consistent document
generation through XMLRepository<T, TId>.
| Capability | XML provider behavior |
|---|---|
| Access mode | Read-only typed repository. |
| Consistency | The complete file is read before parsing; bytes from two file generations are never mixed. |
| Identity | @Id, @EmbeddedId, or @IdClass, bound to elements or attributes. |
| Queries | Provider-neutral KQL over materialized mapped entities. |
| Relationships | Owning paths plus inverse mappedBy reconstruction; eager and fetch-join loading. |
| Mutation | Not supported by the current XML repository. |
Assume a document containing user elements. An XML identity may live in an attribute while ordinary values live in child elements.
<?xml version="1.0" encoding="UTF-8"?>
<users>
<user id="1">
<login>alice</login>
<connectionNumber>4</connectionNumber>
<active>true</active>
</user>
<user id="2">
<login>bob</login>
<connectionNumber>14</connectionNumber>
<active>true</active>
</user>
</users>
Combine provider-neutral annotations from klyn.data.mapping with physical XML paths
from klyn.data.xml.
import klyn.data.mapping
import klyn.data.xml
@Entity
@XMLCollection(path="/users/user")
class User:
@Id
@XMLAttribute(path="@id")
public property id as Int = 0
@XMLElement(path="login")
public property login as String
@XMLElement(path="connectionNumber")
public property connectionNumber as Int = 0
@XMLElement(path="active")
public property active as Boolean = false
public User():
pass
| Annotation | Purpose |
|---|---|
@XMLCollection(path="/users/user") | Selects the absolute set of entity elements. |
@XMLAttribute(path="@id") | Reads an attribute from the current entity element. |
@XMLElement(path="login") | Reads a child element relative to the current entity element. |
Paths are validated while mapping metadata is built. Missing required values, invalid scalar conversions, null identities, and incompatible property types raise an Entity Engine exception instead of producing partially initialized objects.
XMLConfiguration accepts either a string path or a Path. The repository's
generic identity type must exactly match the effective identity mapping.
import klyn.data
import klyn.data.xml
configuration = XMLConfiguration("data/users.xml")
users = XMLRepository<User, Int>.open(configuration)
assert users.providerName() == "xml"
assert users.count() == 2l
Opening the repository validates mapping metadata but does not retain an open file handle. Each execution reads a complete document generation.
XML currently uses the explicit repository form because it is a direct read-only document
source rather than a managed persistence context. The query remains statically typed and lowers
to the same provider-neutral representation as query(em):.
frequentUsers = query:
from users user
where user.active and user.connectionNumber > 10
order by user.connectionNumber desc, user.login
for user in frequentUsers.toList():
print(user.login)
count(), exists(), first(), single(),
toList(), and stream() retain their ordinary KQL contracts.
Entity keys must be present and unique. validateBeforeRead() forces complete key and
relationship validation before the caller consumes results.
users.validateBeforeRead()
userQuery = query:
from users user
where user.id == requestedId
user = userQuery.singleOrNull()
A duplicate key raises DuplicateIdentityException with the first and repeated source
locations. A null or unconvertible key raises an entity mapping exception.
Relationship annotations remain provider-neutral. An owning XML relationship must also declare
the physical path that selects a nested entity, identity attribute, entity element, or repeated
elements. An inverse mappedBy relationship declares no XML path and is reconstructed
from the owning endpoint.
@ManyToOne(optional=false)
@XMLAttribute(path="@customerId")
public property customer as Customer
@OneToMany(mappedBy="customer")
public property orders as List<Order> = []
A to-many owning relationship uses @XMLElement, not @XMLAttribute.
FetchType.EAGER relationships are populated automatically; lazy relationships can be
requested by a KQL join fetch path.
Register an IndexDescriptor<T> for frequently filtered logical properties. The
descriptor resolves its property path once, remains statically associated with the entity type,
and writes type-aware index metadata beside the XML source.
configuration = XMLConfiguration(
"data/users.xml",
indexes=[IndexDescriptor<User>("login", unique=true)]
)
users = XMLRepository<User, Int>.open(configuration)
The sidecar belongs to one exact atomic source generation and is rebuilt when that generation
changes. An index can reduce QIR row evaluation, but XML parsing remains fully buffered. Inspect
query.explain() to confirm index selection; unique=true rejects duplicate
indexed values during preparation.
- Replace complete XML files atomically when another process writes them.
- Do not assume that two separate query executions observe the same file generation.
- Use
validateBeforeRead()when no item may be consumed before complete validation. - Strict validation pins that immutable generation for subsequent non-fetch executions on the repository.
- Close
QueryStream<T>with try-with-resources. - Treat external XML as untrusted input and keep entity/path limits appropriate to the application.
Continue with Entity Engine and JSON for JSONPath-based entity mapping.