diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ddca0342d..4d9afec4b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.51.1 +Bug fixes: +* fix: EXPOSED-389 Coalesce operator returning nullable value by @joc-a in https://github.com/JetBrains/Exposed/pull/2107 + # 0.51.0 Infrastructure: * Spring Boot 3.3.0 diff --git a/README.md b/README.md index b1d3a9cb89..83c6add490 100644 --- a/README.md +++ b/README.md @@ -81,52 +81,52 @@ The Maven Central repository is enabled by default for Maven users. org.jetbrains.exposed exposed-core - 0.51.0 + 0.51.1 org.jetbrains.exposed exposed-crypt - 0.51.0 + 0.51.1 org.jetbrains.exposed exposed-dao - 0.51.0 + 0.51.1 org.jetbrains.exposed exposed-java-time - 0.51.0 + 0.51.1 org.jetbrains.exposed exposed-jdbc - 0.51.0 + 0.51.1 org.jetbrains.exposed exposed-jodatime - 0.51.0 + 0.51.1 org.jetbrains.exposed exposed-json - 0.51.0 + 0.51.1 org.jetbrains.exposed exposed-kotlin-datetime - 0.51.0 + 0.51.1 org.jetbrains.exposed exposed-money - 0.51.0 + 0.51.1 org.jetbrains.exposed exposed-spring-boot-starter - 0.51.0 + 0.51.1 @@ -136,20 +136,20 @@ The Maven Central repository is enabled by default for Maven users. ```groovy dependencies { - implementation 'org.jetbrains.exposed:exposed-core:0.51.0' - implementation 'org.jetbrains.exposed:exposed-crypt:0.51.0' - implementation 'org.jetbrains.exposed:exposed-dao:0.51.0' - implementation 'org.jetbrains.exposed:exposed-jdbc:0.51.0' + implementation 'org.jetbrains.exposed:exposed-core:0.51.1' + implementation 'org.jetbrains.exposed:exposed-crypt:0.51.1' + implementation 'org.jetbrains.exposed:exposed-dao:0.51.1' + implementation 'org.jetbrains.exposed:exposed-jdbc:0.51.1' - implementation 'org.jetbrains.exposed:exposed-jodatime:0.51.0' + implementation 'org.jetbrains.exposed:exposed-jodatime:0.51.1' // or - implementation 'org.jetbrains.exposed:exposed-java-time:0.51.0' + implementation 'org.jetbrains.exposed:exposed-java-time:0.51.1' // or - implementation 'org.jetbrains.exposed:exposed-kotlin-datetime:0.51.0' + implementation 'org.jetbrains.exposed:exposed-kotlin-datetime:0.51.1' - implementation 'org.jetbrains.exposed:exposed-json:0.51.0' - implementation 'org.jetbrains.exposed:exposed-money:0.51.0' - implementation 'org.jetbrains.exposed:exposed-spring-boot-starter:0.51.0' + implementation 'org.jetbrains.exposed:exposed-json:0.51.1' + implementation 'org.jetbrains.exposed:exposed-money:0.51.1' + implementation 'org.jetbrains.exposed:exposed-spring-boot-starter:0.51.1' } ``` @@ -180,7 +180,7 @@ dependencies { and in `gradle.properties` ``` -exposedVersion=0.51.0 +exposedVersion=0.51.1 ``` ## Samples diff --git a/docs/Map.jhm b/docs/Map.jhm index fd745da4a1..d458feee0f 100644 --- a/docs/Map.jhm +++ b/docs/Map.jhm @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/breaking-changes.html b/docs/breaking-changes.html index 46066eabd6..503e9761d6 100644 --- a/docs/breaking-changes.html +++ b/docs/breaking-changes.html @@ -1,9 +1,9 @@ -Breaking Changes | Exposed

Exposed 0.51.0 Help

Breaking Changes

0.51.0

  • The exposed-spring-boot-starter module no longer provides the entire spring-boot-starter-data-jdbc module. It now provides just the spring-boot-starter-jdbc. If there was a reliance on this transitive dependency, please directly include a dependency on Spring Data JDBC in your build files.

  • ulong column type is now NUMERIC(20) instead of BIGINT for H2 (excluding H2_PSQL), SQLite, and SQL Server to allow storing the full range of ULong, including ULong.MAX_VALUE.

0.50.0

  • The Transaction class property repetitionAttempts is being deprecated in favor of maxAttempts. Additionally, the property minRepetitionDelay should be replaced with minRetryDelay, and maxRepetitionDelay with maxRetryDelay. These changes also affect the default variants of these properties in DatabaseConfig.

  • The property maxAttempts represents the maximum amount of attempts to perform a transaction block. Setting it, or the now deprecated repetitionAttempts, to a value less than 1 now throws an IllegalArgumentException.

  • IColumnType and ColumnType now expect a type argument. IColumnType.valueFromDB() also no longer has a default implementation, so an override for this method must be provided in any custom column type implementation. Check this pull request for details regarding this change.

0.49.0

For SQLite database, Exposed now requires bumping the SQLite JDBC driver version to a minimum of 3.45.0.0.

0.48.0

  • In nonNullValueToString for KotlinInstantColumnType and JavaDateColumnType, the formatted String for MySQL did not match the format received from the metadata when isFractionDateTimeSupported is true, so a new formatter specific to that is now used.

  • In nonNullValueToString for KotlinLocalDateTimeColumnType, the formatted String for MySQL did not match the format received from the metadata when isFractionDateTimeSupported is true, so a new formatter specific to MySQL is now used.

  • In nonNullValueToString for DateColumnType, JavaLocalDateTimeColumnType, JavaLocalTimeColumnType, JavaInstantColumnType, KotlinLocalDateTimeColumnType, KotlinLocalTimeColumnType, and KotlinInstantColumnType, the correct formatter for MySQL is used when the version (below 5.6) does not support fractional seconds.

  • In nonNullValueToString for DateColumnType and DateTimeWithTimeZoneColumnType, the formatters used are changed to reflect the fact that Joda-Time stores date/time values only down to the millisecond (up to SSS and not SSSSSS).

  • Functions anyFrom(array) and allFrom(array) now use ArrayColumnType to process the provided array argument when query building. ArrayColumnType requires a base column type to process contents correctly, and Exposed attempts to resolve the best match internally based on the array content type. A specific column type argument should be provided to the function parameter delegateType if the content requires either an unsupported or custom column type, or a column type not defined in the exposed-core module.

  • exposed-crypt module now uses Spring Security Crypto 6.+, which requires Java 17 as a minimum version.

0.47.0

The function SchemaUtils.checkExcessiveIndices is used to check both excessive indices and excessive foreign key constraints. It now has a different behavior and deals with excessive indices only. Also, its return type is now List<Index> instead of Unit. A new function, SchemaUtils.checkExcessiveForeignKeyConstraints, deals with excessive foreign key constraints and has a return type List<ForeignKeyConstraint>.

0.46.0

  • When an Exposed table object is created with a keyword identifier (a table or column name) it now retains the exact case used before being automatically quoted in generated SQL. This primarily affects H2 and Oracle, both of which support folding identifiers to uppercase, and PostgresSQL, which folds identifiers to a lower case.

    If preserveKeywordCasing = true had been previously set in DatabaseConfig to remove logged warnings about any keyword identifiers, this can now be removed as the property is true by default.

    To temporarily opt out of this behavior and to not keep the defined casing of keyword identifiers, please set preserveKeywordCasing = false in DatabaseConfig:

+}

Exposed 0.51.1 Help

Breaking Changes

0.51.0

  • The exposed-spring-boot-starter module no longer provides the entire spring-boot-starter-data-jdbc module. It now provides just the spring-boot-starter-jdbc. If there was a reliance on this transitive dependency, please directly include a dependency on Spring Data JDBC in your build files.

  • ulong column type is now NUMERIC(20) instead of BIGINT for H2 (excluding H2_PSQL), SQLite, and SQL Server to allow storing the full range of ULong, including ULong.MAX_VALUE.

0.50.0

  • The Transaction class property repetitionAttempts is being deprecated in favor of maxAttempts. Additionally, the property minRepetitionDelay should be replaced with minRetryDelay, and maxRepetitionDelay with maxRetryDelay. These changes also affect the default variants of these properties in DatabaseConfig.

  • The property maxAttempts represents the maximum amount of attempts to perform a transaction block. Setting it, or the now deprecated repetitionAttempts, to a value less than 1 now throws an IllegalArgumentException.

  • IColumnType and ColumnType now expect a type argument. IColumnType.valueFromDB() also no longer has a default implementation, so an override for this method must be provided in any custom column type implementation. Check this pull request for details regarding this change.

0.49.0

For SQLite database, Exposed now requires bumping the SQLite JDBC driver version to a minimum of 3.45.0.0.

0.48.0

  • In nonNullValueToString for KotlinInstantColumnType and JavaDateColumnType, the formatted String for MySQL did not match the format received from the metadata when isFractionDateTimeSupported is true, so a new formatter specific to that is now used.

  • In nonNullValueToString for KotlinLocalDateTimeColumnType, the formatted String for MySQL did not match the format received from the metadata when isFractionDateTimeSupported is true, so a new formatter specific to MySQL is now used.

  • In nonNullValueToString for DateColumnType, JavaLocalDateTimeColumnType, JavaLocalTimeColumnType, JavaInstantColumnType, KotlinLocalDateTimeColumnType, KotlinLocalTimeColumnType, and KotlinInstantColumnType, the correct formatter for MySQL is used when the version (below 5.6) does not support fractional seconds.

  • In nonNullValueToString for DateColumnType and DateTimeWithTimeZoneColumnType, the formatters used are changed to reflect the fact that Joda-Time stores date/time values only down to the millisecond (up to SSS and not SSSSSS).

  • Functions anyFrom(array) and allFrom(array) now use ArrayColumnType to process the provided array argument when query building. ArrayColumnType requires a base column type to process contents correctly, and Exposed attempts to resolve the best match internally based on the array content type. A specific column type argument should be provided to the function parameter delegateType if the content requires either an unsupported or custom column type, or a column type not defined in the exposed-core module.

  • exposed-crypt module now uses Spring Security Crypto 6.+, which requires Java 17 as a minimum version.

0.47.0

The function SchemaUtils.checkExcessiveIndices is used to check both excessive indices and excessive foreign key constraints. It now has a different behavior and deals with excessive indices only. Also, its return type is now List<Index> instead of Unit. A new function, SchemaUtils.checkExcessiveForeignKeyConstraints, deals with excessive foreign key constraints and has a return type List<ForeignKeyConstraint>.

0.46.0

  • When an Exposed table object is created with a keyword identifier (a table or column name) it now retains the exact case used before being automatically quoted in generated SQL. This primarily affects H2 and Oracle, both of which support folding identifiers to uppercase, and PostgresSQL, which folds identifiers to a lower case.

    If preserveKeywordCasing = true had been previously set in DatabaseConfig to remove logged warnings about any keyword identifiers, this can now be removed as the property is true by default.

    To temporarily opt out of this behavior and to not keep the defined casing of keyword identifiers, please set preserveKeywordCasing = false in DatabaseConfig:

object TestTable : Table("table") { val col = integer("select") } @@ -31,7 +31,7 @@ } ) // H2 generates SQL -> CREATE TABLE IF NOT EXISTS "TABLE" ("SELECT" INT NOT NULL) -

0.44.0

  • SpringTransactionManager no longer extends DataSourceTransactionManager; instead, it directly extends AbstractPlatformTransactionManager while retaining the previous basic functionality. The class also no longer implements the Exposed interface TransactionManager, as transaction operations are instead delegated to Spring. These changes ensure that Exposed's underlying transaction management no longer interferes with the expected behavior of Spring's transaction management, for example, when using nested transactions or with @Transactional elements like propagation or isolation.

    If integration still requires a DataSourceTransactionManager, please add two bean declarations to the configuration: one for SpringTransactionManager and one for DataSourceTransactionManager. Then define a composite transaction manager that combines these two managers.

    If TransactionManager functions were being invoked by a SpringTransactionManager instance, please replace these calls with the appropriate Spring annotation or, if necessary, by using the companion object of TransactionManager directly (for example, TransactionManager.currentOrNull()).

  • spring-transaction and exposed-spring-boot-starter modules now use Spring Framework 6.0 and Spring Boot 3.0, which require Java 17 as a minimum version.

  • A table that is created with a keyword identifier (a table or column name) now logs a warning that the identifier's case may be lost when it is automatically quoted in generated SQL. This primarily affects H2 and Oracle, both of which support folding identifiers to uppercase, and PostgreSQL, which folds identifiers to a lower case.

    To remove these warnings and to ensure that the keyword identifier sent to the database matches the exact case used in the Exposed table object, please set preserveKeywordCasing = true in DatabaseConfig:

+

0.44.0

  • SpringTransactionManager no longer extends DataSourceTransactionManager; instead, it directly extends AbstractPlatformTransactionManager while retaining the previous basic functionality. The class also no longer implements the Exposed interface TransactionManager, as transaction operations are instead delegated to Spring. These changes ensure that Exposed's underlying transaction management no longer interferes with the expected behavior of Spring's transaction management, for example, when using nested transactions or with @Transactional elements like propagation or isolation.

    If integration still requires a DataSourceTransactionManager, please add two bean declarations to the configuration: one for SpringTransactionManager and one for DataSourceTransactionManager. Then define a composite transaction manager that combines these two managers.

    If TransactionManager functions were being invoked by a SpringTransactionManager instance, please replace these calls with the appropriate Spring annotation or, if necessary, by using the companion object of TransactionManager directly (for example, TransactionManager.currentOrNull()).

  • spring-transaction and exposed-spring-boot-starter modules now use Spring Framework 6.0 and Spring Boot 3.0, which require Java 17 as a minimum version.

  • A table that is created with a keyword identifier (a table or column name) now logs a warning that the identifier's case may be lost when it is automatically quoted in generated SQL. This primarily affects H2 and Oracle, both of which support folding identifiers to uppercase, and PostgreSQL, which folds identifiers to a lower case.

    To remove these warnings and to ensure that the keyword identifier sent to the database matches the exact case used in the Exposed table object, please set preserveKeywordCasing = true in DatabaseConfig:

object TestTable : Table("table") { val col = integer("select") } @@ -49,15 +49,15 @@ } ) // H2 generates SQL -> CREATE TABLE IF NOT EXISTS "table" ("select" INT NOT NULL) -

0.43.0

  • In all databases except MySQL, MariaDB, and SQL Server, the ubyte() column now maps to data type SMALLINT instead of TINYINT, which allows the full range of UByte values to be inserted without any overflow. Registering the column on a table also creates a check constraint that restricts inserted data to the range between 0 and UByte.MAX_VALUE. If a column that only uses 1 byte of storage is needed, but without allowing any non-negative values to be inserted, please use a signed byte() column instead with a manually created check constraint:

+

0.43.0

  • In all databases except MySQL, MariaDB, and SQL Server, the ubyte() column now maps to data type SMALLINT instead of TINYINT, which allows the full range of UByte values to be inserted without any overflow. Registering the column on a table also creates a check constraint that restricts inserted data to the range between 0 and UByte.MAX_VALUE. If a column that only uses 1 byte of storage is needed, but without allowing any non-negative values to be inserted, please use a signed byte() column instead with a manually created check constraint:

byte("number").check { it.between(0, Byte.MAX_VALUE) } // OR byte("number").check { (it greaterEq 0) and (it lessEq Byte.MAX_VALUE) } -
  • In all databases except MySQL and MariaDB, the uint() column now maps to data type BIGINT instead of INT, which allows the full range of UInt values to be inserted without any overflow. Registering the column on a table also creates a check constraint that restricts inserted data to the range between 0 and UInt.MAX_VALUE. If a column that only uses 4 bytes of storage is needed, but without allowing any non-negative values to be inserted, please use a signed integer() column instead with a manually created check constraint:

+
  • In all databases except MySQL and MariaDB, the uint() column now maps to data type BIGINT instead of INT, which allows the full range of UInt values to be inserted without any overflow. Registering the column on a table also creates a check constraint that restricts inserted data to the range between 0 and UInt.MAX_VALUE. If a column that only uses 4 bytes of storage is needed, but without allowing any non-negative values to be inserted, please use a signed integer() column instead with a manually created check constraint:

integer("number").check { it.between(0, Int.MAX_VALUE) } // OR integer("number").check { (it greaterEq 0) and (it lessEq Int.MAX_VALUE) } -

0.42.0

  • SQLite The table column created using date() now uses TEXT datatype instead of DATE (which the database mapped internally to NUMERIC type). This applies to the specific DateColumnType in all 3 date/time modules and means LocalDate comparisons can now be done directly without conversions.

  • H2, PostgreSQL Using replace() now throws an exception as the REPLACE command is not supported by these databases. If replace() was being used to perform an insert or update operation, all usages should instead be switched to upsert(). See documentation for UPSERT details

  • Operator classes exists and notExists have been renamed to Exists and NotExists. The functions exists() and notExists() have been introduced to return an instance of their respectively-named classes and to avoid unresolved reference issues. Any usages of these classes should be renamed to their capitalized forms.

  • customEnumeration() now registers a CustomEnumerationColumnType to allow referencing by another column. The signature of customEnumeration() has not changed and table columns initialized using it are still of type Column<DataClass>.

  • Transaction.suspendedTransaction() has been renamed to Transaction.withSuspendTransaction(). Please run Edit -> Find -> Replace in files... twice with suspendedTransaction( and suspendedTransaction as the search options, to ensure that both variants are replaced without affecting suspendedTransactionAsync() (if used in code).

  • The repetitionAttempts parameter in transaction() has been removed and replaced with a mutable property in the Transaction class. Please remove any arguments for this parameter and assign values to the property directly:

+

0.42.0

  • SQLite The table column created using date() now uses TEXT datatype instead of DATE (which the database mapped internally to NUMERIC type). This applies to the specific DateColumnType in all 3 date/time modules and means LocalDate comparisons can now be done directly without conversions.

  • H2, PostgreSQL Using replace() now throws an exception as the REPLACE command is not supported by these databases. If replace() was being used to perform an insert or update operation, all usages should instead be switched to upsert(). See documentation for UPSERT details

  • Operator classes exists and notExists have been renamed to Exists and NotExists. The functions exists() and notExists() have been introduced to return an instance of their respectively-named classes and to avoid unresolved reference issues. Any usages of these classes should be renamed to their capitalized forms.

  • customEnumeration() now registers a CustomEnumerationColumnType to allow referencing by another column. The signature of customEnumeration() has not changed and table columns initialized using it are still of type Column<DataClass>.

  • Transaction.suspendedTransaction() has been renamed to Transaction.withSuspendTransaction(). Please run Edit -> Find -> Replace in files... twice with suspendedTransaction( and suspendedTransaction as the search options, to ensure that both variants are replaced without affecting suspendedTransactionAsync() (if used in code).

  • The repetitionAttempts parameter in transaction() has been removed and replaced with a mutable property in the Transaction class. Please remove any arguments for this parameter and assign values to the property directly:

// before transaction(Connection.TRANSACTION_READ_COMMITTED, repetitionAttempts = 10) { // statements @@ -68,8 +68,8 @@ repetitionAttempts = 10 // statements } -
  • In all databases except MySQL and MariaDB, the ushort() column now maps to data type INT instead of SMALLINT, which allows the full range of UShort values to be inserted without any overflow. Registering the column on a table also creates a check constraint that restricts inserted data to the range between 0 and UShort.MAX_VALUE. If a column that only uses 2 bytes of storage is needed, but without allowing any non-negative values to be inserted, please use a signed short() column instead with a manually created check constraint:

+
  • In all databases except MySQL and MariaDB, the ushort() column now maps to data type INT instead of SMALLINT, which allows the full range of UShort values to be inserted without any overflow. Registering the column on a table also creates a check constraint that restricts inserted data to the range between 0 and UShort.MAX_VALUE. If a column that only uses 2 bytes of storage is needed, but without allowing any non-negative values to be inserted, please use a signed short() column instead with a manually created check constraint:

short("number").check { it.between(0, Short.MAX_VALUE) } // OR short("number").check { (it greaterEq 0) and (it lessEq Short.MAX_VALUE) } -
Last modified: 31 May 2024
\ No newline at end of file +
Last modified: 03 June 2024
diff --git a/docs/config.json b/docs/config.json index 729021d6f0..84fbbbe9d7 100644 --- a/docs/config.json +++ b/docs/config.json @@ -1 +1 @@ -{"productWebUrl":"https://jetbrains.github.io/Exposed/home.html","productVersion":"0.51.0","productId":"docs","stage":"release","downloadTitle":"Get Exposed","keymaps":{},"logoUrl":"images/exposed-logo.svg","searchMaxHits":75,"productName":"Exposed"} \ No newline at end of file +{"productWebUrl":"https://jetbrains.github.io/Exposed/home.html","productVersion":"0.51.1","productId":"docs","stage":"release","downloadTitle":"Get Exposed","keymaps":{},"logoUrl":"images/exposed-logo.svg","searchMaxHits":75,"productName":"Exposed"} \ No newline at end of file diff --git a/docs/contributing.html b/docs/contributing.html index 0702d3f333..7c4b99757b 100644 --- a/docs/contributing.html +++ b/docs/contributing.html @@ -1,9 +1,9 @@ -Contributing to Exposed | Exposed

Exposed 0.51.0 Help

Contributing to Exposed

We're delighted that you're considering contributing to Exposed!

There are multiple ways you can contribute:

  • Code

  • Documentation

  • Community Support

  • Issues and Feature Requests

This project and the corresponding community is governed by the JetBrains Open Source and Community Code of Conduct. Independently of how you'd like to contribute, please make sure you read and comply with it.

Setup

Testing on Apple Silicon

To run Oracle XE tests, you need to install Colima container runtime. It will work in pair with your docker installation.

+}

Exposed 0.51.1 Help

Contributing to Exposed

We're delighted that you're considering contributing to Exposed!

There are multiple ways you can contribute:

  • Code

  • Documentation

  • Community Support

  • Issues and Feature Requests

This project and the corresponding community is governed by the JetBrains Open Source and Community Code of Conduct. Independently of how you'd like to contribute, please make sure you read and comply with it.

Setup

Testing on Apple Silicon

To run Oracle XE tests, you need to install Colima container runtime. It will work in pair with your docker installation.

brew install colima -

After installing, you need to start the colima daemon in arch x86_64 mode:

+

After installing, you need to start the colima daemon in arch x86_64 mode:

colima start --arch x86_64 --memory 4 --network-address -

The test task can automatically use colima context when needed, and it's better to use default context for other tasks. To switch the context to default, run:

+

The test task can automatically use colima context when needed, and it's better to use default context for other tasks. To switch the context to default, run:

docker context use default -

Make sure that default is used as default docker context:

+

Make sure that default is used as default docker context:

docker context list -

Code

Pull Requests

Contributions are made using Github pull requests:

  1. Fork the Exposed repository, because imitation is the sincerest form of flattery.

  2. Clone your fork to your local machine.

  3. Create a new branch for your changes.

  4. Create a new PR with a request to merge to the master branch.

  5. Ensure that the description is clear and refers to an existing ticket/bug if applicable, prefixing the description with EXPOSED-, where refers to the YouTrack issue.

  6. When contributing a new feature, provide motivation and use-cases describing why the feature not only provides value to Exposed, but also why it would make sense to be part of the Exposed framework itself.

  7. If the contribution requires updates to documentation (be it updating existing contents or creating new one), please file a new ticket on YouTrack.

  8. Make sure any code contributed is covered by tests and no existing tests are broken. We use Docker containers to run tests.

  9. Execute the detekt task in Gradle to perform code style validation.

  10. Finally, make sure to run the apiCheck Gradle task. If it's not successful, run the apiDump Gradle task. Further information can be found here.

Style Guides

A few things to remember:

  • Your code should conform to the official Kotlin code style guide except that star imports should always be enabled. (ensure Preferences | Editor | Code Style | Kotlin, tab Imports, both Use import with '*' should be checked).

  • Every new source file should have a copyright header.

  • Every public API (including functions, classes, objects and so on) should be documented, every parameter, property, return types, and exceptions should be described properly.

Test functions:

  • Begin each test function name with the word test.

  • Employ camelCase for test function names, such as testInsertEmojisWithInvalidLength.

  • Refrain from using names enclosed in backticks for test functions, because KDocs cannot reference function names that contain spaces.

  • In the definition of test functions, use a block body instead of an assignment operator. For example, do write fun testMyTest() { withDb{} }, and avoid writing fun testMyTest() = withDb{}.

Commit messages

  • Commit messages should be written in English.

  • Their title should be prefixed according to Conventional Commits.

  • They should be written in present tense using imperative mood ("Fix" instead of "Fixes", "Improve" instead of "Improved"). See How to Write a Git Commit Message.

  • When applicable, prefix the commit message with EXPOSED- where represents the YouTrack issue number.

  • Add the related bug reference to a commit message (bug number after a hash character between round braces).

Documentation

There are multiple ways in which you can contribute to Exposed docs:

  • Create an issue in YouTrack.

  • Submit a pull request containing your proposed changes. Ensure that these modifications are applied directly within the documentation-website directory.

Community Support

If you'd like to help others, please join our Exposed channel on the Kotlin Slack workspace and help out. It's also a great way to learn!

Issues and Feature Requests

If you encounter a bug or have an idea for a new feature, please submit it to us through YouTrack, our issue tracker.

Before submitting an issue or feature request, please search YouTrack's existing issues to avoid reporting duplicates.

When submitting an issue or feature request, please provide as much detail as possible, including a clear and concise description of the problem or desired functionality, steps to reproduce the issue, and any relevant code snippets or error messages.

Thank you for your cooperation and for helping to improve Exposed.

Last modified: 31 May 2024
\ No newline at end of file +

Code

Pull Requests

Contributions are made using Github pull requests:

  1. Fork the Exposed repository, because imitation is the sincerest form of flattery.

  2. Clone your fork to your local machine.

  3. Create a new branch for your changes.

  4. Create a new PR with a request to merge to the master branch.

  5. Ensure that the description is clear and refers to an existing ticket/bug if applicable, prefixing the description with EXPOSED-, where refers to the YouTrack issue.

  6. When contributing a new feature, provide motivation and use-cases describing why the feature not only provides value to Exposed, but also why it would make sense to be part of the Exposed framework itself.

  7. If the contribution requires updates to documentation (be it updating existing contents or creating new one), please file a new ticket on YouTrack.

  8. Make sure any code contributed is covered by tests and no existing tests are broken. We use Docker containers to run tests.

  9. Execute the detekt task in Gradle to perform code style validation.

  10. Finally, make sure to run the apiCheck Gradle task. If it's not successful, run the apiDump Gradle task. Further information can be found here.

Style Guides

A few things to remember:

  • Your code should conform to the official Kotlin code style guide except that star imports should always be enabled. (ensure Preferences | Editor | Code Style | Kotlin, tab Imports, both Use import with '*' should be checked).

  • Every new source file should have a copyright header.

  • Every public API (including functions, classes, objects and so on) should be documented, every parameter, property, return types, and exceptions should be described properly.

Test functions:

  • Begin each test function name with the word test.

  • Employ camelCase for test function names, such as testInsertEmojisWithInvalidLength.

  • Refrain from using names enclosed in backticks for test functions, because KDocs cannot reference function names that contain spaces.

  • In the definition of test functions, use a block body instead of an assignment operator. For example, do write fun testMyTest() { withDb{} }, and avoid writing fun testMyTest() = withDb{}.

Commit messages

  • Commit messages should be written in English.

  • Their title should be prefixed according to Conventional Commits.

  • They should be written in present tense using imperative mood ("Fix" instead of "Fixes", "Improve" instead of "Improved"). See How to Write a Git Commit Message.

  • When applicable, prefix the commit message with EXPOSED- where represents the YouTrack issue number.

  • Add the related bug reference to a commit message (bug number after a hash character between round braces).

Documentation

There are multiple ways in which you can contribute to Exposed docs:

  • Create an issue in YouTrack.

  • Submit a pull request containing your proposed changes. Ensure that these modifications are applied directly within the documentation-website directory.

Community Support

If you'd like to help others, please join our Exposed channel on the Kotlin Slack workspace and help out. It's also a great way to learn!

Issues and Feature Requests

If you encounter a bug or have an idea for a new feature, please submit it to us through YouTrack, our issue tracker.

Before submitting an issue or feature request, please search YouTrack's existing issues to avoid reporting duplicates.

When submitting an issue or feature request, please provide as much detail as possible, including a clear and concise description of the problem or desired functionality, steps to reproduce the issue, and any relevant code snippets or error messages.

Thank you for your cooperation and for helping to improve Exposed.

Last modified: 03 June 2024
diff --git a/docs/current.help.version b/docs/current.help.version index fdbbf177d6..fb6e7a44dd 100644 --- a/docs/current.help.version +++ b/docs/current.help.version @@ -1 +1 @@ -0.51.0 \ No newline at end of file +0.51.1 \ No newline at end of file diff --git a/docs/data-types.html b/docs/data-types.html index 5ca836f012..15db34933c 100644 --- a/docs/data-types.html +++ b/docs/data-types.html @@ -1,9 +1,9 @@ -Data Types | Exposed

Exposed 0.51.0 Help

Data Types

Exposed supports the following data types in the table definition:

  • integer - translates to DB INT

  • short - translates to DB SMALLINT

  • long - BIGINT

  • float - FLOAT

  • decimal - DECIMAL with scale and precision

  • bool - BOOLEAN

  • char - CHAR

  • varchar - VARCHAR with length

  • text - TEXT

  • enumeration - INT ordinal value

  • enumerationByName - VARCHAR

  • customEnumeration - see additional section

  • blob - BLOB

  • binary - VARBINARY with length

  • uuid - BINARY(16)

  • reference - a foreign key

  • array - ARRAY

The exposed-java-time extension (org.jetbrains.exposed:exposed-java-time:$exposed_version) provides additional types:

  • date - DATETIME

  • time - TIME

  • datetime - DATETIME

  • timestamp - TIMESTAMP

  • duration - DURATION

The exposed-json extension (org.jetbrains.exposed:exposed-json:$exposed_version) provides additional types (see how to use):

  • json - JSON

  • jsonb - JSONB

How to use database ENUM types

Some of the databases (e.g. MySQL, PostgreSQL, H2) support explicit ENUM types. Because keeping such columns in sync with Kotlin enumerations using only JDBC metadata could be a huge challenge, Exposed doesn't provide a possibility to manage such columns in an automatic way, but that doesn't mean that you can't use such column types.

You have two options to work with ENUM database types and you should use customEnumeration() (available since version 0.10.3) in both cases:

  1. Use an existing ENUM column from your table. In this case, the sql parameter in customEnumeration() can be left as null.

  2. Create a new ENUM column using Exposed by providing the raw definition SQL to the sql parameter in customEnumeration().

As a JDBC driver can provide/expect specific classes for ENUM types, you must also provide from/to transformation functions for them when defining a customEnumeration.

For a class like enum class Foo { BAR, BAZ }, you can use the provided code below for your specific database:

MySQL, H2

+}

Exposed 0.51.1 Help

Data Types

Exposed supports the following data types in the table definition:

  • integer - translates to DB INT

  • short - translates to DB SMALLINT

  • long - BIGINT

  • float - FLOAT

  • decimal - DECIMAL with scale and precision

  • bool - BOOLEAN

  • char - CHAR

  • varchar - VARCHAR with length

  • text - TEXT

  • enumeration - INT ordinal value

  • enumerationByName - VARCHAR

  • customEnumeration - see additional section

  • blob - BLOB

  • binary - VARBINARY with length

  • uuid - BINARY(16)

  • reference - a foreign key

  • array - ARRAY

The exposed-java-time extension (org.jetbrains.exposed:exposed-java-time:$exposed_version) provides additional types:

  • date - DATETIME

  • time - TIME

  • datetime - DATETIME

  • timestamp - TIMESTAMP

  • duration - DURATION

The exposed-json extension (org.jetbrains.exposed:exposed-json:$exposed_version) provides additional types (see how to use):

  • json - JSON

  • jsonb - JSONB

How to use database ENUM types

Some of the databases (e.g. MySQL, PostgreSQL, H2) support explicit ENUM types. Because keeping such columns in sync with Kotlin enumerations using only JDBC metadata could be a huge challenge, Exposed doesn't provide a possibility to manage such columns in an automatic way, but that doesn't mean that you can't use such column types.

You have two options to work with ENUM database types and you should use customEnumeration() (available since version 0.10.3) in both cases:

  1. Use an existing ENUM column from your table. In this case, the sql parameter in customEnumeration() can be left as null.

  2. Create a new ENUM column using Exposed by providing the raw definition SQL to the sql parameter in customEnumeration().

As a JDBC driver can provide/expect specific classes for ENUM types, you must also provide from/to transformation functions for them when defining a customEnumeration.

For a class like enum class Foo { BAR, BAZ }, you can use the provided code below for your specific database:

MySQL, H2

val existingEnumColumn = customEnumeration("enumColumn", { value -> Foo.valueOf(value as String) }, { it.name }) val newEnumColumn = customEnumeration("enumColumn", "ENUM('BAR', 'BAZ')", { value -> Foo.valueOf(value as String) }, { it.name }) -

PostgreSQL

PostgreSQL requires that ENUM is defined as a separate type, so you have to create it before creating your table. Also, the PostgreSQL JDBC driver returns PGobject instances for such values, so a PGobject with its type manually set to the ENUM type needs to be used for the toDb parameter. The full working sample is provided below:

+

PostgreSQL

PostgreSQL requires that ENUM is defined as a separate type, so you have to create it before creating your table. Also, the PostgreSQL JDBC driver returns PGobject instances for such values, so a PGobject with its type manually set to the ENUM type needs to be used for the toDb parameter. The full working sample is provided below:

class PGEnum<T : Enum<T>>(enumTypeName: String, enumValue: T?) : PGobject() { init { value = enumValue?.name @@ -32,18 +32,18 @@ exec("CREATE TYPE FooEnum AS ENUM ('BAR', 'BAZ');") SchemaUtils.create(EnumTable) } -

How to use Json and JsonB types

Add the following dependencies to your build.gradle.kts:

+

How to use Json and JsonB types

Add the following dependencies to your build.gradle.kts:

val exposedVersion: String by project dependencies { implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") implementation("org.jetbrains.exposed:exposed-json:$exposedVersion") } -

Exposed works together with the JSON serialization/deserialization library of your choice by allowing column definitions that accept generic serializer and deserializer arguments:

+

Exposed works together with the JSON serialization/deserialization library of your choice by allowing column definitions that accept generic serializer and deserializer arguments:

fun <T : Any> json(name: String, serialize: (T) -> String, deserialize: (String) -> T): Column<T> fun <T : Any> jsonb(name: String, serialize: (T) -> String, deserialize: (String) -> T): Column<T> -

Here's an example that leverages kotlinx.serialization to support @Serializable classes. It uses a simpler form of json() that relies on the library's KSerializer interface:

+

Here's an example that leverages kotlinx.serialization to support @Serializable classes. It uses a simpler form of json() that relies on the library's KSerializer interface:

@Serializable data class Project(val name: String, val language: String, val active: Boolean) @@ -67,7 +67,7 @@ Teams.selectAll().map { "Team ${it[Teams.groupId]} -> ${it[Teams.project]}" }.forEach { println(it) } // Team A -> Project(name=Main, language=Kotlin, active=true) } -

Here's how the same Project and Teams would be defined using Jackson with the jackson-module-kotlin dependency and the full form of json():

+

Here's how the same Project and Teams would be defined using Jackson with the jackson-module-kotlin dependency and the full form of json():

val mapper = jacksonObjectMapper() data class Project(val name: String, val language: String, val active: Boolean) @@ -76,11 +76,11 @@ val groupId = varchar("group_id", 32) val project = json("project", { mapper.writeValueAsString(it) }, { mapper.readValue<Project>(it) }) } -

Json Functions

JSON path strings can be used to extract values (either as JSON or as a scalar value) at a specific field/key:

+

Json Functions

JSON path strings can be used to extract values (either as JSON or as a scalar value) at a specific field/key:

val projectName = Teams.project.extract<String>("name") val languageIsKotlin = Teams.project.extract<String>("language").lowerCase() eq "kotlin" Teams.select(projectName).where { languageIsKotlin }.map { it[projectName] } -

The JSON functions exists() and contains() are currently supported as well:

+

The JSON functions exists() and contains() are currently supported as well:

val hasActiveStatus = Teams.project.exists(".active") val activeProjects = Teams.selectAll().where { hasActiveStatus }.count() @@ -97,7 +97,7 @@ // MySQL example val usesKotlin = Teams.project.contains("\"Kotlin\"", ".language") val kotlinTeams = Teams.selectAll().where { usesKotlin }.count() -

Json Arrays

JSON columns also accept JSON arrays as input values. For example, using the serializable data class Project from the example above, the following details some ways to create such a column:

+

Json Arrays

JSON columns also accept JSON arrays as input values. For example, using the serializable data class Project from the example above, the following details some ways to create such a column:

object TeamProjects : Table("team_projects") { val memberIds = json<IntArray>("member_ids", Json.Default) val projects = json<Array<Project>>("projects", Json.Default) @@ -116,13 +116,13 @@ // generates SQL // INSERT INTO team_projects (member_ids, projects) VALUES ([1,2,3], [{"name":"A","language":"Kotlin","active":true},{"name":"B","language":"Java","active":true}]) } -

How to use Array types

PostgreSQL and H2 databases support the explicit ARRAY data type.

Exposed currently only supports columns defined as one-dimensional arrays, with the stored contents being any out-of-the-box or custom data type. If the contents are of a type with a supported ColumnType in the exposed-core module, the column can be simply defined with that type:

+

How to use Array types

PostgreSQL and H2 databases support the explicit ARRAY data type.

Exposed currently only supports columns defined as one-dimensional arrays, with the stored contents being any out-of-the-box or custom data type. If the contents are of a type with a supported ColumnType in the exposed-core module, the column can be simply defined with that type:

object Teams : Table("teams") { val memberIds = array<UUID>("member_ids") val memberNames = array<String>("member_names") val budgets = array<Double>("budgets") } -

If more control is needed over the base content type, or if the latter is user-defined or from a non-core module, the explicit type should be provided to the function:

+

If more control is needed over the base content type, or if the latter is user-defined or from a non-core module, the explicit type should be provided to the function:

object Teams : Table("teams") { val memberIds = array<UUID>("member_ids") val memberNames = array<String>("member_names", VarCharColumnType(colLength = 32)) @@ -130,20 +130,20 @@ val budgets = array<Double>("budgets") val expenses = array<Double?>("expenses", DoubleColumnType()).default(emptyList()) } -

This will prevent an exception being thrown if Exposed cannot find an associated column mapping for the defined type. Null array contents are allowed, and the explicit column type should be provided for these columns as well.

An array column accepts inserts and retrieves stored array contents as a Kotlin List:

+

This will prevent an exception being thrown if Exposed cannot find an associated column mapping for the defined type. Null array contents are allowed, and the explicit column type should be provided for these columns as well.

An array column accepts inserts and retrieves stored array contents as a Kotlin List:

Teams.insert { it[memberIds] = List(5) { UUID.randomUUID() } it[memberNames] = List(5) { i -> "Member ${'A' + i}" } it[budgets] = listOf(9999.0) } -

Array Functions

A single element in a stored array can be accessed using the index reference get() operator:

+

Array Functions

A single element in a stored array can be accessed using the index reference get() operator:

val firstMember = Teams.memberIds[1] Teams .select(firstMember) .where { Teams.expenses[1] greater Teams.budgets[1] } -

A new subarray can also be accessed by using slice(), which takes a lower and upper bound (inclusive):

+

A new subarray can also be accessed by using slice(), which takes a lower and upper bound (inclusive):

Teams.select(Teams.deadlines.slice(1, 3)) -

Both arguments for these bounds are optional if using PostgreSQL.

An array column can also be used as an argument for the ANY and ALL SQL operators, either by providing the entire column or a new array expression via slice():

+

Both arguments for these bounds are optional if using PostgreSQL.

An array column can also be used as an argument for the ANY and ALL SQL operators, either by providing the entire column or a new array expression via slice():

Teams .selectAll() .where { Teams.budgets[1] lessEq allFrom(Teams.expenses) } @@ -151,4 +151,4 @@ Teams .selectAll() .where { stringParam("Member A") eq anyFrom(Teams.memberNames.slice(1, 4)) } -
Last modified: 31 May 2024
\ No newline at end of file +
Last modified: 03 June 2024
diff --git a/docs/database-and-datasource.html b/docs/database-and-datasource.html index fb8478d9d6..290536a776 100644 --- a/docs/database-and-datasource.html +++ b/docs/database-and-datasource.html @@ -1,9 +1,9 @@ -Working with Database and DataSource | Exposed

Exposed 0.51.0 Help

Working with Database and DataSource

Every database access using Exposed is started by obtaining a connection and creating a transaction.
First of all, you have to tell Exposed how to connect to a database by using the Database.connect function. It won't create a real database connection but will only provide a descriptor for future usage.

A real connection will be instantiated later by calling the transaction lambda (see Transactions for more details).

Use the following to get a Database instance by simply providing connection parameters:

+}

Exposed 0.51.1 Help

Working with Database and DataSource

Every database access using Exposed is started by obtaining a connection and creating a transaction.
First of all, you have to tell Exposed how to connect to a database by using the Database.connect function. It won't create a real database connection but will only provide a descriptor for future usage.

A real connection will be instantiated later by calling the transaction lambda (see Transactions for more details).

Use the following to get a Database instance by simply providing connection parameters:

val db = Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver") -

It is also possible to provide javax.sql.DataSource for advanced behaviors such as connection pooling (see the HikariCP section):

+

It is also possible to provide javax.sql.DataSource for advanced behaviors such as connection pooling (see the HikariCP section):

val db = Database.connect(dataSource) -

DataSource

  • PostgresSQL

Database.connect("jdbc:postgresql://localhost:12346/test", driver = "org.postgresql.Driver", user = "root", password = "your_pwd") // Gradle implementation("org.postgresql:postgresql:42.7.1") -
  • PostgresSQL using the pgjdbc-ng JDBC driver

+
  • PostgresSQL using the pgjdbc-ng JDBC driver

Database.connect("jdbc:pgsql://localhost:12346/test", driver = "com.impossibl.postgres.jdbc.PGDriver", user = "root", password = "your_pwd") // Gradle implementation("com.impossibl.pgjdbc-ng:pgjdbc-ng:0.8.9") -
  • MySQL

+
  • MySQL

Database.connect("jdbc:mysql://localhost:3306/test", driver = "com.mysql.cj.jdbc.Driver", user = "root", password = "your_pwd") // Gradle implementation("mysql:mysql-connector-java:8.0.33") -
  • MariaDB

+
  • MariaDB

Database.connect("jdbc:mariadb://localhost:3306/test", driver = "org.mariadb.jdbc.Driver", user = "root", password = "your_pwd") // Gradle implementation("org.mariadb.jdbc:mariadb-java-client:3.3.1") -
  • Oracle

+
  • Oracle

Database.connect("jdbc:oracle:thin:@//localhost:1521/test", driver = "oracle.jdbc.OracleDriver", user = "root", password = "your_pwd") // Gradle // Oracle jdbc-driver should be obtained from Oracle maven repo: https://blogs.oracle.com/dev2dev/get-oracle-jdbc-drivers-and-ucp-from-oracle-maven-repository-without-ides -
  • SQLite

+
  • SQLite

// In file Database.connect("jdbc:sqlite:/data/data.db", "org.sqlite.JDBC") // In memory @@ -60,7 +60,7 @@ // or Connection.TRANSACTION_READ_UNCOMMITTED // Gradle implementation("org.xerial:sqlite-jdbc:3.44.1.0") -
  • H2

+
  • H2

// Database in file, needs full path or relative path starting with ./ Database.connect("jdbc:h2:./myh2file", "org.h2.Driver") // In memory @@ -69,12 +69,12 @@ Database.connect("jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;", "org.h2.Driver") // Gradle implementation("com.h2database:h2:2.2.224") -
  • SQL Server

+
  • SQL Server

Database.connect("jdbc:sqlserver://localhost:32768;databaseName=test", "com.microsoft.sqlserver.jdbc.SQLServerDriver", user = "root", password = "your_pwd") // Gradle implementation("com.microsoft.sqlserver:mssql-jdbc:9.4.1.jre8") -

HikariCP

To use a JDBC connection pool like HikariCP, first set up a HikariConfig class. This example uses the MySQL JDBC driver (see the official reference for MySQL configuration details):

+

HikariCP

To use a JDBC connection pool like HikariCP, first set up a HikariConfig class. This example uses the MySQL JDBC driver (see the official reference for MySQL configuration details):

val config = HikariConfig().apply { jdbcUrl = "jdbc:mysql://localhost/dbname" driverClassName = "com.mysql.cj.jdbc.Driver" @@ -89,7 +89,7 @@ // Gradle implementation "mysql:mysql-connector-java:8.0.33" implementation "com.zaxxer:HikariCP:4.0.3" -

Then instantiate a HikariDataSource with this configuration class and provide it to Database.connect():

+

Then instantiate a HikariDataSource with this configuration class and provide it to Database.connect():

val dataSource = HikariDataSource(config) Database.connect( @@ -98,4 +98,4 @@ // set other parameters here } ) -
Last modified: 31 May 2024
\ No newline at end of file +
Last modified: 03 June 2024
diff --git a/docs/deep-dive-into-dao.html b/docs/deep-dive-into-dao.html index a1923dea6e..9f798bd590 100644 --- a/docs/deep-dive-into-dao.html +++ b/docs/deep-dive-into-dao.html @@ -1,9 +1,9 @@ -Deep Dive into DAO | Exposed

Exposed 0.51.0 Help

Deep Dive into DAO

Overview

The DAO (Data Access Object) API of Exposed, is similar to ORM frameworks like Hibernate with a Kotlin-specific API.
A DB table is represented by an object inherited from org.jetbrains.exposed.sql.Table like this:

+}

Exposed 0.51.1 Help

Deep Dive into DAO

Overview

The DAO (Data Access Object) API of Exposed, is similar to ORM frameworks like Hibernate with a Kotlin-specific API.
A DB table is represented by an object inherited from org.jetbrains.exposed.sql.Table like this:

object StarWarsFilms : Table() { val id: Column<Int> = integer("id").autoIncrement() val sequelId: Column<Int> = integer("sequel_id").uniqueIndex() @@ -21,19 +21,19 @@ val director: Column<String> = varchar("director", 50) override val primaryKey = PrimaryKey(id, name = "PK_StarWarsFilms_Id") // PK_StarWarsFilms_Id is optional here } -

Tables that contain an Int id with the name id can be declared like this:

+

Tables that contain an Int id with the name id can be declared like this:

object StarWarsFilms : IntIdTable() { val sequelId: Column<Int> = integer("sequel_id").uniqueIndex() val name: Column<String> = varchar("name", 50) val director: Column<String> = varchar("director", 50) } -

Note that these Column types will be defined automatically, so you can also just leave them out. This would produce the same result as the example above:

+

Note that these Column types will be defined automatically, so you can also just leave them out. This would produce the same result as the example above:

object StarWarsFilms : IntIdTable() { val sequelId = integer("sequel_id").uniqueIndex() val name = varchar("name", 50) val director = varchar("director", 50) } -

An entity instance or a row in the table is defined as a class instance:

+

An entity instance or a row in the table is defined as a class instance:

class StarWarsFilm(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<StarWarsFilm>(StarWarsFilms) var sequelId by StarWarsFilms.sequelId @@ -46,29 +46,29 @@ sequelId = 8 director = "Rian Johnson" } -

Read

To get entities use one of the following

+

Read

To get entities use one of the following

val movies = StarWarsFilm.all() val movies = StarWarsFilm.find { StarWarsFilms.sequelId eq 8 } val movie = StarWarsFilm.findById(5) -
  • For a list of available predicates, see DSL Where expression.


    Read a value from a property similar to any property in a Kotlin class:

+
  • For a list of available predicates, see DSL Where expression.


    Read a value from a property similar to any property in a Kotlin class:

val name = movie.name -

Sort (Order-by)

Ascending order:

+

Sort (Order-by)

Ascending order:

val movies = StarWarsFilm.all().sortedBy { it.sequelId } -

Descending order:

+

Descending order:

val movies = StarWarsFilm.all().sortedByDescending{ it.sequelId } -

Update

Update the value of a property similar to any property in a Kotlin class:

+

Update

Update the value of a property similar to any property in a Kotlin class:

movie.name = "Episode VIII – The Last Jedi" -
  • Note: Exposed doesn't make an immediate update when you set a new value for Entity, it just stores it on the inner map. "Flushing" values to the database occurs at the end of the transaction, or before the next select * from the database.

Search for an entity by its id and apply an update:

+
  • Note: Exposed doesn't make an immediate update when you set a new value for Entity, it just stores it on the inner map. "Flushing" values to the database occurs at the end of the transaction, or before the next select * from the database.

Search for an entity by its id and apply an update:

val updatedMovie = StarWarsFilm.findByIdAndUpdate(5) { it.name = "Episode VIII – The Last Jedi" } -

Search for a single entity by a query and apply an update:

+

Search for a single entity by a query and apply an update:

val updatedMovie2 = StarWarsFilm.findSingleByAndUpdate(StarWarsFilms.name eq "The Last Jedi") { it.name = "Episode VIII – The Last Jedi" }

Delete

movie.delete() -

Referencing

many-to-one reference

Let's say you have this table:

+

Referencing

many-to-one reference

Let's say you have this table:

object Users : IntIdTable() { val name = varchar("name", 50) } @@ -76,7 +76,7 @@ companion object : IntEntityClass<User>(Users) var name by Users.name } -

And now you want to add a table referencing this table (and other tables!):

+

And now you want to add a table referencing this table (and other tables!):

object UserRatings : IntIdTable() { val value = long("value") val film = reference("film", StarWarsFilms) @@ -88,26 +88,26 @@ var film by StarWarsFilm referencedOn UserRatings.film // use referencedOn for normal references var user by User referencedOn UserRatings.user } -

Now you can get the film for a UserRating object, filmRating, in the same way you would get any other field:

+

Now you can get the film for a UserRating object, filmRating, in the same way you would get any other field:

filmRating.film // returns a StarWarsFilm object -

Now if you wanted to get all the ratings for a film, you could do that by using the filmRating.find function, but it is much easier to just add a referrersOn field to the StarWarsFilm class:

+

Now if you wanted to get all the ratings for a film, you could do that by using the filmRating.find function, but it is much easier to just add a referrersOn field to the StarWarsFilm class:

class StarWarsFilm(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<StarWarsFilm>(StarWarsFilms) ... val ratings by UserRating referrersOn UserRatings.film // make sure to use val and referrersOn ... } -

You can then access this field on a StarWarsFilm object, movie:

+

You can then access this field on a StarWarsFilm object, movie:

movie.ratings // returns all UserRating objects with this movie as film -

Now imagine a scenario where a user only ever rates a single film. If you want to get the single rating for that user, you can add a backReferencedOn field to the User class to access the UserRating table data:

+

Now imagine a scenario where a user only ever rates a single film. If you want to get the single rating for that user, you can add a backReferencedOn field to the User class to access the UserRating table data:

class User(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<User>(Users) ... val rating by UserRating backReferencedOn UserRatings.user // make sure to use val and backReferencedOn } -

You can then access this field on a User object, user1:

+

You can then access this field on a User object, user1:

user1.rating // returns a UserRating object -

Optional reference

You can also add an optional reference:

+

Optional reference

You can also add an optional reference:

object UserRatings: IntIdTable() { ... val secondUser = reference("second_user", Users).nullable() // this reference is nullable! @@ -119,14 +119,14 @@ var secondUser by User optionalReferencedOn UserRatings.secondUser // use optionalReferencedOn for nullable references ... } -

Now secondUser will be a nullable field, and optionalReferrersOn should be used instead of referrersOn to get all the ratings for a secondUser.

+

Now secondUser will be a nullable field, and optionalReferrersOn should be used instead of referrersOn to get all the ratings for a secondUser.

class User(id: EntityID<Int>): IntEntity(id) { companion object : IntEntityClass<User>(Users) ... val secondRatings by UserRating optionalReferrersOn UserRatings.secondUser // make sure to use val and optionalReferrersOn ... } -

many-to-many reference

In some cases, a many-to-many reference may be required. Let's assume you want to add a reference to the following Actors table to the StarWarsFilm class:

+

many-to-many reference

In some cases, a many-to-many reference may be required. Let's assume you want to add a reference to the following Actors table to the StarWarsFilm class:

object Actors: IntIdTable() { val firstname = varchar("firstname", 50) val lastname = varchar("lastname", 50) @@ -136,20 +136,20 @@ var firstname by Actors.firstname var lastname by Actors.lastname } -

Create an additional intermediate table to store the references:

+

Create an additional intermediate table to store the references:

object StarWarsFilmActors : Table() { val starWarsFilm = reference("starWarsFilm", StarWarsFilms) val actor = reference("actor", Actors) override val primaryKey = PrimaryKey(starWarsFilm, actor, name = "PK_StarWarsFilmActors_swf_act") // PK_StarWarsFilmActors_swf_act is optional here } -

Add a reference to StarWarsFilm:

+

Add a reference to StarWarsFilm:

class StarWarsFilm(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<StarWarsFilm>(StarWarsFilms) ... var actors by Actor via StarWarsFilmActors ... } -

Note: You can set up IDs manually inside a transaction like this:

+

Note: You can set up IDs manually inside a transaction like this:

transaction { // only works with UUIDTable and UUIDEntity StarWarsFilm.new (UUID.randomUUID()){ @@ -157,7 +157,7 @@ actors = SizedCollection(listOf(actor)) } } -

Parent-Child reference

Parent-child reference is very similar to many-to-many version, but an intermediate table contains both references to the same table. Let's assume you want to build a hierarchical entity which could have parents and children. Our tables and an entity mapping will look like

+

Parent-Child reference

Parent-child reference is very similar to many-to-many version, but an intermediate table contains both references to the same table. Let's assume you want to build a hierarchical entity which could have parents and children. Our tables and an entity mapping will look like

object NodeTable : IntIdTable() { val name = varchar("name", 50) } @@ -171,7 +171,7 @@ var parents by Node.via(NodeToNodes.child, NodeToNodes.parent) var children by Node.via(NodeToNodes.parent, NodeToNodes.child) } -

As you can see NodeToNodes columns target only NodeTable and another version of via function were used. Now you can create a hierarchy of nodes.

+

As you can see NodeToNodes columns target only NodeTable and another version of via function were used. Now you can create a hierarchy of nodes.

val root = Node.new { name = "root" } val child1 = Node.new { name = "child1" @@ -179,26 +179,26 @@ child1.parents = SizedCollection(root) // assign parent val child2 = Node.new { name = "child2" } root.children = SizedCollection(listOf(child1, child2)) // assign children -

Eager Loading

Available since 0.13.1. References in Exposed are lazily loaded, meaning queries to fetch the data for the reference are made at the moment the reference is first utilised. For scenarios wherefore you know you will require references ahead of time, Exposed can eager load them at the time of the parent query, this is prevents the classic "N+1" problem as references can be aggregated and loaded in a single query. To eager load a reference you can call the "load" function and pass the DAO's reference as a KProperty:

+

Eager Loading

Available since 0.13.1. References in Exposed are lazily loaded, meaning queries to fetch the data for the reference are made at the moment the reference is first utilised. For scenarios wherefore you know you will require references ahead of time, Exposed can eager load them at the time of the parent query, this is prevents the classic "N+1" problem as references can be aggregated and loaded in a single query. To eager load a reference you can call the "load" function and pass the DAO's reference as a KProperty:

StarWarsFilm.findById(1).load(StarWarsFilm::actors) -

This works for references of references also, for example if Actors had a rating reference you could:

+

This works for references of references also, for example if Actors had a rating reference you could:

StarWarsFilm.findById(1).load(StarWarsFilm::actors, Actor::rating) -

Similarly, you can eagerly load references on Collections of DAO's such as Lists and SizedIterables, for collections you can use the with function in the same fashion as before, passing the DAO's references as KProperty's.

+

Similarly, you can eagerly load references on Collections of DAO's such as Lists and SizedIterables, for collections you can use the with function in the same fashion as before, passing the DAO's references as KProperty's.

StarWarsFilm.all().with(StarWarsFilm::actors) -

NOTE: References that are eagerly loaded are stored inside the transaction cache; this means that they are not available in other transactions and thus must be loaded and referenced inside the same transaction. As of 0.35.1, enabling keepLoadedReferencesOutOfTransaction in DatabaseConfig will allow getting referenced values outside the transaction block.

Eager loading for Text Fields

Some database drivers do not load text content immediately (for performance and memory reasons) which means that you can obtain the column value only within the open transaction.

If you desire to make content available outside the transaction, you can use the eagerLoading param when defining the DB Table.

+

NOTE: References that are eagerly loaded are stored inside the transaction cache; this means that they are not available in other transactions and thus must be loaded and referenced inside the same transaction. As of 0.35.1, enabling keepLoadedReferencesOutOfTransaction in DatabaseConfig will allow getting referenced values outside the transaction block.

Eager loading for Text Fields

Some database drivers do not load text content immediately (for performance and memory reasons) which means that you can obtain the column value only within the open transaction.

If you desire to make content available outside the transaction, you can use the eagerLoading param when defining the DB Table.

object StarWarsFilms : Table() { ... val description = text("name", eagerLoading=true) } -

Advanced CRUD operations

Read entity with a join to another table

Let's imagine that you want to find all users who rated second SW film with more than 5. First of all, we should write that query using Exposed DSL.

+

Advanced CRUD operations

Read entity with a join to another table

Let's imagine that you want to find all users who rated second SW film with more than 5. First of all, we should write that query using Exposed DSL.

val query = Users.innerJoin(UserRatings).innerJoin(StarWarsFilm) .select(Users.columns) .where { StarWarsFilms.sequelId eq 2 and (UserRatings.value gt 5) }.withDistinct() -

After that all we have to do is to "wrap" a result with User entity:

+

After that all we have to do is to "wrap" a result with User entity:

val users = User.wrapRows(query).toList() -

Auto-fill created and updated columns on entity change

See example by @PaulMuriithi here.

Use queries as expressions

Imagine that you want to sort cities by how many users each city has. In order to do so, you can write a sub-query which counts users in each city and order by that number. Though in order to do so you'll have to convert Query to Expression. This can be done using wrapAsExpression function:

+

Auto-fill created and updated columns on entity change

See example by @PaulMuriithi here.

Use queries as expressions

Imagine that you want to sort cities by how many users each city has. In order to do so, you can write a sub-query which counts users in each city and order by that number. Though in order to do so you'll have to convert Query to Expression. This can be done using wrapAsExpression function:

val expression = wrapAsExpression<Int>(Users .select(Users.id.count()) .where { @@ -208,7 +208,7 @@ .selectAll() .orderBy(expression, SortOrder.DESC) .toList() -

Add computed fields to entity class

Imagine that you want to use a window function to rank films with each entity fetch. The companion object of the entity class can override any open function in EntityClass, but to achieve this functionality only searchQuery() needs to be overriden. The results of the function can then be accessed using a property of the entity class:

+

Add computed fields to entity class

Imagine that you want to use a window function to rank films with each entity fetch. The companion object of the entity class can override any open function in EntityClass, but to achieve this functionality only searchQuery() needs to be overriden. The results of the function can then be accessed using a property of the entity class:

object StarWarsFilms : IntIdTable() { val sequelId = integer("sequel_id").uniqueIndex() val name = varchar("name", 50) @@ -246,7 +246,7 @@ // fetch entities with value (or store entities then read value) StarWarsFilm.find { StarWarsFilms.name like "The%" }.map { it.name to it.rank } } -

Entities mapping

Fields transformation

As databases could store only basic types like integers and strings it's not always conveniently to keep the same simplicity on DAO level. Sometimes you may want to make some transformations like parsing json from a varchar column or get some value from a cache based on value from a database. In that case the preferred way is to use column transformations. Assume that we want to define unsigned integer field on Entity, but Exposed doesn't have such column type yet.

+

Entities mapping

Fields transformation

As databases could store only basic types like integers and strings it's not always conveniently to keep the same simplicity on DAO level. Sometimes you may want to make some transformations like parsing json from a varchar column or get some value from a cache based on value from a database. In that case the preferred way is to use column transformations. Assume that we want to define unsigned integer field on Entity, but Exposed doesn't have such column type yet.

object TableWithUnsignedInteger : IntIdTable() { val uint = integer("uint") } @@ -255,4 +255,4 @@ companion object : IntEntityClass<EntityWithUInt>() } -

transform function accept two lambdas to convert values to and from an original column type. After that in your code you'll be able to put only UInt instances into uint field. It still possible to insert/update values with negative integers via DAO, but your business code becomes much cleaner. Please keep in mind what such transformations will aqure on every access to a field what means that you should avoid heavy transformations here.

Last modified: 31 May 2024
\ No newline at end of file +

transform function accept two lambdas to convert values to and from an original column type. After that in your code you'll be able to put only UInt instances into uint field. It still possible to insert/update values with negative integers via DAO, but your business code becomes much cleaner. Please keep in mind what such transformations will aqure on every access to a field what means that you should avoid heavy transformations here.

Last modified: 03 June 2024
diff --git a/docs/deep-dive-into-dsl.html b/docs/deep-dive-into-dsl.html index b134eea2bd..2371631189 100644 --- a/docs/deep-dive-into-dsl.html +++ b/docs/deep-dive-into-dsl.html @@ -1,9 +1,9 @@ -Deep Dive into DSL | Exposed

Exposed 0.51.0 Help

Deep Dive into DSL

Overview

The DSL (Domain-Specific Language) API of Exposed is similar to actual SQL statements, but with the type safety that Kotlin offers.

A database table is represented by an object inherited from org.jetbrains.exposed.sql.Table like this:

+}

Exposed 0.51.1 Help

Deep Dive into DSL

Overview

The DSL (Domain-Specific Language) API of Exposed is similar to actual SQL statements, but with the type safety that Kotlin offers.

A database table is represented by an object inherited from org.jetbrains.exposed.sql.Table like this:

object StarWarsFilms : Table() { val id: Column<Int> = integer("id").autoIncrement() val sequelId: Column<Int> = integer("sequel_id").uniqueIndex() @@ -21,13 +21,13 @@ val director: Column<String> = varchar("director", 50) override val primaryKey = PrimaryKey(id, name = "PK_StarWarsFilms_Id") // PK_StarWarsFilms_Id is optional here } -

Tables that contains Int id with the name id can be declared like this:

+

Tables that contains Int id with the name id can be declared like this:

object StarWarsFilms : IntIdTable() { val sequelId: Column<Int> = integer("sequel_id").uniqueIndex() val name: Column<String> = varchar("name", 50) val director: Column<String> = varchar("director", 50) } -

CRUD operations

CRUD stands for Create Read Update Delete, which are four basic operations for a database to support. This section shows how to perform SQL CRUD operations using Kotlin DSL.

Create

To create a new table row, you use the insert query. Exposed provides several functions to insert rows into a table:

  • insert adds a new row. If the same row already exists in the table, it throws an exception.

    +

CRUD operations

CRUD stands for Create Read Update Delete, which are four basic operations for a database to support. This section shows how to perform SQL CRUD operations using Kotlin DSL.

Create

To create a new table row, you use the insert query. Exposed provides several functions to insert rows into a table:

  • insert adds a new row. If the same row already exists in the table, it throws an exception.

    // SQL: INSERT INTO CITIES (COUNTRY, "NAME", POPULATION) // VALUES ('RUSSIA', 'St. Petersburg', 300) Cities.insert { @@ -35,7 +35,7 @@ it[country] = Country.RUSSIA it[population] = 500 } -
  • insertAndGetId adds a new row and returns its ID. If the same row already exists in the table, it throws an exception. Works only with IntIdTable() tables.

    +
  • insertAndGetId adds a new row and returns its ID. If the same row already exists in the table, it throws an exception. Works only with IntIdTable() tables.

    // SQL: INSERT INTO CITIES (COUNTRY, "NAME", POPULATION) // VALUES ('RUSSIA', 'St. Petersburg', 300) val id = Cities.insertAndGetId { @@ -43,7 +43,7 @@ it[country] = Country.RUSSIA it[population] = 500 } -
  • insertIgnore adds a new row. If the same row already exists in the table, it ignores it and doesn't throw an exception. This function is supported only for MySQL, PostgreSQL, and SQLite.

    +
  • insertIgnore adds a new row. If the same row already exists in the table, it ignores it and doesn't throw an exception. This function is supported only for MySQL, PostgreSQL, and SQLite.

    // SQL: INSERT IGNORE INTO CITIES (COUNTRY, "NAME", POPULATION) // VALUES ('RUSSIA', 'St. Petersburg', 300) Cities.insertIgnore { @@ -51,7 +51,7 @@ it[country] = Country.RUSSIA it[population] = 500 } -
  • insertIgnoreAndGetId adds a new row and returns its ID. If the same row already exists in the table, it ignores it and doesn't throw an exception. This function is supported only for MySQL, PostgreSQL, and SQLite. Works only with IntIdTable() tables.

    +
  • insertIgnoreAndGetId adds a new row and returns its ID. If the same row already exists in the table, it ignores it and doesn't throw an exception. This function is supported only for MySQL, PostgreSQL, and SQLite. Works only with IntIdTable() tables.

    // SQL: INSERT IGNORE INTO CITIES (COUNTRY, "NAME", POPULATION) // VALUES ('RUSSIA', 'St. Petersburg', 300) val id = Cities.insertIgnoreAndGetId { @@ -59,7 +59,7 @@ it[country] = Country.RUSSIA it[population] = 500 } -

Some databases return a count of the number of rows inserted, updated, or deleted by the CRUD operation. For insert(), upsert(), and replace(), this value can be accessed using the statement class property, insertedCount:

+

Some databases return a count of the number of rows inserted, updated, or deleted by the CRUD operation. For insert(), upsert(), and replace(), this value can be accessed using the statement class property, insertedCount:

val insertStatement = StarWarsFilms.insertIgnore { it[name] = "The Last Jedi" it[sequelId] = 8 @@ -68,15 +68,15 @@ val rowCount: Int = insertStatement.insertedCount

Read

val query: Query = StarWarsFilms.selectAll().where { StarWarsFilms.sequelId eq 8 } -

Query inherit Iterable so it is possible to traverse it with map/foreach etc'. For example:

+

Query inherit Iterable so it is possible to traverse it with map/foreach etc'. For example:

StarWarsFilms.selectAll().where { StarWarsFilms.sequelId eq 8 }.forEach { println(it[StarWarsFilms.name]) } -

There is select function which allows you to select specific columns or/and expressions.

+

There is select function which allows you to select specific columns or/and expressions.

val filmAndDirector = StarWarsFilms.select(StarWarsFilms.name, StarWarsFilms.director).map { it[StarWarsFilms.name] to it[StarWarsFilms.director] } -

If you want to select only distinct value then use withDistinct() function:

+

If you want to select only distinct value then use withDistinct() function:

val directors = StarWarsFilms.select(StarWarsFilms.director).where { StarWarsFilms.sequelId less 5 }.withDistinct().map { it[StarWarsFilms.director] } @@ -84,7 +84,7 @@ StarWarsFilms.update({ StarWarsFilms.sequelId eq 8 }) { it[StarWarsFilms.name] = "Episode VIII – The Last Jedi" } -

If you want to update column value with some expression like increment use update function or setter:

+

If you want to update column value with some expression like increment use update function or setter:

StarWarsFilms.update({ StarWarsFilms.sequelId eq 8 }) { with(SqlExpressionBuilder) { it.update(StarWarsFilms.sequelId, StarWarsFilms.sequelId + 1) @@ -92,13 +92,13 @@ it[StarWarsFilms.sequelId] = StarWarsFilms.sequelId + 1 } } -

If you want to know the number of updated rows, this value is also returned by the update() function:

+

If you want to know the number of updated rows, this value is also returned by the update() function:

val updatedCount = StarWarsFilms.update({ StarWarsFilms.name like "Episode%" }) { it[StarWarsFilms.director] = StarWarsFilms.director.upperCase() }

Delete

StarWarsFilms.deleteWhere { StarWarsFilms.sequelId eq 8 } -

Delete functions also return a count of the number of deleted rows, as for Update above.

Returning Data from Modified Rows

Some databases (like PostgreSQL and SQLite) allow the return of additional data every time a row is either inserted, updated, or deleted by a CRUD operation. This can be accomplished by using insertReturning(), upsertReturning(), updateReturning(), or deleteReturning() with a list of the required table columns provided as an argument. If the latter is omitted, all table columns will be returned by default:

+

Delete functions also return a count of the number of deleted rows, as for Update above.

Returning Data from Modified Rows

Some databases (like PostgreSQL and SQLite) allow the return of additional data every time a row is either inserted, updated, or deleted by a CRUD operation. This can be accomplished by using insertReturning(), upsertReturning(), updateReturning(), or deleteReturning() with a list of the required table columns provided as an argument. If the latter is omitted, all table columns will be returned by default:

object Projects : Table("projects") { val title = varchar("title", 64) val budget = integer("budget") @@ -116,7 +116,7 @@ }.map { it[Projects.budget] } -

Where expression

Query expression (where) expects a boolean operator (ie: Op<Boolean>). Allowed conditions are:

+

Where expression

Query expression (where) expects a boolean operator (ie: Op<Boolean>). Allowed conditions are:

eq - (==) neq - (!=) isNull() @@ -136,7 +136,7 @@ match (MySQL MATCH AGAINST) isDistinctFrom (null-safe equality comparison) isNotDistinctFrom (null-safe equality comparison) -

Allowed logical conditions are:

+

Allowed logical conditions are:

not and or @@ -144,7 +144,7 @@ orIfNotNull compoundAnd() compoundOr() -

Conditional where

It is a rather common case to have a query with a where clause that depends on some other code's conditions. Moreover, independent or nested conditions could make it more complicated to prepare such where clauses. Let's imagine that we have a form on a website where a user can optionally filter "Star Wars" films by a director and/or a sequel. In Exposed version before 0.8.1 you had to code it like:

+

Conditional where

It is a rather common case to have a query with a where clause that depends on some other code's conditions. Moreover, independent or nested conditions could make it more complicated to prepare such where clauses. Let's imagine that we have a form on a website where a user can optionally filter "Star Wars" films by a director and/or a sequel. In Exposed version before 0.8.1 you had to code it like:

val condition = when { directorName!=null && sequelId!=null -> Op.build { StarWarsFilms.director eq directorName and (StarWarsFilms.sequelId eq sequelId) } @@ -155,7 +155,7 @@ else -> null } val query = condition?.let { StarWarsFilms.selectAll().where(condition) } ?: StarWarsFilms.selectAll() -

or

+

or

val query = when { directorName!=null && sequelId!=null -> StarWarsFilms.selectAll().where { StarWarsFilms.director eq directorName and (StarWarsFilms.sequelId eq sequelId) } @@ -165,7 +165,7 @@ StarWarsFilms.selectAll().where { StarWarsFilms.sequelId eq sequelId } else -> StarWarsFilms.selectAll() } -

This is a very primitive example, but you should get the main idea about the problem. Now let's try to write the same query in a more simple way (andWhere function available since 0.10.5):

+

This is a very primitive example, but you should get the main idea about the problem. Now let's try to write the same query in a more simple way (andWhere function available since 0.10.5):

val query = StarWarsFilms.selectAll() directorName?.let { query.andWhere { StarWarsFilms.director eq it } @@ -173,7 +173,7 @@ sequelId?.let { query.andWhere { StarWarsFilms.sequelId eq it } } -

But what if we want to conditionally select from another table and join it only when a condition is true? You have to use adjustColumnSet and adjustSelect functions, which allow to extend and modify join and select parts of a query (see kdoc on that functions):

+

But what if we want to conditionally select from another table and join it only when a condition is true? You have to use adjustColumnSet and adjustSelect functions, which allow to extend and modify join and select parts of a query (see kdoc on that functions):

actorName?.let { query.adjustColumnSet { innerJoin(Actors, { StarWarsFilms.sequelId }, { Actors.sequelId }) } .adjustSelect { select(fields + Actors.columns).set } @@ -181,38 +181,38 @@ }

Check for a match in a pattern

StarWarsFilms.selectAll().where { StarWarsFilms.name like "The %" } -

notLike is also available to check for expressions that do not match the provided pattern.

To perform a pattern match that supports regular expressions, use regexp instead:

+

notLike is also available to check for expressions that do not match the provided pattern.

To perform a pattern match that supports regular expressions, use regexp instead:

StarWarsFilms.selectAll().where { StarWarsFilms.name regexp "^The(\\s\\w+){2}\$" }

Check for a match in a range

StarWarsFilms.selectAll().where { StarWarsFilms.sequelId.between(4, 6) } -

The between operator returns true if the expression is between the lower and upper range values (inclusive). Date and time values are also supported as arguments.

Check for a match in a collection

+

The between operator returns true if the expression is between the lower and upper range values (inclusive). Date and time values are also supported as arguments.

Check for a match in a collection

StarWarsFilms.selectAll().where { StarWarsFilms.sequelId inList listOf(6, 4) } -

inList also accepts multiple expressions to check for equality, either as a Pair or a Triple:

+

inList also accepts multiple expressions to check for equality, either as a Pair or a Triple:

val topRated = listOf(5 to "Empire Strikes Back", 4 to "A New Hope") StarWarsFilms.selectAll().where { StarWarsFilms.sequelId to StarWarsFilms.name inList topRated } -

notInList is available to check for expressions that are not equal to any elements in the provided collection.

In addition to the IN operator, the ANY and ALL operators are available with any preceding comparison operator:

+

notInList is available to check for expressions that are not equal to any elements in the provided collection.

In addition to the IN operator, the ANY and ALL operators are available with any preceding comparison operator:

StarWarsFilms.selectAll().where { StarWarsFilms.sequelId eq anyFrom(arrayOf(6, 4)) } -

anyFrom() and allFrom() also accept subqueries, tables, and array expressions as arguments.

Count

count() is a method of Query that is used like in the example below:

+

anyFrom() and allFrom() also accept subqueries, tables, and array expressions as arguments.

Count

count() is a method of Query that is used like in the example below:

val count = StarWarsFilms.selectAll().where { StarWarsFilms.sequelId eq 8 }.count() -

Order-by

Order-by accepts a list of columns mapped to boolean indicates if sorting should be ascending or descending. Example:

+

Order-by

Order-by accepts a list of columns mapped to boolean indicates if sorting should be ascending or descending. Example:

StarWarsFilms.selectAll().orderBy(StarWarsFilms.sequelId to SortOrder.ASC) -

Group-by

In group-by, define fields and their functions (such as count) by the select() method.

+

Group-by

In group-by, define fields and their functions (such as count) by the select() method.

StarWarsFilms .select(StarWarsFilms.sequelId.count(), StarWarsFilms.director) .groupBy(StarWarsFilms.director) -

Available functions are:

+

Available functions are:

count sum max min average ... -

Limit

You can use limit function to prevent loading large data sets or use it for pagination with second offset parameter.

+

Limit

You can use limit function to prevent loading large data sets or use it for pagination with second offset parameter.

// Take 2 films after the first one. StarWarsFilms.selectAll().where { StarWarsFilms.sequelId eq Actors.sequelId }.limit(2, offset = 1) -

Join

For the join examples below, consider the following tables:

+

Join

For the join examples below, consider the following tables:

object StarWarsFilms : IntIdTable() { val sequelId: Column<Int> = integer("sequel_id").uniqueIndex() val name: Column<String> = varchar("name", 50) @@ -227,42 +227,42 @@ val actorId: Column<EntityID<Int>> = reference("actor_id", Actors) val characterName: Column<String> = varchar("name", 50) } -

Join to count how many actors star in each movie:

+

Join to count how many actors star in each movie:

Actors.join(StarWarsFilms, JoinType.INNER, onColumn = Actors.sequelId, otherColumn = StarWarsFilms.sequelId) .select(Actors.name.count(), StarWarsFilms.name) .groupBy(StarWarsFilms.name) -

Instead of specifying onColumn and otherColumn, additionalConstraint can be used (and allows specifying other types of join conditions).

+

Instead of specifying onColumn and otherColumn, additionalConstraint can be used (and allows specifying other types of join conditions).

Actors.join(StarWarsFilms, JoinType.INNER, additionalConstraint = { StarWarsFilms.sequelId eq Actors.sequelId }) .select(Actors.name.count(), StarWarsFilms.name) .groupBy(StarWarsFilms.name) -

When joining on a foreign key, the more concise innerJoin can be used:

+

When joining on a foreign key, the more concise innerJoin can be used:

(Actors innerJoin Roles) .select(Roles.characterName.count(), Actors.name) .groupBy(Actors.name) .toList() -

This is equivalent to the following:

+

This is equivalent to the following:

Actors.join(Roles, JoinType.INNER, onColumn = Actors.id, otherColumn = Roles.actorId) .select(Roles.characterName.count(), Actors.name) .groupBy(Actors.name) .toList() -

Union

You can combine the results of multiple queries using .union(...). Per the SQL specification, the queries must have the same number of columns, and not be marked for update. Subqueries may be combined when supported by the database.

+

Union

You can combine the results of multiple queries using .union(...). Per the SQL specification, the queries must have the same number of columns, and not be marked for update. Subqueries may be combined when supported by the database.

val lucasDirectedQuery = StarWarsFilms.select(StarWarsFilms.name).where { StarWarsFilms.director eq "George Lucas" } val abramsDirectedQuery = StarWarsFilms.select(StarWarsFilms.name).where { StarWarsFilms.director eq "J.J. Abrams" } val filmNames = lucasDirectedQuery.union(abramsDirectedQuery).map { it[StarWarsFilms.name] } -

Only unique rows are returned by default. Duplicates may be returned using .unionAll().

+

Only unique rows are returned by default. Duplicates may be returned using .unionAll().

val lucasDirectedQuery = StarWarsFilms.select(StarWarsFilms.name).where { StarWarsFilms.director eq "George Lucas" } val originalTrilogyQuery = StarWarsFilms.select(StarWarsFilms.name).where { StarWarsFilms.sequelId inList (3..5) } val filmNames = lucasDirectedQuery.unionAll(originalTrilogyQuery).map { it[StarWarsFilms.name] } -

Alias

Aliases allow preventing ambiguity between field names and table names. Use the aliased var instead of original one:

+

Alias

Aliases allow preventing ambiguity between field names and table names. Use the aliased var instead of original one:

val filmTable1 = StarWarsFilms.alias("ft1") filmTable1.selectAll() // can be used in joins etc' -

Also, aliases allow you to use the same table in a join multiple times:

+

Also, aliases allow you to use the same table in a join multiple times:

val sequelTable = StarWarsFilms.alias("sql") val originalAndSequelNames = StarWarsFilms .innerJoin(sequelTable, { StarWarsFilms.sequelId }, { sequelTable[StarWarsFilms.id] }) .select(StarWarsFilms.name, sequelTable[StarWarsFilms.name]) .map { it[StarWarsFilms.name] to it[sequelTable[StarWarsFilms.name]] } -

And they can be used when selecting from sub-queries:

+

And they can be used when selecting from sub-queries:

val starWarsFilms = StarWarsFilms .select(StarWarsFilms.id, StarWarsFilms.name) .alias("swf") @@ -271,19 +271,19 @@ starWarsFilms .select(id, name) .map { it[id] to it[name] } -

Schema

You can create a schema or drop an existing one:

+

Schema

You can create a schema or drop an existing one:

val schema = Schema("my_schema") // my_schema is the schema name. // Creates a Schema SchemaUtils.createSchema(schema) // Drops a Schema SchemaUtils.dropSchema(schema) -

Also, you can specify the schema owner like this (some databases require the explicit owner) :

+

Also, you can specify the schema owner like this (some databases require the explicit owner) :

val schema = Schema("my_schema", authorization = "owner") -

If you have many schemas and you want to set a default one, you can use:

+

If you have many schemas and you want to set a default one, you can use:

SchemaUtils.setSchema(schema) -

Sequence

If you want to use Sequence, Exposed allows you to:

Define a Sequence

+

Sequence

If you want to use Sequence, Exposed allows you to:

Define a Sequence

val myseq = Sequence("my_sequence") // my_sequence is the sequence name. -

Several parameters can be specified to control the properties of the sequence:

+

Several parameters can be specified to control the properties of the sequence:

private val myseq = Sequence( name = "my_sequence", startWith = 4, @@ -298,7 +298,7 @@ SchemaUtils.createSequence(myseq) // Drops a sequence SchemaUtils.dropSequence(myseq) -

Use the NextVal function

You can use the nextVal function like this:

+

Use the NextVal function

You can use the nextVal function like this:

val nextVal = myseq.nextVal() val id = StarWarsFilms.insertAndGetId { it[id] = nextVal @@ -308,12 +308,12 @@ }
val firstValue = StarWarsFilms.select(nextVal).single()[nextVal] -

Batch Insert

Batch Insert allow mapping a list of entities into DB raws in one sql statement. It is more efficient than inserting one by one as it initiates only one statement. Here is an example that uses a simple list:

+

Batch Insert

Batch Insert allow mapping a list of entities into DB raws in one sql statement. It is more efficient than inserting one by one as it initiates only one statement. Here is an example that uses a simple list:

val cityNames = listOf("Paris", "Moscow", "Helsinki") val allCitiesID = cities.batchInsert(cityNames) { name -> this[cities.name] = name } -

Here is an example that uses a list of data class instances:

+

Here is an example that uses a list of data class instances:

data class SWFilmData(val sequelId: Int, val name: String, val director: String) transaction { @@ -332,13 +332,13 @@ StarWarsFilms.selectAll().count() // 3 } -

If you don't need to get the newly generated values (example: auto incremented ID), set the shouldReturnGeneratedValues parameter to false, this increases the performance of batch inserts by batching them in chunks, instead of always waiting for the database to synchronize the newly inserted object state.

If you want to check if the rewriteBatchedInserts + batchInsert is working correctly, check how to enable JDBC logging for your driver because Exposed will always show the non-rewritten multiple inserts. You can find the documentation for how to enable logging in PostgresSQL here.

Insert From Select

If you want to use INSERT INTO ... SELECT SQL clause try Exposed analog Table.insert(Query).

+

If you don't need to get the newly generated values (example: auto incremented ID), set the shouldReturnGeneratedValues parameter to false, this increases the performance of batch inserts by batching them in chunks, instead of always waiting for the database to synchronize the newly inserted object state.

If you want to check if the rewriteBatchedInserts + batchInsert is working correctly, check how to enable JDBC logging for your driver because Exposed will always show the non-rewritten multiple inserts. You can find the documentation for how to enable logging in PostgresSQL here.

Insert From Select

If you want to use INSERT INTO ... SELECT SQL clause try Exposed analog Table.insert(Query).

val substring = users.name.substring(1, 2) cities.insert(users.select(substring).orderBy(users.id).limit(2)) -

By default it will try to insert into all non auto-increment Table columns in order they defined in Table instance. If you want to specify columns or change the order, provide list of columns as second parameter:

+

By default it will try to insert into all non auto-increment Table columns in order they defined in Table instance. If you want to specify columns or change the order, provide list of columns as second parameter:

val userCount = users.selectAll().count() users.insert(users.select(stringParam("Foo"), Random().castTo<String>(VarCharColumnType()).substring(1, 10)), columns = listOf(users.name, users.id)) -

Insert Or Ignore

If supported by your specific database, insertIgnore() allows insert statements to be executed without throwing any ignorable errors. This may be useful, for example, when insertion conflicts are possible:

+

Insert Or Ignore

If supported by your specific database, insertIgnore() allows insert statements to be executed without throwing any ignorable errors. This may be useful, for example, when insertion conflicts are possible:

StarWarsFilms.insert { it[sequelId] = 8 // column pre-defined with a unique index it[name] = "The Last Jedi" @@ -351,7 +351,7 @@ it[name] = "The Rise of Skywalker" it[director] = "JJ Abrams" } -

Insert Or Update

Insert or update (Upsert) is a database operation that either inserts a new row or updates an existing row if a duplicate constraint already exists. The supported functionality of upsert() is dependent on the specific database being used. For example, MySQL's INSERT ... ON DUPLICATE KEY UPDATE statement automatically assesses the primary key and unique indices for a duplicate value, so using the function in Exposed would look like this:

+

Insert Or Update

Insert or update (Upsert) is a database operation that either inserts a new row or updates an existing row if a duplicate constraint already exists. The supported functionality of upsert() is dependent on the specific database being used. For example, MySQL's INSERT ... ON DUPLICATE KEY UPDATE statement automatically assesses the primary key and unique indices for a duplicate value, so using the function in Exposed would look like this:

// inserts a new row StarWarsFilms.upsert { it[sequelId] = 9 // column pre-defined with a unique index @@ -364,7 +364,7 @@ it[name] = "The Rise of Skywalker" it[director] = "JJ Abrams" } -

If none of the optional arguments are provided to upsert(), the statements in the body block will be used for both the insert and update parts of the operation. This means that, for example, if a table mapping has columns with default values and these columns are omitted from the body block, the default values will be used for insertion as well as for the update operation. If the update operation should differ from the insert operation, then onUpdate should be provided an argument with the specific columns to update, as seen in the example below.

Using another example, PostgreSQL allows more control over which key constraint columns to check for conflict, whether different values should be used for an update, and whether the update statement should have a WHERE clause:

+

If none of the optional arguments are provided to upsert(), the statements in the body block will be used for both the insert and update parts of the operation. This means that, for example, if a table mapping has columns with default values and these columns are omitted from the body block, the default values will be used for insertion as well as for the update operation. If the update operation should differ from the insert operation, then onUpdate should be provided an argument with the specific columns to update, as seen in the example below.

Using another example, PostgreSQL allows more control over which key constraint columns to check for conflict, whether different values should be used for an update, and whether the update statement should have a WHERE clause:

val incrementSequelId = listOf(StarWarsFilms.sequelId to StarWarsFilms.sequelId.plus(1)) StarWarsFilms.upsert( StarWarsFilms.sequelId, @@ -375,7 +375,7 @@ it[name] = "The Rise of Skywalker" it[director] = "JJ Abrams" } -

If the update operation should be identical to the insert operation except for a few columns, then onUpdateExclude should be provided an argument with the specific columns to exclude. This parameter could also be used for the reverse case when only a small subset of columns should be updated but duplicating the insert values is tedious:

+

If the update operation should be identical to the insert operation except for a few columns, then onUpdateExclude should be provided an argument with the specific columns to exclude. This parameter could also be used for the reverse case when only a small subset of columns should be updated but duplicating the insert values is tedious:

// on conflict, all columns EXCEPT [director] are updated with values from the lambda block StarWarsFilms.upsert(onUpdateExclude = listOf(StarWarsFilms.director)) { it[sequelId] = 9 @@ -391,7 +391,7 @@ it[name] = "The Rise of Skywalker" it[director] = "JJ Abrams" } -

If a specific database supports user-defined key columns and none are provided, the table's primary key is used. If there is no defined primary key, the first unique index is used. If there are no unique indices, each database handles this case differently, so it is strongly advised that keys are defined to avoid unexpected results.

Replace

SQLite, MySQL, and MariaDB (as well as the H2 compatibility modes of the latter 2 databases) support a REPLACE statement that acts in a similar manner to an INSERT OR UPDATE statement. The only difference is that, if an insertion would violate a unique constraint, the existing row is deleted (not updated) before the new row is inserted.

+

If a specific database supports user-defined key columns and none are provided, the table's primary key is used. If there is no defined primary key, the first unique index is used. If there are no unique indices, each database handles this case differently, so it is strongly advised that keys are defined to avoid unexpected results.

Replace

SQLite, MySQL, and MariaDB (as well as the H2 compatibility modes of the latter 2 databases) support a REPLACE statement that acts in a similar manner to an INSERT OR UPDATE statement. The only difference is that, if an insertion would violate a unique constraint, the existing row is deleted (not updated) before the new row is inserted.

object StarWarsFilms : Table() { val sequelId: Column<Int> = integer("sequel_id").uniqueIndex() val releaseYear: Column<Int> = integer("release_year") @@ -420,4 +420,4 @@ it[rating] = 5.2 } } -

Unlike Insert or Update, none of the supporting databases allows a WHERE clause. Also, the constraints used to assess a violation are limited to the primary key and unique indexes, so there is no parameter for a custom key set.

The values specified in the statement block will be used for the insert statement, and any omitted columns are set to their default values, if applicable.

Last modified: 31 May 2024
\ No newline at end of file +

Unlike Insert or Update, none of the supporting databases allows a WHERE clause. Also, the constraints used to assess a violation are limited to the primary key and unique indexes, so there is no parameter for a custom key set.

The values specified in the statement block will be used for the insert statement, and any omitted columns are set to their default values, if applicable.

Last modified: 03 June 2024
diff --git a/docs/exposed-modules.html b/docs/exposed-modules.html index ce9cc83373..b262ad9f14 100644 --- a/docs/exposed-modules.html +++ b/docs/exposed-modules.html @@ -1,9 +1,9 @@ -Exposed Modules | Exposed

Exposed 0.51.0 Help

Exposed Modules

Dependencies

Exposed modules are available from Maven Central repository. To use them you have to add appropriate dependency into your repositories mapping.

+}

Exposed 0.51.1 Help

Exposed Modules

Dependencies

Exposed modules are available from Maven Central repository. To use them you have to add appropriate dependency into your repositories mapping.

repositories { mavenCentral() } -

The Maven Central repository is enabled by default for Maven users.

+

The Maven Central repository is enabled by default for Maven users.

repositories { mavenCentral() } -

Base Modules

Exposed 0.18.1 and higher

To move forward and support such features as Java 8 Time, async drivers, and so on, it was decided to split Exposed into more specific modules. It will allow you to take the only modules you need and will add flexibility in the future.

Exposed consists of the following modules:

  • exposed-core - base module, which contains both DSL api along with mapping

  • exposed-crypt - provides additional column types to store encrypted data in DB and encode/decode it on client-side

  • exposed-dao - DAO api

  • exposed-java-time - date-time extensions based on Java8 Time API

  • exposed-jdbc - transport level implementation based on Java JDBC API

  • exposed-jodatime - date-time extensions based on JodaTime library

  • exposed-json - JSON and JSONB data type extensions

  • exposed-kotlin-datetime - date-time extensions based on kotlinx-datetime

  • exposed-money - extensions to support MonetaryAmount from "javax.money:money-api"

  • exposed-spring-boot-starter - a starter for Spring Boot to utilize Exposed as the ORM instead of Hibernate

Dependencies mapping listed below is similar (by functionality) to the previous versions:

- val exposedVersion: String = "0.51.0" +

Base Modules

Exposed 0.18.1 and higher

To move forward and support such features as Java 8 Time, async drivers, and so on, it was decided to split Exposed into more specific modules. It will allow you to take the only modules you need and will add flexibility in the future.

Exposed consists of the following modules:

  • exposed-core - base module, which contains both DSL api along with mapping

  • exposed-crypt - provides additional column types to store encrypted data in DB and encode/decode it on client-side

  • exposed-dao - DAO api

  • exposed-java-time - date-time extensions based on Java8 Time API

  • exposed-jdbc - transport level implementation based on Java JDBC API

  • exposed-jodatime - date-time extensions based on JodaTime library

  • exposed-json - JSON and JSONB data type extensions

  • exposed-kotlin-datetime - date-time extensions based on kotlinx-datetime

  • exposed-money - extensions to support MonetaryAmount from "javax.money:money-api"

  • exposed-spring-boot-starter - a starter for Spring Boot to utilize Exposed as the ORM instead of Hibernate

Dependencies mapping listed below is similar (by functionality) to the previous versions:

+ val exposedVersion: String = "0.51.1" dependencies { implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") implementation("org.jetbrains.exposed:exposed-crypt:$exposedVersion") @@ -41,61 +41,61 @@ // optional spring-boot implementation("org.jetbrains.exposed:exposed-spring-boot-starter:$exposedVersion") } -
+
<dependencies> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-core</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-crypt</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-dao</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-java-time</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-jdbc</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-jodatime</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-json</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-kotlin-datetime</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-money</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-spring-boot-starter</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> </dependencies> -
- def exposedVersion = "0.51.0" +
+ def exposedVersion = "0.51.1" dependencies { implementation "org.jetbrains.exposed:exposed-core:$exposedVersion" implementation "org.jetbrains.exposed:exposed-crypt:$exposedVersion" @@ -114,7 +114,7 @@ // optional spring-boot implementation "org.jetbrains.exposed:exposed-spring-boot-starter:$exposedVersion" } -

JDBC driver and logging

You also need a JDBC driver for the database system you are using (see Databases) and a logger for addLogger(StdOutSqlLogger). Example (Gradle syntax):

+

JDBC driver and logging

You also need a JDBC driver for the database system you are using (see Databases) and a logger for addLogger(StdOutSqlLogger). Example (Gradle syntax):

dependencies { // for H2 implementation("com.h2database:h2:2.1.214") @@ -122,11 +122,11 @@ // http://www.slf4j.org/codes.html#StaticLoggerBinder implementation("org.slf4j:slf4j-nop:1.7.30") } -

Exposed 0.17.x and lower

Prior Exposed 0.18.1 there was only one base module exposed which contains everything you may need including JodaTime as date-time library. To add Exposed framework of that version, you had to use:

+

Exposed 0.17.x and lower

Prior Exposed 0.18.1 there was only one base module exposed which contains everything you may need including JodaTime as date-time library. To add Exposed framework of that version, you had to use:

dependencies { implementation("org.jetbrains.exposed:exposed:0.17.7") } -
+
<dependencies> <dependency> <groupId>org.jetbrains.exposed</groupId> @@ -134,8 +134,8 @@ <version>0.17.7</version> </dependency> </dependencies> -
+
dependencies { implementation("org.jetbrains.exposed:exposed:0.17.7") } -
Last modified: 31 May 2024
\ No newline at end of file +
Last modified: 03 June 2024
diff --git a/docs/frequently-asked-questions.html b/docs/frequently-asked-questions.html index 1c44ac7332..8d12421007 100644 --- a/docs/frequently-asked-questions.html +++ b/docs/frequently-asked-questions.html @@ -1,9 +1,9 @@ -Frequently Asked Questions | Exposed

Exposed 0.51.0 Help

Frequently Asked Questions

Q: Squash is same as Exposed. Where is the difference?

A: Ilya Ryzhenkov (Squash maintainer) answers:

Q: Can I use multiple Database Connections?

A: Yes. See Transactions

Q: Is Array column type supported?

A: Not at the moment. More info here: https://github.com/JetBrains/Exposed/issues/150
The complete list of supported data types can be found here: Data Types.

Q: Is upsert supported?

A: Yes. See Insert Or Update

Q: Is json type supported?

A: Yes. See JSON

Q: How to get a plain SQL query which will be executed?

A:

+}

Exposed 0.51.1 Help

Frequently Asked Questions

Q: Squash is same as Exposed. Where is the difference?

A: Ilya Ryzhenkov (Squash maintainer) answers:

Q: Can I use multiple Database Connections?

A: Yes. See Transactions

Q: Is Array column type supported?

A: Not at the moment. More info here: https://github.com/JetBrains/Exposed/issues/150
The complete list of supported data types can be found here: Data Types.

Q: Is upsert supported?

A: Yes. See Insert Or Update

Q: Is json type supported?

A: Yes. See JSON

Q: How to get a plain SQL query which will be executed?

A:

val plainSQL = FooTable.selectAll().where {}.prepareSQL(QueryBuilder(false)) -

Use QueryBuiler with false - if you want to inline statement arguments, true - to see '?' in query.

Q: Is it possible to use native sql / sql as a string?

A: It is not supported as part of the library, but it is possible to implement on top of it and use it like this:

+

Use QueryBuiler with false - if you want to inline statement arguments, true - to see '?' in query.

Q: Is it possible to use native sql / sql as a string?

A: It is not supported as part of the library, but it is possible to implement on top of it and use it like this:

fun <T:Any> String.execAndMap(transform : (ResultSet) -> T) : List<T> { val result = arrayListOf<T>() TransactionManager.current().exec(this) { rs -> @@ -29,15 +29,15 @@ "select u.name, c.name from user u inner join city c where blah blah".execAndMap { rs -> rs.getString("u.name") to rs.getString("c.name") } -

More info in this issue: https://github.com/JetBrains/Exposed/issues/118

Q: Is it possible to update a field relative to current field value?

A: Yes. See example here: https://github.com/JetBrains/Exposed/wiki/DSL#update

Q: How can I add another type of Database?

A: Implement DatabaseDialect interface and register it with Database.registerDialect().
If the implementation adds a lot of value consider contributing it as a PR to Exposed.

Q: Is it possible to create tables with cross / cyclic reference?

A: Yes, it's possible since Exposed 0.11.1 version

Q: How can I implement nested queries?

A: See example here: https://github.com/JetBrains/Exposed/issues/248

Q: How can I use SAVEPOINT?

A: It possible only through using a raw connection. See example here.

Q: How to prepare query like: SELECT * FROM table WHERE (x,y) IN ((1, 2), (3, 4), (5, 6))

A: It possible with custom function. See example.

Q: Where can I find snapshot builds of Exposed

A: You could use jitpack.io service for that.

Add jitpack.io to repositories:

+

More info in this issue: https://github.com/JetBrains/Exposed/issues/118

Q: Is it possible to update a field relative to current field value?

A: Yes. See example here: https://github.com/JetBrains/Exposed/wiki/DSL#update

Q: How can I add another type of Database?

A: Implement DatabaseDialect interface and register it with Database.registerDialect().
If the implementation adds a lot of value consider contributing it as a PR to Exposed.

Q: Is it possible to create tables with cross / cyclic reference?

A: Yes, it's possible since Exposed 0.11.1 version

Q: How can I implement nested queries?

A: See example here: https://github.com/JetBrains/Exposed/issues/248

Q: How can I use SAVEPOINT?

A: It possible only through using a raw connection. See example here.

Q: How to prepare query like: SELECT * FROM table WHERE (x,y) IN ((1, 2), (3, 4), (5, 6))

A: It possible with custom function. See example.

Q: Where can I find snapshot builds of Exposed

A: You could use jitpack.io service for that.

Add jitpack.io to repositories:

repositories { maven { url 'https://jitpack.io' } } -

Then add Exposed dependency as stated below:

+

Then add Exposed dependency as stated below:

dependencies { implementation 'com.github.JetBrains:Exposed:-SNAPSHOT' } -

Q: How can I specify a primary key column type e.g StringIdTable?

A: You need to define your own! See examples:
#855
https://stackoverflow.com/a/61940820/1155026

Q: How can I create a custom column type?

A: Just implements IColumnType and use registerColumn to extends a Table

eg: Create custom UUID types (inpired by @pjagielski article)

+

Q: How can I specify a primary key column type e.g StringIdTable?

A: You need to define your own! See examples:
#855
https://stackoverflow.com/a/61940820/1155026

Q: How can I create a custom column type?

A: Just implements IColumnType and use registerColumn to extends a Table

eg: Create custom UUID types (inpired by @pjagielski article)

abstract class TypedId(open val id: UUID): Serializable, Comparable<TypedId> { override fun compareTo(other: TypedId) = this.id.compareTo(other.id) } @@ -61,4 +61,4 @@ val director: Column<String> = varchar("director", 50) final override val primaryKey = PrimaryKey(id) } -

Reference: #149

More questions on Stack Overflow:

https://stackoverflow.com/questions/tagged/kotlin-exposed

Last modified: 31 May 2024
\ No newline at end of file +

Reference: #149

More questions on Stack Overflow:

https://stackoverflow.com/questions/tagged/kotlin-exposed

Last modified: 03 June 2024
diff --git a/docs/getting-started-with-exposed.html b/docs/getting-started-with-exposed.html index 3cc3ec88e9..c919713c65 100644 --- a/docs/getting-started-with-exposed.html +++ b/docs/getting-started-with-exposed.html @@ -1,9 +1,9 @@ -Getting Started with Exposed | Exposed

Exposed 0.51.0 Help

Getting Started with Exposed

Adding Dependencies

Before starting using Exposed, you need to add the dependencies to your project.

-val exposed_version = "0.51.0" +}

Exposed 0.51.1 Help

Getting Started with Exposed

Adding Dependencies

Before starting using Exposed, you need to add the dependencies to your project.

+val exposed_version = "0.51.1" dependencies { implementation("org.jetbrains.exposed", "exposed-core", exposed_version) implementation("org.jetbrains.exposed", "exposed-dao", exposed_version) implementation("org.jetbrains.exposed", "exposed-jdbc", exposed_version) } -
+
<dependencies> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-core</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-dao</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-jdbc</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> </dependencies> -

Getting Started

Starting a transaction

Every database access using Exposed is started by obtaining a connection and creating a transaction.

To get a connection:

+

Getting Started

Starting a transaction

Every database access using Exposed is started by obtaining a connection and creating a transaction.

To get a connection:

Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver") -

It is also possible to provide javax.sql.DataSource for advanced behaviors such as connection pooling:

+

It is also possible to provide javax.sql.DataSource for advanced behaviors such as connection pooling:

Database.connect(dataSource) -

More details on Database and DataSource.

After obtaining a connection, all SQL statements should be placed inside a transaction:

+

More details on Database and DataSource.

After obtaining a connection, all SQL statements should be placed inside a transaction:

transaction { // Statements here } -

To see the actual DB calls, add a logger:

+

To see the actual DB calls, add a logger:

transaction { // print sql to std-out addLogger(StdOutSqlLogger) } -

To log DB calls when using Exposed with a Spring Boot application, add the following property to the application.properties file:

+

To log DB calls when using Exposed with a Spring Boot application, add the following property to the application.properties file:

spring.exposed.show-sql=true -

The exposed-spring-boot-starter README covers the necessary steps to configure Exposed to use this and any other properties.

Alternatively, the SpringTransactionManager class has the parameter showSql, which can be set to true if a transaction manager bean is defined manually.

DSL & DAO

Exposed comes in two flavors: DSL (Domain-Specific Language) and DAO (Data Access Object).
On a high level, DSL means type-safe syntax that is similar to SQL, whereas DAO means doing CRUD operations on entities.
Observe the below examples and head on to the specific section of each API for more details.

Your first Exposed DSL

+

The exposed-spring-boot-starter README covers the necessary steps to configure Exposed to use this and any other properties.

Alternatively, the SpringTransactionManager class has the parameter showSql, which can be set to true if a transaction manager bean is defined manually.

DSL & DAO

Exposed comes in two flavors: DSL (Domain-Specific Language) and DAO (Data Access Object).
On a high level, DSL means type-safe syntax that is similar to SQL, whereas DAO means doing CRUD operations on entities.
Observe the below examples and head on to the specific section of each API for more details.

Your first Exposed DSL

fun main(args: Array<String>) { @@ -82,7 +82,7 @@ val name = varchar("name", 50) } -

More on DSL API

Your first Exposed DAO

+

More on DSL API

Your first Exposed DAO

fun main(args: Array<String>) { @@ -114,4 +114,4 @@ var name by Cities.name } -

More on DAO API

Last modified: 31 May 2024
\ No newline at end of file +

More on DAO API

Last modified: 03 June 2024
diff --git a/docs/home.html b/docs/home.html index 9f287a8236..ff33c9e74f 100644 --- a/docs/home.html +++ b/docs/home.html @@ -1,9 +1,9 @@ -Exposed Documentation | Exposed

Exposed 0.51.0 Help

Migration Guide

Migrating from 0.45.0 to 0.46.0

While Exposed provides migration support in the code itself (by using the @Deprecated annotation and ReplaceWith quickfix), this document serves as a reference point for the migration steps necessary to switch to the new query DSL.

SELECT Query DSL

Exposed's query DSL has been refactored to bring it closer to the syntax of a standard SQL SELECT statement.

The slice() function has been deprecated in favor of a new select() function that accepts the same variable amount of columns and creates a Query instance. If all columns should be selected, use selectAll() to create a Query instance.

The Query class now has the method where(), which can be chained to replace the old version of select { }.

Go to migration steps

Putting these changes together results in the following new DSL:

+}

Exposed 0.51.1 Help

Migration Guide

Migrating from 0.45.0 to 0.46.0

While Exposed provides migration support in the code itself (by using the @Deprecated annotation and ReplaceWith quickfix), this document serves as a reference point for the migration steps necessary to switch to the new query DSL.

SELECT Query DSL

Exposed's query DSL has been refactored to bring it closer to the syntax of a standard SQL SELECT statement.

The slice() function has been deprecated in favor of a new select() function that accepts the same variable amount of columns and creates a Query instance. If all columns should be selected, use selectAll() to create a Query instance.

The Query class now has the method where(), which can be chained to replace the old version of select { }.

Go to migration steps

Putting these changes together results in the following new DSL:

// Example 1 // before TestTable @@ -47,7 +47,7 @@ // Example 4 - no change TestTable.selectAll() -

To be consistent with these changes, the functions selectBatched() and selectAllBatched() have also been deprecated. A new Query method, fetchBatchedResults(), should be used instead as a terminal operation on an existing Query:

+

To be consistent with these changes, the functions selectBatched() and selectAllBatched() have also been deprecated. A new Query method, fetchBatchedResults(), should be used instead as a terminal operation on an existing Query:

// Example 1 // before TestTable @@ -69,7 +69,7 @@ TestTable .select(TestTable.columnA) .fetchBatchedResults(50) -

Lastly, adjustSlice() has been renamed to adjustSelect():

+

Lastly, adjustSlice() has been renamed to adjustSelect():

// before val originalQuery = TestTable.select { TestTable.columnA eq 1 } originalQuery.adjustSlice { slice(TestTable.columnA) } @@ -77,4 +77,4 @@ // after val originalQuery = TestTable.selectAll().where { TestTable.columnA eq 1 } originalQuery.adjustSelect { select(TestTable.columnA) } -

Migration Steps

  1. Use Edit > Find > Find in Files... to find any use of adjustSlice, then use the Alt+Enter quickfix with "Replace usages of '...' in whole project".

  2. Repeat step 1 with all the deprecated methods in the following list:

    • slice

    • Query.select: enter select\((\s*.+\s*)\)(\s*)\.select in the search bar (with the regex tab enabled) to find this method easily

    • select

    • selectBatched

    • selectAllBatched

  3. Use Edit > Find > Replace in Files... to resolve any redundant/incompatible uses of selectAll():

    • Enter select\((\s*.+\s*)\)(\s*)\.selectAll\(\) in the search bar (with the regex tab enabled)

    • Enter select\($1\) in the replace bar

    • Confirm the results and select "Replace All"

  4. Rebuild the project

Last modified: 31 May 2024
\ No newline at end of file +

Migration Steps

  1. Use Edit > Find > Find in Files... to find any use of adjustSlice, then use the Alt+Enter quickfix with "Replace usages of '...' in whole project".

  2. Repeat step 1 with all the deprecated methods in the following list:

    • slice

    • Query.select: enter select\((\s*.+\s*)\)(\s*)\.select in the search bar (with the regex tab enabled) to find this method easily

    • select

    • selectBatched

    • selectAllBatched

  3. Use Edit > Find > Replace in Files... to resolve any redundant/incompatible uses of selectAll():

    • Enter select\((\s*.+\s*)\)(\s*)\.selectAll\(\) in the search bar (with the regex tab enabled)

    • Enter select\($1\) in the replace bar

    • Confirm the results and select "Replace All"

  4. Rebuild the project

Last modified: 03 June 2024
diff --git a/docs/samples.html b/docs/samples.html index 1a37a87625..cd05c31a06 100644 --- a/docs/samples.html +++ b/docs/samples.html @@ -1,9 +1,9 @@ -Samples | Exposed

Exposed 0.51.0 Help

\ No newline at end of file +}

Exposed 0.51.1 Help

diff --git a/docs/sql-functions.html b/docs/sql-functions.html index b304c3523a..684721a578 100644 --- a/docs/sql-functions.html +++ b/docs/sql-functions.html @@ -1,9 +1,9 @@ -SQL Functions | Exposed

Exposed 0.51.0 Help

SQL Functions

Exposed provides basic support for classic SQL functions. This topic consists of definitions for those functions, and their usage examples. It also explains how to define custom functions.

How to use functions

If you want to retrieve a function result from a query, you have to declare the function as a variable:

+}

Exposed 0.51.1 Help

SQL Functions

Exposed provides basic support for classic SQL functions. This topic consists of definitions for those functions, and their usage examples. It also explains how to define custom functions.

How to use functions

If you want to retrieve a function result from a query, you have to declare the function as a variable:

val lowerCasedName = FooTable.name.lowerCase() val lowerCasedNames = FooTable.select(lowerCasedName).map { it[lowerCasedName] } -

Also, functions could be chained and combined:

+

Also, functions could be chained and combined:

val trimmedAndLoweredFullName = Concat(FooTable.firstName, stringLiteral(" "), FooTable.lastName).trim().lowerCase() val fullNames = FooTable.select(trimmedAndLoweredFullName).map { it[trimmedAndLoweredFullName] } -

String functions

LowerCase/UpperCase

Returns a lower-cased/upper-cased string value.

+

String functions

LowerCase/UpperCase

Returns a lower-cased/upper-cased string value.

val lowerCasedName = FooTable.name.lowerCase() val lowerCasedNames = FooTable.select(lowerCasedName).map { it[lowerCasedName] } -

Substring

Returns a substring value from the specified start and with the specified length.

+

Substring

Returns a substring value from the specified start and with the specified length.

val shortenedName = FooTable.name.substring(start = 1, length = 3) val shortenedNames = FooTable.select(shortenedName).map { it[shortenedName] } -

Concat

Returns a string value that concatenates the text representations of all non-null input values, separated by an optional separator.

+

Concat

Returns a string value that concatenates the text representations of all non-null input values, separated by an optional separator.

val userName = concat(stringLiteral("User - "), FooTable.name) val userNames = FooTable.select(userName).map { it[userName] } -

Locate

Returns the index of the first occurrence of a specified substring or 0.

+

Locate

Returns the index of the first occurrence of a specified substring or 0.

val firstAIndex = FooTable.name.locate("a") val firstAIndices = FooTable.select(firstAIndex).map { it[firstAIndex] } -

CharLength

Returns the length, measured in characters, or null if the String value is null.

+

CharLength

Returns the length, measured in characters, or null if the String value is null.

val nameLength = FooTable.name.charLength() val nameLengths = FooTable.select(nameLength).map { it[nameLength] } -

Aggregating functions

These functions should be used in queries with groupBy.

Min/Max/Average

Returns minimum/maximum/average value and can be applied to any comparable expression:

+

Aggregating functions

These functions should be used in queries with groupBy.

Min/Max/Average

Returns minimum/maximum/average value and can be applied to any comparable expression:

val minId = FooTable.id.min() val maxId = FooTable.id.max() val averageId = FooTable.id.avg() @@ -49,15 +49,15 @@ Triple(it[minId], it[maxId], it[averageId]) } -

Custom functions

If you can't find your most loved function used in your database (as Exposed provides only basic support for classic SQL functions), you can define your own functions.

Since Exposed 0.15.1 there multiple options to define custom functions:

  1. Function without parameters:

+

Custom functions

If you can't find your most loved function used in your database (as Exposed provides only basic support for classic SQL functions), you can define your own functions.

Since Exposed 0.15.1 there multiple options to define custom functions:

  1. Function without parameters:

val sqrt = FooTable.id.function("SQRT") -

In SQL representation it will be SQRT(FooTable.id)

  1. Function with additional parameters:

+

In SQL representation it will be SQRT(FooTable.id)

  1. Function with additional parameters:

val replacedName = CustomFunction<String?>("REPLACE", VarCharColumnType(), FooTable.name, stringParam("foo"), stringParam("bar")) -

CustomFunction class accepts a function name as a first parameter and the resulting column type as second. After that, you can provide any amount of parameters separated by a comma.

There are also shortcuts for string, long, and datetime functions:

  • CustomStringFunction

  • CustomLongFunction

  • CustomDateTimeFunction

The code above could be simplified to:

+

CustomFunction class accepts a function name as a first parameter and the resulting column type as second. After that, you can provide any amount of parameters separated by a comma.

There are also shortcuts for string, long, and datetime functions:

  • CustomStringFunction

  • CustomLongFunction

  • CustomDateTimeFunction

The code above could be simplified to:

val replacedName = CustomStringFunction("REPLACE", FooTable.name, stringParam("foo"), stringParam("bar")) -

For example, the following could be used in SQLite to mimic its date() function:

+

For example, the following could be used in SQLite to mimic its date() function:

val lastDayOfMonth = CustomDateFunction( "date", FooTable.dateColumn, @@ -65,7 +65,7 @@ stringLiteral("+1 month"), stringLiteral("-1 day") ) -
  1. Function that requires more complex query building:

All functions in Exposed extend the abstract class Function, which takes a column type and allows overriding toQueryBuilder(). This is what CustomFunction actually does, which can be leveraged to create more complex queries.

For example, Exposed provides a trim() function that removes leading and trailing whitespace from a String. In MySQL, this is just the default behavior as specifiers can be provided to limit the trim to either leading or trailing, as well as providing a specific substring other than spaces to remove. The custom function below supports this extended behavior:

+
  1. Function that requires more complex query building:

All functions in Exposed extend the abstract class Function, which takes a column type and allows overriding toQueryBuilder(). This is what CustomFunction actually does, which can be leveraged to create more complex queries.

For example, Exposed provides a trim() function that removes leading and trailing whitespace from a String. In MySQL, this is just the default behavior as specifiers can be provided to limit the trim to either leading or trailing, as well as providing a specific substring other than spaces to remove. The custom function below supports this extended behavior:

enum class TrimSpecifier { BOTH, LEADING, TRAILING } class CustomTrim<T : String?>( @@ -100,15 +100,15 @@ FooTable.select(trailingXTrim) // xxxbar } -

Window Functions

Window functions allow calculations across a set of table rows that are related to the current row.

Existing aggregate functions (like sum(), avg()) can be used, as well as new rank and value functions:

  • cumeDist()

  • denseRank()

  • firstValue()

  • lag()

  • lastValue()

  • lead()

  • nthValue()

  • nTile()

  • percentRank()

  • rank()

  • rowNumber()

To use a window function, include the OVER clause by chaining .over() after the function call. A PARTITION BY and ORDER BY clause can be optionally chained using .partitionBy() and .orderBy(), which both take multiple arguments:

+

Window Functions

Window functions allow calculations across a set of table rows that are related to the current row.

Existing aggregate functions (like sum(), avg()) can be used, as well as new rank and value functions:

  • cumeDist()

  • denseRank()

  • firstValue()

  • lag()

  • lastValue()

  • lead()

  • nthValue()

  • nTile()

  • percentRank()

  • rank()

  • rowNumber()

To use a window function, include the OVER clause by chaining .over() after the function call. A PARTITION BY and ORDER BY clause can be optionally chained using .partitionBy() and .orderBy(), which both take multiple arguments:

FooTable.amount.sum().over().partitionBy(FooTable.year, FooTable.product).orderBy(FooTable.amount) rowNumber().over().partitionBy(FooTable.year, FooTable.product).orderBy(FooTable.amount) FooTable.amount.sum().over().orderBy(FooTable.year to SortOrder.DESC, FooTable.product to SortOrder.ASC_NULLS_FIRST) -

Frame clause functions (like rows(), range(), and groups()) are also supported and take a WindowFrameBound option depending on the expected result:

  • WindowFrameBound.currentRow()

  • WindowFrameBound.unboundedPreceding()

  • WindowFrameBound.unboundedFollowing()

  • WindowFrameBound.offsetPreceding()

  • WindowFrameBound.offsetFollowing()

+

Frame clause functions (like rows(), range(), and groups()) are also supported and take a WindowFrameBound option depending on the expected result:

  • WindowFrameBound.currentRow()

  • WindowFrameBound.unboundedPreceding()

  • WindowFrameBound.unboundedFollowing()

  • WindowFrameBound.offsetPreceding()

  • WindowFrameBound.offsetFollowing()

FooTable.amount.sum().over() .partitionBy(FooTable.year, FooTable.product) .orderBy(FooTable.amount) .range(WindowFrameBound.offsetPreceding(2), WindowFrameBound.currentRow()) -
Last modified: 31 May 2024
\ No newline at end of file +
Last modified: 03 June 2024
diff --git a/docs/transactions.html b/docs/transactions.html index 9c07eb3852..e509975d7a 100644 --- a/docs/transactions.html +++ b/docs/transactions.html @@ -1,9 +1,9 @@ -Transactions | Exposed

Exposed 0.51.0 Help

Transactions

Overview

CRUD operations in Exposed must be called from within a transaction. Transactions encapsulate a set of DSL operations. To create and execute a transaction with default parameters, simply pass a function block to the transaction function:

+}

Exposed 0.51.1 Help

Transactions

Overview

CRUD operations in Exposed must be called from within a transaction. Transactions encapsulate a set of DSL operations. To create and execute a transaction with default parameters, simply pass a function block to the transaction function:

transaction { // DSL/DAO operations go here } -

Transactions are executed synchronously on the current thread, so they will block other parts of your application! If you need to execute a transaction asynchronously, consider running it on a separate Thread.

Accessing returned values

Although you can modify variables from your code within the transaction block, transaction supports returning a value directly, enabling immutability:

+

Transactions are executed synchronously on the current thread, so they will block other parts of your application! If you need to execute a transaction asynchronously, consider running it on a separate Thread.

Accessing returned values

Although you can modify variables from your code within the transaction block, transaction supports returning a value directly, enabling immutability:

val jamesList = transaction { Users.selectAll().where { Users.firstName eq "James" }.toList() } // jamesList is now a List<ResultRow> containing Users data -
+
// without eagerLoading val idsAndContent = transaction { Documents.selectAll().limit(10).map { it[Documents.id] to it[Documents.content] } @@ -37,7 +37,7 @@ val documentsWithContent = transaction { Documents.selectAll().limit(10) } -

Working with multiple databases

This functionality supported since 0.10.1 version

When you want to work with different databases then you have to store database reference returned by Database.connect() and provide it to transaction function as first parameter.

+

Working with multiple databases

This functionality supported since 0.10.1 version

When you want to work with different databases then you have to store database reference returned by Database.connect() and provide it to transaction function as first parameter.

val db1 = connect("jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;", "org.h2.Driver", "root", "") val db2 = connect("jdbc:h2:mem:db2;DB_CLOSE_DELAY=-1;", "org.h2.Driver", "root", "") transaction(db1) { @@ -48,10 +48,10 @@ val count = Table2.selectAll().where { Table2.name inList result }.count() } -

Entities (see DAO API page) stick to a transaction that was used to load that entity. That means that all changes persist to the same database and what cross-database references are prohibited and will throw exceptions.

Setting default database

transaction block without parameters will use the default database. As before 0.10.1 this will be the latest connected database. It is also possible to set the default database explicitly.

+

Entities (see DAO API page) stick to a transaction that was used to load that entity. That means that all changes persist to the same database and what cross-database references are prohibited and will throw exceptions.

Setting default database

transaction block without parameters will use the default database. As before 0.10.1 this will be the latest connected database. It is also possible to set the default database explicitly.

val db = Database.connect() TransactionManager.defaultDatabase = db -

Using nested transactions

By default, a nested transaction block shares the transaction resources of its parent transaction block, so any effect on the child affects the parent:

+

Using nested transactions

By default, a nested transaction block shares the transaction resources of its parent transaction block, so any effect on the child affects the parent:

val db = Database.connect() db.useNestedTransactions = false // set by default @@ -70,7 +70,7 @@ println(FooTable.selectAll().count()) // 0 } -

Since Exposed 0.16.1 it is possible to use nested transactions as separate transactions by setting useNestedTransactions = true on the desired Database instance.

After that any exception or rollback operation that happens within a transaction block will not roll back the whole transaction but only the code inside the current transaction. Exposed uses SQL SAVEPOINT functionality to mark the current transaction at the beginning of a transaction block and release it on exit.

Using savepoint could affect performance, so please read the documentation on your DBMS for more details.

+

Since Exposed 0.16.1 it is possible to use nested transactions as separate transactions by setting useNestedTransactions = true on the desired Database instance.

After that any exception or rollback operation that happens within a transaction block will not roll back the whole transaction but only the code inside the current transaction. Exposed uses SQL SAVEPOINT functionality to mark the current transaction at the beginning of a transaction block and release it on exit.

Using savepoint could affect performance, so please read the documentation on your DBMS for more details.

val db = Database.connect() db.useNestedTransactions = true @@ -89,7 +89,7 @@ println(FooTable.selectAll().count()) // 1 } -

Working with Coroutines

In the modern world, non-blocking and asynchronous code is popular. Kotlin has Coroutines, which provide an imperative way to write asynchronous code. Most Kotlin frameworks (like Ktor) have built-in support for coroutines, while Exposed is mostly blocking.

Why?

Because Exposed interacts with databases using JDBC API, which was designed in an era of blocking APIs. Additionally, Exposed stores some values in thread-local variables while coroutines could (and will) be executed in different threads.

Starting from Exposed 0.15.1, bridge functions are available that give you a safe way to interact with Exposed within suspend blocks: newSuspendedTransaction() and Transaction.withSuspendTransaction(). These have the same parameters as a blocking transaction() but allow you to provide a CoroutineContext argument that explicitly specifies the CoroutineDispatcher in which the function will be executed. If context is not provided your code will be executed within the current CoroutineContext.

Here is an example that uses these three types of transactions:

+

Working with Coroutines

In the modern world, non-blocking and asynchronous code is popular. Kotlin has Coroutines, which provide an imperative way to write asynchronous code. Most Kotlin frameworks (like Ktor) have built-in support for coroutines, while Exposed is mostly blocking.

Why?

Because Exposed interacts with databases using JDBC API, which was designed in an era of blocking APIs. Additionally, Exposed stores some values in thread-local variables while coroutines could (and will) be executed in different threads.

Starting from Exposed 0.15.1, bridge functions are available that give you a safe way to interact with Exposed within suspend blocks: newSuspendedTransaction() and Transaction.withSuspendTransaction(). These have the same parameters as a blocking transaction() but allow you to provide a CoroutineContext argument that explicitly specifies the CoroutineDispatcher in which the function will be executed. If context is not provided your code will be executed within the current CoroutineContext.

Here is an example that uses these three types of transactions:

transaction { println("Transaction # ${this.id}") // Transaction # 1 SchemaUtils.create(FooTable) // Table will be created on a current thread @@ -121,7 +121,7 @@ SchemaUtils.drop(Testing) } -

Please note that such code remains blocking (as it still uses JDBC) and you should not try to share a transaction between multiple threads as it may lead to undefined behavior.

If you desire to execute some code asynchronously and use the result later, take a look at suspendedTransactionAsync():

+

Please note that such code remains blocking (as it still uses JDBC) and you should not try to share a transaction between multiple threads as it may lead to undefined behavior.

If you desire to execute some code asynchronously and use the result later, take a look at suspendedTransactionAsync():

runBlocking { val launchResult = suspendedTransactionAsync(Dispatchers.IO) { FooTable.insert { it[id] = 2 } @@ -131,11 +131,11 @@ println("Async result: " + (launchResult.await() ?: -1)) // Async result: 2 } -

This function will accept the same parameters as newSuspendedTransaction() above but returns its future result as an implementation of Deferred, which you can await on to achieve your result.

Advanced parameters and usage

For specific functionality, transactions can be created with the additional parameters: transactionIsolation, readOnly, and db:

+

This function will accept the same parameters as newSuspendedTransaction() above but returns its future result as an implementation of Deferred, which you can await on to achieve your result.

Advanced parameters and usage

For specific functionality, transactions can be created with the additional parameters: transactionIsolation, readOnly, and db:

transaction (Connection.TRANSACTION_SERIALIZABLE, true, db = db) { // DSL/DAO operations go here } -

Transaction Isolation: This parameter, defined in the SQL standard, specifies what is supposed to happen when multiple transactions execute concurrently on the database. This value does NOT affect Exposed operation directly, but is sent to the database, where it is expected to be obeyed. Allowable values are defined in java.sql.Connection and are as follows:

  • TRANSACTION_NONE: Transactions are not supported.

  • TRANSACTION_READ_UNCOMMITTED: The most lenient setting. Allows uncommitted changes from one transaction to affect a read in another transaction (a "dirty read").

  • TRANSACTION_READ_COMMITTED: This setting prevents dirty reads from occurring, but still allows non-repeatable reads to occur. A non-repeatable read is when a transaction ("Transaction A") reads a row from the database, another transaction ("Transaction B") changes the row, and Transaction A reads the row again, resulting in an inconsistency.

  • TRANSACTION_REPEATABLE_READ: The default setting for Exposed transactions. Prevents both dirty and non-repeatable reads, but still allows for phantom reads. A phantom read is when a transaction ("Transaction A") selects a list of rows through a WHERE clause, another transaction ("Transaction B") performs an INSERT or DELETE with a row that satisfies Transaction A's WHERE clause, and Transaction A selects using the same WHERE clause again, resulting in an inconsistency.

  • TRANSACTION_SERIALIZABLE: The strictest setting. Prevents dirty reads, non-repeatable reads, and phantom reads.

readOnly: This parameter indicates whether any database connection used by the transaction is in read-only mode, and is set to false by default. Much like with transactionIsolation, this value is not directly used by Exposed, but is simply relayed to the database.

db: This parameter is optional and is used to select the database where the transaction should be settled (see the section above).

Transaction Repetition Attempts

Transactions also provide a property, repetitionAttempts, which sets the number of retries that should be made if an SQLException occurs inside the transaction block. If this property is not set, any default value provided in DatabaseConfig will be used instead:

+

Transaction Isolation: This parameter, defined in the SQL standard, specifies what is supposed to happen when multiple transactions execute concurrently on the database. This value does NOT affect Exposed operation directly, but is sent to the database, where it is expected to be obeyed. Allowable values are defined in java.sql.Connection and are as follows:

  • TRANSACTION_NONE: Transactions are not supported.

  • TRANSACTION_READ_UNCOMMITTED: The most lenient setting. Allows uncommitted changes from one transaction to affect a read in another transaction (a "dirty read").

  • TRANSACTION_READ_COMMITTED: This setting prevents dirty reads from occurring, but still allows non-repeatable reads to occur. A non-repeatable read is when a transaction ("Transaction A") reads a row from the database, another transaction ("Transaction B") changes the row, and Transaction A reads the row again, resulting in an inconsistency.

  • TRANSACTION_REPEATABLE_READ: The default setting for Exposed transactions. Prevents both dirty and non-repeatable reads, but still allows for phantom reads. A phantom read is when a transaction ("Transaction A") selects a list of rows through a WHERE clause, another transaction ("Transaction B") performs an INSERT or DELETE with a row that satisfies Transaction A's WHERE clause, and Transaction A selects using the same WHERE clause again, resulting in an inconsistency.

  • TRANSACTION_SERIALIZABLE: The strictest setting. Prevents dirty reads, non-repeatable reads, and phantom reads.

readOnly: This parameter indicates whether any database connection used by the transaction is in read-only mode, and is set to false by default. Much like with transactionIsolation, this value is not directly used by Exposed, but is simply relayed to the database.

db: This parameter is optional and is used to select the database where the transaction should be settled (see the section above).

Transaction Repetition Attempts

Transactions also provide a property, repetitionAttempts, which sets the number of retries that should be made if an SQLException occurs inside the transaction block. If this property is not set, any default value provided in DatabaseConfig will be used instead:

val db = Database.connect( datasource = datasource, databaseConfig = DatabaseConfig { @@ -148,7 +148,7 @@ repetitionAttempts = 25 // operation that may need multiple attempts } -

If this property is set to a value greater than 0, minRepetitionDelay and maxRepetitionDelay can also be set in the transaction block to indicate the minimum and maximum number of milliseconds to wait before retrying.

Transaction Query Timeout

Another advanced property available in a transaction block is queryTimeout. This sets the number of seconds to wait for each statement in the block to execute before timing out:

+

If this property is set to a value greater than 0, minRepetitionDelay and maxRepetitionDelay can also be set in the transaction block to indicate the minimum and maximum number of milliseconds to wait before retrying.

Transaction Query Timeout

Another advanced property available in a transaction block is queryTimeout. This sets the number of seconds to wait for each statement in the block to execute before timing out:

transaction { queryTimeout = 3 try { @@ -157,4 +157,4 @@ // logic to perform if execution timed out } } -

Note As is the case for transactionIsolation and readOnly properties, this value is not directly managed by Exposed, but is simply relayed to the JDBC driver. Some drivers may not support implementing this limit.

Statement Interceptors

DSL operations within a transaction create SQL statements, on which commands like Execute, Commit, and Rollback are issued. Exposed provides the StatementInterceptor interface that allows you to implement your own logic before and after these specific steps in a statement's lifecycle.

registerInterceptor() and unregisterInterceptor() can be used to enable and disable a custom interceptor in a single transaction.

To use a custom interceptor that acts on all transactions, extend the GlobalStatementInterceptor class instead. Exposed uses the Java SPI ServiceLoader to discover and load any implementations of this class. In this situation, a new file should be created in the resources folder named: META-INF/services/org.jetbrains.exposed.sql.statements.GlobalStatementInterceptor. The contents of this file should be the fully qualified class names of all custom implementations.

Last modified: 31 May 2024
\ No newline at end of file +

Note As is the case for transactionIsolation and readOnly properties, this value is not directly managed by Exposed, but is simply relayed to the JDBC driver. Some drivers may not support implementing this limit.

Statement Interceptors

DSL operations within a transaction create SQL statements, on which commands like Execute, Commit, and Rollback are issued. Exposed provides the StatementInterceptor interface that allows you to implement your own logic before and after these specific steps in a statement's lifecycle.

registerInterceptor() and unregisterInterceptor() can be used to enable and disable a custom interceptor in a single transaction.

To use a custom interceptor that acts on all transactions, extend the GlobalStatementInterceptor class instead. Exposed uses the Java SPI ServiceLoader to discover and load any implementations of this class. In this situation, a new file should be created in the resources folder named: META-INF/services/org.jetbrains.exposed.sql.statements.GlobalStatementInterceptor. The contents of this file should be the fully qualified class names of all custom implementations.

Last modified: 03 June 2024
diff --git a/documentation-website/Writerside/topics/Exposed-Modules.md b/documentation-website/Writerside/topics/Exposed-Modules.md index 316449a849..ee664c228a 100644 --- a/documentation-website/Writerside/topics/Exposed-Modules.md +++ b/documentation-website/Writerside/topics/Exposed-Modules.md @@ -53,7 +53,7 @@ Dependencies mapping listed below is similar (by functionality) to the previous - val exposedVersion: String = "0.51.0" + val exposedVersion: String = "0.51.1" dependencies { implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") implementation("org.jetbrains.exposed:exposed-crypt:$exposedVersion") @@ -80,59 +80,59 @@ Dependencies mapping listed below is similar (by functionality) to the previous <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-core</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-crypt</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-dao</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-java-time</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-jdbc</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-jodatime</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-json</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-kotlin-datetime</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-money</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> <dependency> <groupId>org.jetbrains.exposed</groupId> <artifactId>exposed-spring-boot-starter</artifactId> - <version>0.51.0</version> + <version>0.51.1</version> </dependency> </dependencies> - def exposedVersion = "0.51.0" + def exposedVersion = "0.51.1" dependencies { implementation "org.jetbrains.exposed:exposed-core:$exposedVersion" implementation "org.jetbrains.exposed:exposed-crypt:$exposedVersion" diff --git a/documentation-website/Writerside/topics/Getting-Started-with-Exposed.md b/documentation-website/Writerside/topics/Getting-Started-with-Exposed.md index 57f07f6215..4df1dc87a3 100644 --- a/documentation-website/Writerside/topics/Getting-Started-with-Exposed.md +++ b/documentation-website/Writerside/topics/Getting-Started-with-Exposed.md @@ -7,7 +7,7 @@ Before starting using Exposed, you need to add the dependencies to your project. -val exposed_version = "0.51.0" +val exposed_version = "0.51.1" dependencies { implementation("org.jetbrains.exposed", "exposed-core", exposed_version) implementation("org.jetbrains.exposed", "exposed-dao", exposed_version) @@ -22,17 +22,17 @@ dependencies { org.jetbrains.exposed exposed-core - 0.51.0 + 0.51.1 org.jetbrains.exposed exposed-dao - 0.51.0 + 0.51.1 org.jetbrains.exposed exposed-jdbc - 0.51.0 + 0.51.1 ]]> diff --git a/documentation-website/Writerside/writerside.cfg b/documentation-website/Writerside/writerside.cfg index dc8571be8b..5557f7d6f8 100644 --- a/documentation-website/Writerside/writerside.cfg +++ b/documentation-website/Writerside/writerside.cfg @@ -5,5 +5,5 @@ - + diff --git a/exposed-bom/README.md b/exposed-bom/README.md index 9cfe8e03fb..899ccb000c 100644 --- a/exposed-bom/README.md +++ b/exposed-bom/README.md @@ -8,7 +8,7 @@ Bill of Materials for all Exposed modules org.jetbrains.exposed exposed-bom - 0.51.0 + 0.51.1 pom import @@ -39,7 +39,7 @@ repositories { } dependencies { - implementation(platform("org.jetbrains.exposed:exposed-bom:0.51.0")) + implementation(platform("org.jetbrains.exposed:exposed-bom:0.51.1")) implementation("org.jetbrains.exposed", "exposed-core") implementation("org.jetbrains.exposed", "exposed-dao") implementation("org.jetbrains.exposed", "exposed-jdbc") diff --git a/exposed-spring-boot-starter/README.md b/exposed-spring-boot-starter/README.md index 5a11a5102d..deda4b5a9d 100644 --- a/exposed-spring-boot-starter/README.md +++ b/exposed-spring-boot-starter/README.md @@ -10,7 +10,7 @@ This starter will give you the latest version of [Exposed](https://github.com/Je org.jetbrains.exposed exposed-spring-boot-starter - 0.51.0 + 0.51.1 ``` @@ -20,7 +20,7 @@ repositories { mavenCentral() } dependencies { - implementation 'org.jetbrains.exposed:exposed-spring-boot-starter:0.51.0' + implementation 'org.jetbrains.exposed:exposed-spring-boot-starter:0.51.1' } ``` ### Gradle Kotlin DSL @@ -36,7 +36,7 @@ dependencies { ``` In `gradle.properties` ```properties -exposedVersion=0.51.0 +exposedVersion=0.51.1 ``` ## Setting up a database connection diff --git a/gradle.properties b/gradle.properties index f07231054a..5f9d0d045d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,4 +4,4 @@ org.gradle.configuration.cache=true org.gradle.caching=true group=org.jetbrains.exposed -version=0.51.0 +version=0.51.1 diff --git a/samples/exposed-ktor/gradle.properties b/samples/exposed-ktor/gradle.properties index 8c3aa370b5..ba2adc1c56 100644 --- a/samples/exposed-ktor/gradle.properties +++ b/samples/exposed-ktor/gradle.properties @@ -2,5 +2,5 @@ ktorVersion=2.3.4 kotlinVersion=1.8.10 logbackVersion=1.2.11 kotlin.code.style=official -exposedVersion=0.51.0 +exposedVersion=0.51.1 h2Version=2.1.214 diff --git a/samples/exposed-spring/gradle.properties b/samples/exposed-spring/gradle.properties index a9badb759b..fed2d0a6b9 100644 --- a/samples/exposed-spring/gradle.properties +++ b/samples/exposed-spring/gradle.properties @@ -1,2 +1,2 @@ -exposedVersion=0.51.0 +exposedVersion=0.51.1 kotlinVersion=1.8.21