import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
When debugging performance issues or tracing application behavior, you often need visibility into the SQL statements your ORM generates. Standard JDBC logging shows raw statements with ? placeholders, giving you no context about which repository method triggered the query or what the actual parameter values were.
Storm provides the @SqlLog annotation for declarative SQL logging on repositories. Place it on a repository interface or an individual method, and Storm will log every SQL statement that method generates, including which method triggered it. No boilerplate, no manual interceptor setup, no dependency on a specific logging framework.
@SqlLog uses the JDK Platform Logging API (System.Logger), which automatically bridges to whatever logging backend is on your classpath (SLF4J, Log4j2, java.util.logging). This means it works out of the box in any environment.
The simplest way to enable SQL logging is to annotate the repository interface itself. This logs every SQL statement generated by any method in the repository.
@SqlLog
interface UserRepository : EntityRepository<User, Int> {
fun findByEmail(email: String): User? =
find(User_.email eq email)
fun findActiveUsers(): List<User> =
findAll(User_.active eq true)
}@SqlLog
public interface UserRepository extends EntityRepository<User, Integer> {
default Optional<User> findByEmail(String email) {
return select(User_.email.eq(email)).getOptionalResult();
}
default List<User> findActiveUsers() {
return select(User_.active.eq(true)).getResultList();
}
}When you call userRepository.findByEmail("alice@example.com"), the log output looks like this:
INFO com.example.UserRepository - [SQL] (UserRepository.findByEmail(String))
SELECT u.id, u.email, u.name, u.active
FROM user u
WHERE u.email = ?
The log message includes the repository type and method name, making it easy to trace which code path triggered the query.
When you only need logging for specific operations (for example, a complex query you are developing or debugging), annotate the method instead of the entire interface. This avoids noisy output from methods you are not interested in.
interface OrderRepository : EntityRepository<Order, Int> {
// No logging
fun findById(id: Int): Order? =
find(Order_.id eq id)
// Logged
@SqlLog
fun findExpiredOrders(cutoff: LocalDate): List<Order> =
findAll(Order_.expiresAt lt cutoff)
}public interface OrderRepository extends EntityRepository<Order, Integer> {
// No logging
default Optional<Order> findById(int id) {
return select(Order_.id.eq(id)).getOptionalResult();
}
// Logged
@SqlLog
default List<Order> findExpiredOrders(LocalDate cutoff) {
return select(Order_.expiresAt.lt(cutoff)).getResultList();
}
}Method-level annotations override type-level annotations. If the interface has @SqlLog but a specific method has @SqlLog(level = Level.DEBUG), the method's configuration takes precedence.
By default, SQL log output shows parameterized queries with ? placeholders, just as they are sent to the database. This is useful for understanding query structure, but when you are debugging a specific issue, you often want to see the actual values.
Setting inlineParameters = true replaces the ? placeholders with the actual bound values. This produces SQL you can copy directly into a database tool and execute, which makes it invaluable for reproducing issues.
@SqlLog(inlineParameters = true)
interface UserRepository : EntityRepository<User, Int> {
fun findByEmail(email: String): User? =
find(User_.email eq email)
}@SqlLog(inlineParameters = true)
public interface UserRepository extends EntityRepository<User, Integer> {
default Optional<User> findByEmail(String email) {
return select(User_.email.eq(email)).getOptionalResult();
}
}Compare the output:
| Setting | Output |
|---|---|
inlineParameters = false (default) |
SELECT u.id, u.email FROM user u WHERE u.email = ? |
inlineParameters = true |
SELECT u.id, u.email FROM user u WHERE u.email = 'alice@example.com' |
With inlined parameters, the logged SQL is a complete, executable statement. You can paste it directly into your database client to inspect the result set, check the query plan with EXPLAIN, or verify that the WHERE clause matches the rows you expect. This is especially helpful when debugging queries with multiple parameters, date ranges, or complex filter expressions where it is not obvious which ? corresponds to which value.
Important:
inlineParametersonly affects the log output. The actual SQL sent to the database always uses parameterized queries with?placeholders, regardless of this setting. Storm never sends inlined parameter values to the database, so there is no risk of SQL injection or behavioral changes. This is purely a logging convenience.
Tip: Use
inlineParameters = trueduring development and debugging. For production logging, prefer the default (false) to keep log output concise and avoid accidentally logging sensitive data such as passwords or personal information.
The level attribute controls the System.Logger.Level used for log output. If the configured logger is not enabled for the specified level, the interceptor is skipped entirely, so there is zero overhead when logging is disabled.
@SqlLog(level = System.Logger.Level.DEBUG)
interface UserRepository : EntityRepository<User, Int> { ... }@SqlLog(level = System.Logger.Level.DEBUG)
public interface UserRepository extends EntityRepository<User, Integer> { ... }The available levels follow the standard System.Logger.Level enum:
| Level | Typical use |
|---|---|
TRACE |
Very detailed diagnostics, high volume |
DEBUG |
Development-time query inspection |
INFO |
Default; visible in standard log output |
WARNING |
Highlight potentially problematic queries |
By default, the logger name is the fully qualified name of the repository interface (e.g., com.example.UserRepository). This works well with standard logging configuration where you can enable or disable logging per package.
If you need a different logger name, for example, to group all SQL logs under a single category, use the name attribute:
@SqlLog(name = "sql")
interface UserRepository : EntityRepository<User, Int> { ... }@SqlLog(name = "sql")
public interface UserRepository extends EntityRepository<User, Integer> { ... }This logs to a logger named sql instead of the repository's class name, so you can configure a single logger to capture (or silence) SQL output from all repositories at once.
| Attribute | Type | Default | Description |
|---|---|---|---|
inlineParameters |
boolean |
false |
Replace ? placeholders with actual parameter values in log output |
level |
System.Logger.Level |
INFO |
Log level for SQL output; logging is skipped entirely if the level is not enabled |
name |
String |
"" (repository class name) |
Custom logger name; useful for grouping all SQL logging under one category |
@SqlLog is processed by the repository proxy, so it works everywhere repositories are used:
- Repositories obtained via
orm.repository()(standalone usage, no framework required) - Spring-managed repository beans (auto-configured through the Spring Boot starter)
No additional configuration or dependencies are needed beyond the Storm dependency you already have.
- Start with type-level annotation during development to see all queries a repository generates, then narrow down to method-level once you know which queries to focus on.
- Use
inlineParameters = truefor debugging to get copy-pasteable SQL. Switch back tofalsebefore committing to avoid leaking sensitive values in production logs. - Set level to
DEBUGorTRACEfor repositories in production code, so SQL logging is available on demand through log level configuration without code changes. - Combine with a custom logger name like
@SqlLog(name = "sql")to create a single switch for all SQL logging across your application.