Databases & Entities Relations SQL mapping

Entity Engine Relations

Entity relationships are ordinary typed properties backed by validated mapping metadata. The Entity Engine manages ownership, foreign keys, loading, identity fix-up, cascades, orphan removal, and inheritance without exposing persistence proxies in application source.

Relationship Model
AnnotationCardinalityDefault fetchTypical owner
@OneToOneOne source to zero or one targetEAGEREndpoint carrying @JoinColumn
@OneToManyOne source to many targetsLAZYUsually the target's @ManyToOne
@ManyToOneMany sources to one targetEAGEREndpoint carrying the foreign key
@ManyToManyMany sources to many targetsLAZYEndpoint carrying @JoinTable

The owning endpoint defines the physical association. The inverse endpoint declares mappedBy="propertyName", where the property name belongs to the target entity. Both endpoints still expose direct entity values such as post.blog and blog.posts.

One-to-Many Blog Model

The following model is based on the relational integration scenario in tests/klyn/data/db/OneToManyRelationTest.kn. Post.blog owns the foreign key; Blog.posts is its inverse collection.

import klyn.collections
import klyn.data.mapping
import klyn.data.sql
import klyn.time

@Entity
@Table(name="T_Blogs")
class Blog:

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="idBlog")
    public property identifier as Int = 0

    @Column(nullable=false)
    public property url as String

    @Column(nullable=false)
    public property name as String

    @Column(nullable=false)
    public property description as String

    @OneToMany(mappedBy="blog", cascade=[CascadeType.PERSIST])
    public property posts as List<Post> = []

    public Blog():
        this("", "", "")

    public Blog(url as String, name as String, description as String):
        this.url = url
        this.name = name
        this.description = description

    public addPost(post as Post) as Void:
        post.blog = this
        this.posts.add(post)


@Entity
@Table(name="T_Posts")
class Post:

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    public property idPost as Int = 0

    @Column(nullable=false)
    public property date as DateTime

    @Column(nullable=false)
    public property title as String

    @Column(nullable=false)
    public property content as String

    @ManyToOne(optional=false)
    @JoinColumn(name="idBlog", nullable=false)
    public property blog as Blog

    public Post(title as String = "", content as String = ""):
        this.date = DateTime.now
        this.title = title
        this.content = content
Keep both endpoints coherent

A helper such as addPost() updates the collection and the owning reference together. The Entity Engine validates and persists the graph, but domain code should not deliberately leave its two in-memory endpoints contradictory.

Declare the Complete Model

Every relationship target must be part of the factory's entity model. A missing target is a mapping error raised while the factory is acquired, before schema or query work starts.

configuration = SQLConfiguration(
    "mariadb://localhost:3306/klyn",  # Database URL
    "klyn",                           # Login
    "secret"                          # Password
)

try factory = EntityManagerFactory(
    configuration,
    entities=[Blog, Post]
):
    factory.schema.validate()
Persist a Connected Graph

CascadeType.PERSIST on Blog.posts lets one call register both the blog and its new posts. Cascades are opt-in; without this annotation each transient entity must be persisted explicitly.

try em = factory.createEntityManager():
    transaction = em.beginTransaction()
    try:
        blog = Blog(
            "https://klyn.blog.org",
            "A blog about Klyn",
            "The Klyn blog description"
        )
        blog.addPost(Post("First post", "First description"))
        blog.addPost(Post("Second post", "Second description"))

        em.persist(blog)
        em.saveChanges()
        transaction.commit()
    catch error as Exception:
        if transaction.active:
            transaction.rollback()
        throw error
Lazy Loading and Fetch Joins

A lazy relationship can be loaded while its owning manager remains open. Accessing an unloaded relationship after that manager has closed raises UnloadedRelationshipException. Use join fetch when the relationship must leave the persistence context already loaded.

try em = factory.createEntityManager():
    blog = (query(em):
        from Blog b
            left join fetch b.posts
        where b.identifier == requestedId
    ).single()

# The collection was fetched explicitly and remains available here.
for post in blog.posts:
    print(post.title)

A fetch join is a loading directive, not a second query root. It does not introduce another alias and does not change the query's result type. Nested paths can be fetched when every path segment is a mapped relationship.

One-to-One

A one-to-one owner carries the join column. orphanRemoval=true schedules deletion of the former owned target when the association is severed and changes are saved.

@OneToOne(
    cascade=[CascadeType.ALL],
    orphanRemoval=true
)
@JoinColumn(
    name="idInformations",
    referencedColumnName="idInformations",
    nullable=true
)
public property informations as UserInformations = null

The inverse endpoint, when required, declares only @OneToOne(mappedBy="informations"). It must not duplicate the join column.

Many-to-One

A many-to-one property normally owns a non-unique foreign key. Align the logical optionality and SQL nullability; contradictory explicit settings are rejected during mapping validation.

@ManyToOne(optional=false)
@JoinColumn(
    name="idUser",
    referencedColumnName="idUser",
    nullable=false
)
public property user as User
Many-to-Many

The owning endpoint declares the join table. The inverse endpoint names the owning property with mappedBy and must not redeclare physical columns.

@ManyToMany(cascade=[CascadeType.PERSIST, CascadeType.MERGE])
@JoinTable(
    name="T_UserRoles",
    joinColumn="idUser",
    inverseJoinColumn="idRole"
)
public property roles as List<Role> = []

# In Role:
@ManyToMany(mappedBy="roles")
public property users as List<User> = []

@JoinColumns and the plural column arguments of @JoinTable support composite identities. Column order must match the mapped key order.

Cascades and Orphans
CascadePropagated lifecycle operation
PERSISTRegister a transient related entity for insertion.
MERGEMerge detached related state.
REMOVESchedule related entities for deletion.
REFRESHReload related managed state.
DETACHDetach related managed entities.
ALLEnable every lifecycle cascade above.

No cascade is implicit. orphanRemoval is separate from remove cascading: it applies when an owner-controlled to-one value is replaced or an item leaves an owner-controlled collection.

Entity Inheritance

@Inheritance supports SINGLE_TABLE, JOINED, and TABLE_PER_CLASS. A discriminator identifies the concrete type when the selected strategy needs one.

@Entity
@Table(name="T_Payments")
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(name="paymentKind", length=32)
public abstract class Payment:

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    public property id as Int = 0

    public property amount as Double


@Entity
@Table(name="T_PayPalPayments")
@DiscriminatorValue(value="PAYPAL")
public class PayPalPayment extends Payment:
    public property accountNumber as String

Include the root and every concrete entity used by the application in the factory model. Queries over the root materialize the correct concrete subtype and preserve identity-map reuse.

Next Step

Continue with Entity Engine KQL for filters, projections, ordering, pagination, fetch joins, and terminal operations.