{"id":17257557,"url":"https://github.com/hjohn/templatedjdbc","last_synced_at":"2025-04-14T06:10:56.387Z","repository":{"id":224119942,"uuid":"762471251","full_name":"hjohn/TemplatedJDBC","owner":"hjohn","description":"A zero dependency light-weight wrapper for JDBC, using String Templates and records.","archived":false,"fork":false,"pushed_at":"2024-08-17T22:16:12.000Z","size":241,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-27T19:54:29.253Z","etag":null,"topics":["java","jdbc"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hjohn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-02-23T21:10:33.000Z","updated_at":"2024-11-18T22:21:07.000Z","dependencies_parsed_at":"2024-03-01T00:32:01.541Z","dependency_job_id":"45f431b9-7bba-4b5e-aedd-65e338845aec","html_url":"https://github.com/hjohn/TemplatedJDBC","commit_stats":null,"previous_names":["hjohn/templatedjdbc"],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hjohn%2FTemplatedJDBC","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hjohn%2FTemplatedJDBC/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hjohn%2FTemplatedJDBC/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hjohn%2FTemplatedJDBC/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hjohn","download_url":"https://codeload.github.com/hjohn/TemplatedJDBC/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248830395,"owners_count":21168272,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["java","jdbc"],"created_at":"2024-10-15T07:17:50.568Z","updated_at":"2025-04-14T06:10:56.344Z","avatar_url":"https://github.com/hjohn.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.int4.db/parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.int4.db/parent)\n[![Build Status](https://github.com/hjohn/TemplatedJDBC/actions/workflows/maven.yml/badge.svg?branch=master)](https://github.com/hjohn/TemplatedJDBC/actions)\n[![Coverage](https://codecov.io/gh/hjohn/TemplatedJDBC/branch/master/graph/badge.svg?token=QCNNRFYF98)](https://codecov.io/gh/hjohn/TemplatedJDBC)\n[![License](https://img.shields.io/badge/License-BSD_2--Clause-orange.svg)](https://opensource.org/licenses/BSD-2-Clause)\n[![javadoc](https://javadoc.io/badge2/org.int4.db/parent/javadoc.svg)](https://javadoc.io/doc/org.int4.db/parent)\n\n# TemplatedJDBC\n\nA zero dependency light-weight wrapper for JDBC, using String Templates and records.\n\n# Introduction\n\nThis framework uses the String Templates preview feature. In order to use it you must use Java 21 or higher with preview features enabled (`--enable-preview`). This is still very much an experimental framework to see what a convenient syntax would be for a light-weight JDBC wrapper, and as such is subject to change.\n\nIt leans heavily on immutable `record`s, and is assuming there will be a future convenient syntax for reconstructing records with different values. Until that time, custom `with`-ers, or a framework like [Record Builder](https://github.com/Randgalt/record-builder) can be used to alleviate some of these discomforts.\n\nIt is extensible enough to allow using types that are not `record`s, although currently there is limited support to make this usage easy and convenient. A custom solution that generates the necessary meta data using reflection and annotations may be included at a later time; until that time it is possible to provide your own (or make a pull request).\n\n## Features\n\n- Zero dependencies\n- Use String Templates to execute SQL safe from injection attacks\n- Use `Row`s, records or custom types to read and write data\n- Lambda based transactions that can be retried on transitive failures\n\n## Maven\n\nAdd this dependency to your `pom.xml`.\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eorg.int4.db\u003c/groupId\u003e\n    \u003cartifactId\u003edb-core\u003c/artifactId\u003e\n    \u003cversion\u003e0.1.0\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\nYou will also need a JDBC driver for your database, and something that can provide `Connection`s on demand (you can roll your own `Supplier\u003cConnection\u003e` or, highly recommended, use a connection pool like [HikariCP](https://github.com/brettwooldridge/HikariCP)).\n\n### Activating Preview Features\n\nFor the compiler plugin, add:\n\n```xml\n\u003cconfiguration\u003e\n    \u003cenablePreview\u003etrue\u003c/enablePreview\u003e\n\u003c/configuration\u003e\n```\n\nFor the surefire and failsafe plugins add:\n\n```xml\n\u003cconfiguration\u003e\n    \u003cargLine\u003e--enable-preview\u003c/argLine\u003e\n\u003c/configuration\u003e\n```\n\n# Quick Example\n\n```java\nclass Example {\n    record Employee(Integer id, String name, double salary) {}\n\n    // Create a Reflector to hold Java to Database name mapping information:\n    static final Reflector ALL = Reflector.of(Employee.class);\n\n    final Database database;\n\n    // Obtain DataSource from HikariCP for example\n    Example(DataSource dataSource) {\n        // Database is reusable, and can be shared for a whole application\n        // so this can also be provided as a constructor parameter instead\n        // of creating it here:\n        this.database = DatabaseBuilder.using(dataSource::getConnection).build();\n    }\n\n    // Generates: \"INSERT INTO employee (id, name, salary) VALUES (?, ?, ?)\"\n    void insertExample() {\n        Employee employee = new Employee(1, \"Jane\", 1000.0);\n\n        db.accept(tx -\u003e\n            // Execute an insert:\n            tx.\"INSERT INTO employee (\\{ALL}) VALUES (\\{employee})\"\n                .execute()\n        );\n    }\n\n    // Generates: \"SELECT e.id, e.name, e.salary FROM employee e\"\n    List\u003cEmployee\u003e queryExample() {\n        return db.query(tx -\u003e \n            tx.\"SELECT e.\\{ALL} FROM employee e\"\n                .map(ALL)  // Convert to Employee records\n                .toList();  // Execute and return a List\n        );\n    }\n\n    // Generates: \"UPDATE employee SET id = ?, name = ?, salary = ? WHERE id = ?\"\n    long updateExample() {\n        return db.apply(tx -\u003e \n            tx.\"\"\"\n                UPDATE employee SET \\{ALL.entries(employee)}\n                    WHERE id = \\{employee.id()}\n            \"\"\".executeUpdate();  // Execute and return row count\n        );\n    }\n}\n```\nFor more examples see `DatabaseIT.java`.\n\n# The Basics\n\n## Creating a `Database` instance\n\nThe `Database` class is the starting point for all your SQL needs. To create one, all you need is a source of `Connection`s that it can use when needed. It is highly recommended to use a connection pool for this, and the below examples shows how to do this with HikariCP:\n\n```java\nHikariConfig config = new HikariConfig();\n\nconfig.setJdbcUrl(url);\nconfig.setUsername(user);\nconfig.setPassword(password);\n\nDataSource dataSource = new HikariDataSource(config);\n\nDatabase db = DatabaseBuilder.using(dataSource::getConnection).build();\n```\n\nIn further examples below, we'll assume that `db` holds an instance of `Database`.\n\n## Transactions\n\nTransactions can either be obtained directly by calling `Database::beginTransaction` or can be provided via a functional approach using `Database::apply` or `Database::query`. Transactions are associated with a thread, and starting a nested transaction will create a save point in the top level transaction to which a nested transaction may be rolled back.\n\n### Provided Transactions\n\nTransactions provided via a callback do not need to be closed. They will be automatically committed if the function completes normally. If the function completes exceptionally, the transaction is rolled back.\n\n```java\ndb.apply(tx -\u003e {\n    // Perform database modifications here, transaction\n    // commits if the function completes normally.\n});\n```\n\n### Manual Transactions\n\nManually created transactions should be closed. They are `AutoCloseable` making it possible to use them with a try with resources construct.\n\nTransactions when closed will by default perform a rollback, unless `Transaction::commit` is called.\n\n```java\ntry (Transaction tx = db.beginTransaction()) {\n    // query or modify database here\n\n    // when done commit (or do nothing for rollback)\n    tx.commit();\n}\n```\n\nIn further examples, `tx` will hold an active `Transaction`.\n\n## SQL execution\n\nSimple SQL statements which require no template parameters can be executed directly:\n\n```java\nList\u003cRow\u003e employees = tx.\"SELECT * FROM employee\".toList();\nList\u003cDouble\u003e salaries = tx.\"SELECT salary FROM employee\".asDouble().toList();\nint count = tx.\"SELECT COUNT(*) FROM employee\".map(r -\u003e r.getInteger(0)).get();\nlong affectedRows = tx.\"UPDATE employee SET salary = salary * 2\".execute();\n```\n\nThe template allows several customizations before the statement is executed. Depending on the statement type, the template can be executed immediately to get the number of affected rows, or it can be treated as a query in which case it will return `Row`s unless mapped to a more specific type. Only terminal operations will trigger the execution of the statement.\n\n|Method Signature|Operation Type|Description|\n|---|---|---|\n|`map(Function)`|Intermediate|Maps from the current type `T` to a new type `U`|\n|`mapGeneratedKeys`|Intermediate|Switches to the `Row`s that contain the generated keys|\n|`asInt`, `asLong`, ...|Intermediate|Assumes a single column, and maps the first column to the indicated type|\n|`execute`|Terminal Statement|Executes the statement and returns the number of affected rows|\n|`get`|Terminal Query|Executes the statement and returns the first and only result, or `null` if unavailable|\n|`getFirst`|Terminal Query|Executes the statement and returns the first result, or `null` if unavailable|\n|`getOptional`|Terminal Query|Executes the statement and returns the first and only result as an `Optional\u003cT\u003e`|\n|`toList`|Terminal Query|Executes the statement and returns the results as a (possibly empty) `List`|\n|`consume(Consumer)`|Terminal Query|Executes the statement, and calls the given consumer for each type `T`|\n|`consume(Consumer, long)`|Terminal Query|Executes the statement, and calls the given consumer for each type `T` up to a given maximum, returns `false` if all rows were processed|\n\n\u003e Note: `Row`s are thin wrapper around a JDBC `ResultSet` but use zero based column indices to fit in better.\n\n## Using template parameters\n```java\nString name = \"John\"\n\nint count = tx.\"SELECT COUNT(*) FROM employee WHERE name = \\{name}\".asInt().get();\n```\n\u003e Resulting SQL: `SELECT COUNT(*) FROM employee WHERE name = ?`\n\n# Leveraging `record`s\n\nHandling data using untyped `Row`s is cumbersome and error prone. Instead, `record`s can be used to handle groups of fields as a single unit.\n\nIn its most simple form, a `record` can be used as a list of values. Given:\n\n```java\nrecord Employee(Integer id, String name, double salary);\n```\nThen the record can be leveraged as:\n```java\nRecord employee = new Employee(5, \"Jane\", 1000.0);\n\ntx.\"INSERT INTO employee (id, name, salary) VALUES (\\{employee})\".execute();\n```\n\u003e SQL: `INSERT INTO employee (id, name, salary) VALUES (?, ?, ?)`\n\nThe above statement will take the required values directly form the record. Note that this is still error prone as the order, and number of the fields must match those of the record.\n\n## Introducing `Reflector`s, `Mapper`s and `Extractor`s\n\n|Type|Purpose|\n|---|---|\n|`Mapper`|Takes a `Row` from a database and converts it a Java object|\n|`Extractor`|Provides fields or fields + values to `INSERT`, `UPDATE` or `SELECT` statements|\n|`Reflector`|The combination of a `Mapper` and `Extractor`|\n\nA `Reflector` can be obtained by providing it with a suitable record definition. The record must use primitive types supported by the underlying database, but can include nested record types consisting of only supported types.\n\n`Reflector`s can also be created for arbitrary types, although this involves providing both the means to create such a type, and a way to extract values from it. At the moment there is only limited support for such types, and some manual work is needed. In the future it's possible that Reflection + Annotations may make this task a bit easier. For now it is assumed we're dealing with record types.\n\nBy default, the component names of a `record` are translated by converting a camel case name to an underscored name. For example, `creationDate` would become `creation_date`. The names can be overridden by calling additional methods on the `Reflector`.\n\n`Reflector`s and `Extractor`s can be used as template parameters. Given the following static definitions:\n\n```java\nstatic final Reflector\u003cEmployee\u003e ALL = Reflector.of(Employee.class);\nstatic final Extractor\u003cEmployee\u003e EXCEPT_ID = ALL.excluding(\"id\");\n```\nThe table below shows the generated SQL for all possible usages of these types:\n\n|Template Parameter|Templated as|SQL|\n|---|---|---|\n|`\\{ALL}`|Field List|`id, name, salary`|\n|`\\{EXCEPT_ID}`||`name, salary`|\n|`e.\\{EXCEPT_ID}` (with alias)||`e.name, e.salary`|\n|||\n|`\\{ALL.values(employee)}` or `\\{employee}`|Value List|`?, ?, ?` (id, name, salary)|\n|`\\{EXCEPT_ID.values(employee)}`||`?, ?` (name, salary)|\n|||\n|`\\{EXCEPT_ID.entries(employee)}`|Key/Value List|`name = ?, salary = ?`|\n\n\u003e Note: `record`s that are provided directly as template parameters must have their components defined in the exact same order as the supplied fields. No attempt is made to match fields by name.\n\n## Examples\n\n```java\n// Reading records\nList\u003cEmployee\u003e = tx.\"SELECT \\{ALL} FROM employee WHERE id \u003e 2\"\n    .map(ALL)  // The ALL Reflector is also a Mapper\n    .toList();\n```\n\u003e SQL: `SELECT id, name, salary FROM employee WHERE id \u003e 2`\n```java\n// Inserting records\nEmployee newEmployee = new Employee(null, \"John\", 42.42);\n\ntx.\"INSERT INTO employee (\\{EXCEPT_ID}) VALUES (\\{newEmployee})\".execute();\n```\n\u003e SQL: `INSERT INTO employee (name, salary) VALUES (?, ?)`\n```java\n// Updating records\nEmployee updatedEmployee = newEmployee.withName(\"Jane\");\n\ntx.\"UPDATE employee SET \\{EXCEPT_ID.entries(updatedEmployee)} WHERE id = 5\".execute();\n```\n\u003e SQL: `UPDATE employee SET name = ?, salary = ? WHERE id = 5`\n\n## Nested Records\n\n`Reflector`s support nested records amd cam flatten such records to a single set of fields. Given:\n\n```java\nrecord Company(Integer id, String name, Address address) {}\nrecord Address(String street, String city) {}\n```\nA `Reflector` can be created for `Company` which inlines the fields of `Address`:\n\n```java\nReflector\u003cCompany\u003e COMPANY = Reflector.of(Company.class)\n    .inline(\"address\", Reflector.of(Address.class));\n```\n\nThe fields will be: `id`, `name`, `address_street` and `address_city`\n\n```java\nCompany c = tx.\"SELECT \\{COMPANY} FROM company c JOIN address a WHERE a.id = c.id\"\n    .map(COMPANY)\n    .get();\n```\n\n## Obtaining and using generated ID's\n\nTables are often defined with id's generated by the database. In order to obtain these and (re)construct the resulting `record` with the correct id, use the following form for an `INSERT` statement:\n\n```java\nEmployee employee = new Employee(null, \"John\", 42.42);\n\nEmployee employeeWithId = tx.\"INSERT INTO employee (\\{ALL}) VALUES (\\{employee})\"\n    .mapGeneratedKeys()  // switch to the generated keys result set\n    .asInt()  // map first column to integers\n    .map(employee::withId)  // reconstruct the record using a custom wither\n    .get();  // executes the statement and returns the new record\n```\n\n# Advanced Stuff\n\nIn JDBC, normally only parameters can be templated, which is very limited. In this framework, the templates recognize several types that it can integrate safely into an SQL statement. This is done by strictly limiting what these types can provide.\n\n|Type|Use|\n|---|---|\n|Any `Record` type|As JDBC placeholders|\n|`Extractor`|As field names|\n|`Extractor::entries`|A combination of names and JDBC placeholders suitable for `UPDATE` statements|\n|`Extractor::values`|As JDBC placeholders, possibly filtered|\n|`Identifier`|As an SQL identifier|\n\n## `Identifier`s\n\nSometimes it is needed to be able to template more than just fields and values. A system that can create tables on demand, and query from them, may need to be able to specify its own table name. It is not possible to concatenate such information into a String Template, as templates do not allow concatenation for security reasons, nor does the template engine in this framework allow for inclusion of arbitrary `String`s.\n\nIn order to support this use case, one can convert a `String` into an `Identifier`. This involves a strict validation check that ensures the `String` can only be an SQL identifier, and thus would be safe to include. This looks like this:\n\n```java\ntx.\"CREATE TABLE \\{Identifier.of(\"my_table\")} ( ... )\".execute();\n```\n\n# Checked Exceptions, or not...\n\nYou choose. You can create either a normal `Database` type that will throw an unchecked `DatabaseException` which is a wrapper around an `SQLException`, or you can create a `CheckedDatabase` that will throw `SQLException`s.\n\n```java\nDatabase db = DatabaseBuilder.using(dataSource::getConnection).build();\n\nCheckedDatabase checkedDB = DatabaseBuilder.using(dataSource::getConnection).throwingSQLExceptions();\n```\n\n# Future\n\n- Batch support, by providing a `Collection` to appropriate statements\n- More investigating how to support aliasing of fields\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhjohn%2Ftemplatedjdbc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhjohn%2Ftemplatedjdbc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhjohn%2Ftemplatedjdbc/lists"}