MyBatis Dynamic SQL

repository·master·Indexed 22 days ago

https://github.com/mybatis/mybatis-dynamic-sql

A type-safe SQL DSL for generating complex, dynamic SQL statements in MyBatis3 and Spring JDBC environments. It provides a pseudo-functional API to handle conditional logic and optional search parameters without manual string concatenation. The library supports Java 8 (Version 1.x) and Java 17 (Version 2.x), with optional integrations for Kotlin and the Spring Framework.

Tokens
64.1K
Snippets
147
Records
182
Agent score
77%

What's inside MyBatis Dynamic SQL

  1. Overview of MyBatis Dynamic SQL

    master

    MyBatis Dynamic SQL is a typesafe SQL templating framework designed to generate full SQL statements (DELETE, INSERT, SELECT, UPDATE) and their corresponding parameter sets. It is specifically designed to work with:

    • MyBatis3: Generate statements that can be passed directly as parameters to mapper methods.
    • Spring JDBC Templates: Generate statements and parameter objects compatible with Spring's JDBC abstraction.
    • Plain JDBC: Generate statements for standard JDBC usage.

    The library uses an SQL-like DSL to create objects containing both the SQL string and the required parameters, ensuring that parameter types match database column types as closely as possible.

  2. Choose an interface for extending SELECT capabilities

    master

    When extending the library's SELECT support, choose between two primary interfaces based on where you want your extension to be usable:

    • org.mybatis.dynamic.sql.BasicColumn: Use this to add capabilities to a SELECT list, a GROUP BY expression, or an ORDER BY expression (e.g., database functions or calculated columns).
    • org.mybatis.dynamic.sql.BindableColumn: Use this if you want your extension to be usable in a WHERE clause in addition to the SELECT list/GROUP BY/ORDER BY capabilities of BasicColumn (e.g., custom conditions).
  3. Write custom database functions

    master

    The library provides base classes in the org.mybatis.dynamic.sql.select.function package to simplify writing custom database functions. All supplied functions are implemented as BindableColumn, meaning they can be used in both SELECT lists and WHERE clauses.

    Choose the appropriate base class:

    • AbstractTypeConvertingFunction: Use when the function changes the column data type (e.g., converting byte[] to a Base64 String).
    • AbstractUniTypeFunction: Use when the function does not change the data type (e.g., UPPER(), LOWER()).
    • OperatorFunction: Use when implementing an operator (e.g., column1 + column2).
  4. Handle Invalid SQL Detection

    master

    The library attempts to prevent the generation of invalid SQL. If invalid SQL is detected, it throws org.mybatis.dynamic.sql.exception.InvalidSQLException or its derivatives.

    Common causes of invalid SQL include:

    1. DSL Misuse: Building statements that are syntactically incorrect (e.g., an update statement without any set clauses).
    2. Kotlin DSL Misuse: Using the flexibility of the Kotlin DSL to create invalid structures (e.g., an insert statement without an into clause).
    3. Failed Optional Mappings: In insert or update statements, if all optional column mappings fail to render (for example, if all provided values are null and the mappings are conditional), the resulting SQL will be invalid.

    To avoid these exceptions, ensure proper use of the DSL and validate input values before building statements.

  5. Prevent accidental mass updates with NonRenderingWhereClauseException

    master

    To prevent dangerous SQL generation (like a DELETE or UPDATE statement that affects all rows because the WHERE clause was empty), the library throws org.mybatis.dynamic.sql.exception.NonRenderingWhereClauseException if a coded where clause fails to render any conditions.

    Key Behaviors:

    • The exception is only thrown if a where clause was explicitly coded but all its conditions failed to render.
    • If you do not code a where clause, the library assumes you intentionally want to affect all rows.
    • This safety mechanism was introduced in version 1.4.1.

    How to override: If you have a legitimate use case where a where clause should be allowed to drop, you can override this behavior via:

    1. Global configuration.
    2. Individual statement configuration.

    Refer to the Configuration of the Library for implementation details.

  6. How Statement Configuration Scope Works with Embedded Selects

    master

    When using embedded statements (such as an insertSelect or a subquery within a select), statement configuration must always be specified on the outermost statement.

    Configuration applied to an embedded select statement will be ignored. To ensure your settings are applied, always place the configureStatement call as the last step in the outermost DSL before calling build (Java) or ending the lambda (Kotlin).

    Mental Model: The outermost statement's configuration governs the entire execution context of that specific DSL tree.

    val insertStatement = insertSelect {
        into(person)
        select(id, firstName, lastName, birthDate, employed, occupation, addressId) {
            from(person)
            where { id isGreaterThanOrEqualToWhenPresent null }
            // The following will be IGNORED:
            configureStatement { isNonRenderingWhereClauseAllowed = false }
        }
        // This is the correct way to configure the statement:
        configureStatement { isNonRenderingWhereClauseAllowed = true }
    }
  7. Use SelectDSLCompleter for dynamic queries

    master

    The SelectDSLCompleter is a specialization of java.util.Function used within MyBatis mapper default methods. It allows the caller to provide a lambda expression to define runtime query criteria such as where clauses and order by clauses without needing to know the underlying statement provider logic.

    Common operations within a SelectDSLCompleter include:

    • .where(column, condition)
    • .or(column, condition)
    • .orderBy(column)
    • Using static helpers like SelectDSLCompleter.allRows() or SelectDSLCompleter.allRowsOrderedBy(...).
  8. Define Kotlin Dynamic SQL Support Objects (Metamodel)

    master

    To use the library, you must define a metamodel using AlisableSqlTable. This involves creating an object that holds column definitions and a nested class representing the table structure.

    Using Parameter Type Converters

    You can use parameterTypeConverter on columns to map Kotlin types to database-specific formats. For example, mapping a Kotlin Boolean to a database string like "Yes" or "No".

    Important: Type converters are used in general inserts, updates, and where clauses, but not in single-row insert statements that map fields directly to properties in a data class. For those, you must provide a property in your data class that performs the conversion.

    import org.mybatis.dynamic.sql.AlisableSqlTable
    import org.mybatis.dynamic.sql.util.kotlin.elements.column
    import java.util.Date
    
    object PersonDynamicSqlSupport {
        val person = Person()
        val id = person.id
        val firstName = person.firstName
        val lastName = person.lastName
        val birthDate = person.birthDate
        val employed = person.employed
        val occupation = person.occupation
        val addressId = person.addressId
    
        class Person : AlisableSqlTable<Person>("Person", ::Person) {
            val id = column<Int>(name = "id")
            val firstName = column<String>(name = "first_name")
            val lastName = column(
                name = "last_name",
                parameterTypeConverter = lastNameConverter
            )
            val birthDate = column<Date>(name = "birth_date")
            val employed = column(
                name = "employed",
                parameterTypeConverter = booleanToStringConverter
            )
            val occupation = column<String>(name = "occupation")
            val addressId = column<Int>(name = "address_id")
        }
    }
    
    // Example converter
    val booleanToStringConverter: (Boolean?) -> String = { it?.let { if (it) "Yes" else "No" } ?: "No" }
    
    // Data class for single-row inserts
    data class PersonRecord(
        var id: Int? = null,
        var firstName: String? = null,
        var lastName: String? = null,
        var birthDate: Date? = null,
        var employed: Boolean? = null,
        var occupation: String? = null,
        var addressId: Int? = null
    ) {
        val employedAsString: String
            get() = booleanToStringConverter(employed)
    }
  9. Implement Row Mappers for Select Statements

    master

    When executing select statements, you must provide a RowMapper to transform a ResultSet into your domain objects. A RowMapper is a function that accepts (ResultSet, Int) (where Int is the row number).

    In Kotlin, you can define a row mapper as:

    1. A declared function: fun rowMapper(rs: ResultSet, rowNum: Int): T { ... }
    2. A function variable: val rowMapper: (ResultSet, Int) -> T = { rs, _ -> ... }
    3. An inline lambda passed to .withRowMapper { ... }.
    // Example of a declared row mapper
    fun rowMapper(rs: ResultSet, rowNum: Int): PersonRecord =
       PersonRecord(
          id = rs.getInt(id.name()),
          firstName = rs.getString(firstName.name()),
          // ...
       )
  10. Create Union and Multi-Select queries

    master

    Union Queries

    Use .union() to combine multiple SELECT statements. Note that only one ORDER BY phrase is allowed for the entire union, which applies to the result set as a whole.

    Multi-Select Queries

    If you need to apply ORDER BY or paging (limit/offset) to the nested queries before they are merged, use multiSelect(...). This is a special case of union where individual select statements can have their own ordering and paging.

    SelectStatementProvider selectStatement = multiSelect(
            select(id, animalName, bodyWeight, brainWeight)
                    .from(animalData)
                    .orderBy(id)
                    .limit(2)
            ).union(
                    selectDistinct(id, animalName, bodyWeight, brainWeight)
                    .from(animalData)
                    .orderBy(id.descending())
                    .limit(3)
            )
            .build()
            .render(RenderingStrategies.MYBATIS3);
    SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
            .from(animalData)
            .union()
            .selectDistinct(id, animalName, bodyWeight, brainWeight)
            .from(animalData)
            .orderBy(id)
            .build()
            .render(RenderingStrategies.MYBATIS3);
  11. Handle Bind Variables and Casting in Case Expressions

    master

    When constructing case expressions, understand how the library handles values:

    • when clauses: Always rendered using bind variables.
    • then and else_ clauses: Rendered as constants by default. To force them to use bind variables, use the value() function.
    • cast() function: Use this to explicitly define datatypes. This is critical when:
      • Using value() for all result branches (to prevent database type inference errors).
      • Using string constants (to avoid CHAR types with trailing spaces and use VARCHAR instead).
      • Using float constants (to prevent them from being interpreted as BigDecimal).
  12. Understand Initial Condition Types in Where Clauses

    master

    Every context (where, and, or, not, group) supports exactly one initial condition and any number of subsequent conditions (created via and or or). If no initial condition is provided, the first connector (and/or) is automatically stripped during rendering to prevent invalid SQL like WHERE AND id = 3.

    There are four types of initial conditions:

    1. Column and Criterion: A standard condition (e.g., id isEqualTo 3 or using the invoke operator).
    2. Not: Negates a group or single criterion (e.g., not { id isEqualTo 3 }).
    3. Exists: Executes an EXISTS sub-query.
      exists { select(foo.allColumns()).from(foo).where { foo.id isEqualTo bar.fooId } }
      To perform a NOT EXISTS, wrap the exists block in a not block.
    4. Group: Groups conditions with parentheses using the group function.
      group { id isEqualTo 3; and { id isEqualTo 4 } }