Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import TabItem from '@theme/TabItem';

# Storm vs Other Frameworks

There is no universally "best" database framework. Each has strengths suited to different situations, team preferences, and project requirements. Teams approach data access differently, including using frameworks at various abstraction levels or even plain SQL. This page provides an honest comparison to help you evaluate whether Storm fits your needs, particularly if you value explicit and predictable behavior and fast development. We encourage you to explore the linked documentation for each framework and form your own conclusions.
There is no universally "best" database framework. Each has strengths suited to different situations, team preferences, and project requirements. Teams approach data access differently, including using frameworks at various abstraction levels or even plain SQL. This page provides a comparison to help you evaluate whether Storm fits your needs, particularly if you value explicit and predictable behavior and fast development. We encourage you to explore the linked documentation for each framework and form your own conclusions.

## Feature Comparison

Expand Down
2 changes: 1 addition & 1 deletion docs/entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ In this example, the `owner` and `city` foreign keys define the actual persisted

## Foreign Keys

The `@FK` annotation marks a field as a foreign key reference to another entity. Storm uses these annotations to automatically generate JOINs when querying and to derive column names (by default, appending `_id` to the field name).
The `@FK` annotation marks a field as a foreign key reference to another table-backed type (entity, projection, or data class with a `@PK`). Storm uses these annotations to automatically generate JOINs when querying and to derive column names (by default, appending `_id` to the field name).

<Tabs groupId="language">
<TabItem value="kotlin" label="Kotlin" default>
Expand Down
6 changes: 1 addition & 5 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Yes. Storm is used in production environments and follows semantic versioning fo

### Does Storm support schema validation?

Yes. Storm can validate your entity definitions against the actual database schema, catching mismatches like missing tables, missing columns, type incompatibilities, nullability differences, primary key mismatches, and missing sequences. This works similarly to Hibernate's `ddl-auto=validate`, but Storm never modifies the schema.
Yes. Storm can validate your entity definitions against the actual database schema, catching mismatches like missing tables, missing columns, type incompatibilities, type narrowing (potential precision loss), nullability differences, primary key mismatches, missing sequences, missing unique constraints, and missing foreign key constraints. This works similarly to Hibernate's `ddl-auto=validate`, but Storm never modifies the schema.

Enable it in Spring Boot:

Expand Down Expand Up @@ -73,10 +73,6 @@ Storm does not use bytecode manipulation or runtime proxies to intercept field a

Storm maintains only a transaction-scoped entity cache for identity guarantees and dirty checking. There is no cross-transaction or application-wide cache. This avoids cache invalidation complexity, stale data bugs, and the configuration burden of managing cache regions. For caching reference data or frequently-read entities, use Spring's `@Cacheable` annotation or a dedicated caching layer (Redis, Caffeine) at the service level, where cache scope and invalidation strategy are explicit.

### No Distributed Transactions

Storm works with single-datasource JDBC transactions. You can manage transactions using Storm's own `transaction { }` block or Spring's transaction infrastructure. For distributed transactions, configure Spring's `JtaTransactionManager` with a JTA provider (Atomikos, Narayana) and an XA-capable DataSource. Storm will participate in the distributed transaction automatically through Spring's transaction support.

### No Bytecode Manipulation

Storm does not enhance, instrument, or proxy your entity classes at build time or runtime. Entities are plain Kotlin data classes or Java records with no hidden behavior. The metamodel is generated at compile time by a KSP plugin (Kotlin) or annotation processor (Java), but this is standard code generation, not bytecode rewriting.
Expand Down
2 changes: 1 addition & 1 deletion docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The central entry point for all Storm database operations (`ORMTemplate`). Creat
A read-only data class or record that implements the `Projection<ID>` interface. Projections represent database views or complex query results defined via `@ProjectionQuery`. Unlike entities, projections only support read operations. See [Projections](projections.md).

**Ref**
A lightweight identifier (`Ref<T>`) that carries only the entity type and primary key, deferring the loading of the full entity until `fetch()` is called. Using `Ref<City>` instead of `City` in a foreign key field avoids the automatic JOIN, reducing query width when the related entity is not always needed. See [Refs](refs.md).
A lightweight identifier (`Ref<T>`) that carries only the record type and primary key, deferring the loading of the full record until `fetch()` is called. Using `Ref<City>` instead of `City` in a foreign key field avoids the automatic JOIN, reducing query width when the related data is not always needed. See [Refs](refs.md).

**Repository**
An interface that provides database access methods for an entity or projection type. `EntityRepository<E, ID>` offers built-in CRUD operations; `ProjectionRepository<P, ID>` offers read-only operations. Custom repositories extend these interfaces with domain-specific query methods. See [Repositories](repositories.md).
Expand Down
10 changes: 5 additions & 5 deletions docs/hydration.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ Hydration reconstructs from the **innermost** level outward:

## Foreign Keys (@FK)

The `@FK` annotation marks a field as a foreign key relationship. When the result set includes a joined entity, Storm hydrates all its columns into the nested record. See [SQL Templates](sql-templates.md) for how `@FK` affects query generation.
The `@FK` annotation marks a field as a foreign key relationship. When the result set includes a joined table, Storm hydrates all its columns into the nested record. See [SQL Templates](sql-templates.md) for how `@FK` affects query generation.

### FK Column Layout

Expand Down Expand Up @@ -312,7 +312,7 @@ When `city` is nullable and all city columns are NULL in a row, the hydrated `ci

Eagerly loading every related entity is not always desirable. When a `User` references a `City`, which references a `Country`, a simple user query can cascade into loading the entire object graph. In many cases, the calling code only needs the foreign key value, not the full related entity.

A `Ref<T>` is a lightweight reference that stores only the foreign key value, not the full entity. This gives you control over how much data is loaded during hydration. Use `Ref<T>` when:
A `Ref<T>` is a lightweight reference that stores only the foreign key value, not the full record. This gives you control over how much data is loaded during hydration. Use `Ref<T>` when:
- You need to break circular dependencies (self-referential entities like a tree structure)
- You want to defer entity loading until the related data is actually needed
- You are processing large result sets and want to minimize memory consumption
Expand Down Expand Up @@ -598,15 +598,15 @@ If all columns for `address` are NULL, the field is set to `null`. If some colum
|---------|-----------------|
| **Simple field** | 1 column per field |
| **Nested record** | Flattened: all nested fields become consecutive columns |
| **`@FK` entity** | All entity columns hydrated |
| **`@FK Ref<T>`** | Only FK column hydrated (entity PK) |
| **`@FK` record** | All record columns hydrated |
| **`@FK Ref<T>`** | Only FK column hydrated (record PK) |
| **Composite PK** | Multiple columns for PK fields |
| **Converter** | 1 column mapped to custom type |

**Key principles:**
- Columns map by **position**, not name
- Nested records are **flattened** into consecutive columns
- `@FK` hydrates all columns from the related entity
- `@FK` hydrates all columns from the related record
- `Ref<T>` hydrates only the foreign key value
- The interner ensures identity within a query result

Expand Down
3 changes: 1 addition & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ If you are new to Storm, follow these guides in order to build a solid foundatio
If you are coming from JPA or Hibernate, these pages explain the key differences and how to transition:

1. [Migration from JPA](migration-from-jpa.md) -- annotation mapping, concept translation, coexistence strategy
2. [Storm vs Other Frameworks](comparison.md) -- honest feature comparison with JPA, jOOQ, MyBatis, and others
2. [Storm vs Other Frameworks](comparison.md) -- feature comparison with JPA, jOOQ, MyBatis, and others
3. [Entities](entities.md) -- how Storm entities differ from JPA entities
4. [Repositories](repositories.md) -- Storm repositories vs. Spring Data repositories
5. [Transactions](transactions.md) -- transaction management without an EntityManager
Expand All @@ -222,7 +222,6 @@ If you are a tech lead or architect evaluating Storm for a production system, th
Storm is focused on being a great ORM and SQL template engine. It intentionally does not include:

- **Schema migration or DDL generation.** Storm does not create, alter, or drop tables. Use [Flyway](https://flywaydb.org/) or [Liquibase](https://www.liquibase.com/) for schema versioning and migrations.
- **Distributed transactions.** Storm works with single-datasource JDBC transactions. For distributed transactions, you can use Spring's transaction support. See the [FAQ](faq.md) for details.
- **Second-level cache.** Storm's entity cache is transaction-scoped and cleared on commit. For cross-transaction caching, use Spring's `@Cacheable` or a dedicated cache layer like Caffeine or Redis.
- **Lazy loading proxies.** Entities are plain records with no proxies. Related entities are loaded eagerly in a single query via JOINs. For deferred loading, use [Refs](refs.md) to explicitly control when related data is fetched.

Expand Down
4 changes: 2 additions & 2 deletions docs/refs.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

Refs are lightweight identifiers for entities that defer fetching until explicitly required. They optimize performance by avoiding unnecessary data retrieval and are useful for managing large object graphs.
Refs are lightweight identifiers for entities, projections, and other data types that defer fetching until explicitly required. They optimize performance by avoiding unnecessary data retrieval and are useful for managing large object graphs.

---

## Using Refs in Entities

To declare a relationship as a Ref, replace the entity type with `Ref<T>` in the field declaration. Storm stores only the foreign key column value and does not generate a JOIN for the referenced table. This reduces the width of SELECT queries and avoids loading data you may never access.
To declare a relationship as a Ref, replace the direct type with `Ref<T>` in the field declaration. Storm stores only the foreign key column value and does not generate a JOIN for the referenced table. This reduces the width of SELECT queries and avoids loading data you may never access.

<Tabs groupId="language">
<TabItem value="kotlin" label="Kotlin" default>
Expand Down
4 changes: 2 additions & 2 deletions docs/repositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ For queries that need joins, projections, or more complex filtering, use the que

## Refs

Refs are lightweight identifiers that carry only the entity type and primary key. Selecting refs instead of full entities reduces memory usage and network bandwidth when you only need IDs for subsequent operations, such as batch lookups or filtering. See [Refs](refs.md) for a detailed discussion.
Refs are lightweight identifiers that carry only the record type and primary key. Selecting refs instead of full entities reduces memory usage and network bandwidth when you only need IDs for subsequent operations, such as batch lookups or filtering. See [Refs](refs.md) for a detailed discussion.

<Tabs groupId="language">
<TabItem value="kotlin" label="Kotlin" default>
Expand All @@ -322,7 +322,7 @@ val users: Flow<User> = userRepository.selectByRef(refs)
</TabItem>
<TabItem value="java" label="Java">

Ref operations in Java return `Stream` objects that must be closed. Refs carry only the primary key and entity type, making them suitable for batch operations where loading full entities would be wasteful.
Ref operations in Java return `Stream` objects that must be closed. Refs carry only the primary key and record type, making them suitable for batch operations where loading full records would be wasteful.

```java
// Select refs (lightweight identifiers)
Expand Down
2 changes: 1 addition & 1 deletion docs/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ When Storm first encounters an entity or projection type, it inspects the record

**Foreign key rules:**

- Fields annotated with `@FK` must be either an entity type or a `Ref` type. Scalars like `String` or `Integer` cannot be foreign keys.
- Fields annotated with `@FK` must be a `Data` type (entity, projection, or data class with a `@PK`) or a `Ref` wrapping such a type. Scalars like `String` or `Integer` cannot be foreign keys.
- Auto-generated foreign keys (`@FK(generation = ...)`) cannot be inlined.

**Inline component rules:**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package st.orm.core.template.impl;

import static st.orm.core.spi.Providers.getSqlDialect;
import static st.orm.core.template.impl.RecordReflection.isPolymorphicData;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
Expand Down Expand Up @@ -545,6 +546,13 @@ private void validateForeignKeys(
if (!Data.class.isAssignableFrom(targetType)) {
continue;
}
// Polymorphic FK: the target is a sealed Data interface whose subtypes are independent
// entities in separate tables. Standard DB foreign key constraints cannot express this
// (the FK id column can reference any of the target tables, determined by the
// discriminator column at runtime), so skip FK constraint validation for these fields.
if (isPolymorphicData(targetType)) {
continue;
}
// Build a model for the target entity to get its table name.
@SuppressWarnings("unchecked")
Class<? extends Data> targetDataType = (Class<? extends Data>) targetType;
Expand Down
109 changes: 109 additions & 0 deletions storm-core/src/test/java/st/orm/core/DeleteBuilderIntegrationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package st.orm.core;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static st.orm.Operator.EQUALS;

import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import st.orm.PersistenceException;
import st.orm.core.model.City;
import st.orm.core.model.City_;
import st.orm.core.template.ORMTemplate;
import st.orm.core.template.TemplateString;

/**
* Integration tests for DeleteBuilder covering distinct, offset, limit, forShare, forUpdate,
* forLock, subquery, and getResultStream restrictions.
*/
@SuppressWarnings("ALL")
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = IntegrationConfig.class)
@DataJpaTest(showSql = false)
public class DeleteBuilderIntegrationTest {

@Autowired
private DataSource dataSource;

@Test
public void testDeleteDistinctThrows() {
var orm = ORMTemplate.of(dataSource);
assertThrows(PersistenceException.class,
() -> orm.deleteFrom(City.class).distinct());
}

@Test
public void testDeleteOffsetThrows() {
var orm = ORMTemplate.of(dataSource);
assertThrows(PersistenceException.class,
() -> orm.deleteFrom(City.class).offset(1));
}

@Test
public void testDeleteLimitThrows() {
var orm = ORMTemplate.of(dataSource);
assertThrows(PersistenceException.class,
() -> orm.deleteFrom(City.class).limit(1));
}

@Test
public void testDeleteForShareThrows() {
var orm = ORMTemplate.of(dataSource);
assertThrows(PersistenceException.class,
() -> orm.deleteFrom(City.class).forShare());
}

@Test
public void testDeleteForUpdateThrows() {
var orm = ORMTemplate.of(dataSource);
assertThrows(PersistenceException.class,
() -> orm.deleteFrom(City.class).forUpdate());
}

@Test
public void testDeleteForLockThrows() {
var orm = ORMTemplate.of(dataSource);
assertThrows(PersistenceException.class,
() -> orm.deleteFrom(City.class).forLock(TemplateString.of("FOR LOCK")));
}

@Test
public void testDeleteGetResultStreamThrows() {
var orm = ORMTemplate.of(dataSource);
assertThrows(PersistenceException.class,
() -> orm.deleteFrom(City.class).unsafe().getResultStream());
}

@Test
public void testDeleteWithWhereClause() {
var orm = ORMTemplate.of(dataSource);
var cities = orm.entity(City.class);
// Insert a city we can delete.
var insertedId = cities.insertAndFetchId(City.builder().name("DeleteMe").build());
long countBefore = cities.count();

orm.deleteFrom(City.class)
.where(City_.id, EQUALS, insertedId)
.executeUpdate();

assertEquals(countBefore - 1, cities.count());
}

@Test
public void testDeleteWithUnsafe() {
var orm = ORMTemplate.of(dataSource);
var cities = orm.entity(City.class);
// Insert a standalone city.
cities.insertAndFetchId(City.builder().name("UnsafeDelete").build());

// Unsafe delete without WHERE should attempt to delete all.
// This will fail due to FK constraints on existing cities, which is expected.
assertThrows(PersistenceException.class,
() -> orm.deleteFrom(City.class).unsafe().executeUpdate());
}
}
Loading