diff --git a/pom.xml b/pom.xml index 17cca3d15482..bcd4b8b13a23 100644 --- a/pom.xml +++ b/pom.xml @@ -262,6 +262,7 @@ onion-architecture microservices-bulkhead microservices-load-shedding + table-data-gateway @@ -323,6 +324,12 @@ ${system-lambda.version} test + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + org.junit.jupiter junit-jupiter-engine diff --git a/table-data-gateway/README.md b/table-data-gateway/README.md new file mode 100644 index 000000000000..8b8938d3d5f9 --- /dev/null +++ b/table-data-gateway/README.md @@ -0,0 +1,54 @@ +--- +title: "Table Data Gateway Pattern in Java: Encapsulating Table Operations" +shortTitle: Table Data Gateway +description: "Learn how the Table Data Gateway pattern centralizes SQL operations for a database table behind a single gateway class." +category: Data access +language: en +tag: + - Data access + - Persistence + - Encapsulation +--- + +## Also known as + +* Table Gateway + +## Intent + +The Table Data Gateway pattern provides one object that acts as the gateway to all rows in a database table. It encapsulates SQL operations so callers can work with domain objects instead of issuing SQL directly. + +## Detailed Explanation + +CustomerTableGateway encapsulates create, read, update, and delete operations for the CUSTOMERS table. It accepts a DataSource, uses prepared statements, and maps query results to immutable Customer values. + +Unlike a Row Data Gateway, a Table Data Gateway instance handles operations across the table rather than representing one specific row. + +## Programmatic Example + +The example uses an in-memory H2 database: + +```java +var gateway = new CustomerTableGateway(dataSource); +gateway.insert(new Customer(1, "Alice", "Smith")); + +var customer = gateway.findById(1).orElseThrow(); +gateway.update(new Customer(customer.id(), customer.firstName(), "Jones")); +gateway.delete(customer.id()); +``` + +## Known Uses + +* Fowler's Patterns of Enterprise Application Architecture, Table Data Gateway: https://martinfowler.com/eaaCatalog/tableDataGateway.html + +## Consequences + +* SQL for a table is centralized in one class. +* Domain objects remain independent of database access code. +* The gateway can grow large when a table has many operations or complex query behavior. + +## Related Patterns + +* Row Data Gateway represents one row per gateway instance. +* Table Module combines table-wide data and domain logic in one class. +* Data Access Object provides an abstraction over a data access mechanism. diff --git a/table-data-gateway/etc/table-data-gateway.urm.puml b/table-data-gateway/etc/table-data-gateway.urm.puml new file mode 100644 index 000000000000..b0c58c90fee7 --- /dev/null +++ b/table-data-gateway/etc/table-data-gateway.urm.puml @@ -0,0 +1,27 @@ +@startuml +package com.iluwatar.tabledatagateway { + class App { + - DB_URL : String {static} + - LOGGER : Logger {static} + + main(args : String[]) {static} + } + class Customer <> { + + id : int + + firstName : String + + lastName : String + } + class CustomerTableGateway { + + CREATE_SCHEMA_SQL : String {static} + + DELETE_SCHEMA_SQL : String {static} + - dataSource : DataSource + + CustomerTableGateway(dataSource : DataSource) + + insert(customer : Customer) : int + + findById(id : int) : Optional + + findAll() : List + + update(customer : Customer) : int + + delete(id : int) : int + } +} +App --> CustomerTableGateway +CustomerTableGateway --> Customer +@enduml diff --git a/table-data-gateway/pom.xml b/table-data-gateway/pom.xml new file mode 100644 index 000000000000..ddeadfe362d4 --- /dev/null +++ b/table-data-gateway/pom.xml @@ -0,0 +1,84 @@ + + + + + java-design-patterns + com.iluwatar + 1.26.0-SNAPSHOT + + 4.0.0 + table-data-gateway + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + com.h2database + h2 + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.tabledatagateway.App + + + + + + + + + diff --git a/table-data-gateway/src/main/java/com/iluwatar/tabledatagateway/App.java b/table-data-gateway/src/main/java/com/iluwatar/tabledatagateway/App.java new file mode 100644 index 000000000000..b82774c0688d --- /dev/null +++ b/table-data-gateway/src/main/java/com/iluwatar/tabledatagateway/App.java @@ -0,0 +1,77 @@ +/* + * This project is licensed under the MIT license. Module table-data-gateway uses H2 database licensed under EPL (see epl-1.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.tabledatagateway; + +import java.sql.SQLException; +import javax.sql.DataSource; +import org.h2.jdbcx.JdbcDataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Table Data Gateway pattern demonstration. */ +public class App { + + private static final Logger LOGGER = LoggerFactory.getLogger(App.class); + + private static final String DB_URL = "jdbc:h2:mem:tabledatagateway;DB_CLOSE_DELAY=-1"; + + /** + * Program entry point. + * + * @param args command line args + * @throws SQLException if a database operation fails + */ + public static void main(final String[] args) throws SQLException { + var dataSource = createDataSource(); + execute(dataSource, CustomerTableGateway.CREATE_SCHEMA_SQL); + + var gateway = new CustomerTableGateway(dataSource); + gateway.insert(new Customer(1, "Alice", "Smith")); + + var found = gateway.findById(1); + LOGGER.info("Found customer: {}", found.orElse(null)); + + gateway.update(new Customer(1, "Alice", "Jones")); + LOGGER.info("Customers: {}", gateway.findAll()); + + gateway.delete(1); + LOGGER.info("Customers after delete: {}", gateway.findAll()); + + execute(dataSource, CustomerTableGateway.DELETE_SCHEMA_SQL); + } + + private static DataSource createDataSource() { + var dataSource = new JdbcDataSource(); + dataSource.setURL(DB_URL); + return dataSource; + } + + private static void execute(final DataSource dataSource, final String sql) throws SQLException { + try (var connection = dataSource.getConnection(); + var statement = connection.createStatement()) { + statement.execute(sql); + } + } +} diff --git a/table-data-gateway/src/main/java/com/iluwatar/tabledatagateway/Customer.java b/table-data-gateway/src/main/java/com/iluwatar/tabledatagateway/Customer.java new file mode 100644 index 000000000000..3bdf40a2f783 --- /dev/null +++ b/table-data-gateway/src/main/java/com/iluwatar/tabledatagateway/Customer.java @@ -0,0 +1,28 @@ +/* + * This project is licensed under the MIT license. Module table-data-gateway uses H2 database licensed under EPL (see epl-1.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.tabledatagateway; + +/** Immutable representation of a row in the CUSTOMERS table. */ +public record Customer(int id, String firstName, String lastName) {} diff --git a/table-data-gateway/src/main/java/com/iluwatar/tabledatagateway/CustomerTableGateway.java b/table-data-gateway/src/main/java/com/iluwatar/tabledatagateway/CustomerTableGateway.java new file mode 100644 index 000000000000..42930a0efda9 --- /dev/null +++ b/table-data-gateway/src/main/java/com/iluwatar/tabledatagateway/CustomerTableGateway.java @@ -0,0 +1,171 @@ +/* + * This project is licensed under the MIT license. Module table-data-gateway uses H2 database licensed under EPL (see epl-1.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.tabledatagateway; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import javax.sql.DataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Gateway that encapsulates all SQL operations for the CUSTOMERS table. + * + *

Unlike a Row Data Gateway, one instance of this class can operate on any row in the table. + */ +public final class CustomerTableGateway { + + /** SQL statement for creating the schema used by this example. */ + public static final String CREATE_SCHEMA_SQL = + "CREATE TABLE IF NOT EXISTS CUSTOMERS (ID INT PRIMARY KEY, FIRST_NAME VARCHAR(30) " + + "NOT NULL, LAST_NAME VARCHAR(30) NOT NULL)"; + + /** SQL statement for deleting the schema used by this example. */ + public static final String DELETE_SCHEMA_SQL = "DROP TABLE IF EXISTS CUSTOMERS"; + + private static final Logger LOGGER = LoggerFactory.getLogger(CustomerTableGateway.class); + + private final DataSource dataSource; + + /** + * Creates a gateway for the CUSTOMERS table. + * + * @param dataSource data source used by all gateway operations + */ + public CustomerTableGateway(final DataSource dataSource) { + this.dataSource = Objects.requireNonNull(dataSource); + } + + /** + * Inserts a customer row. + * + * @param customer customer data to insert + * @return number of rows inserted + * @throws SQLException if the database operation fails + */ + public int insert(final Customer customer) throws SQLException { + Objects.requireNonNull(customer); + var sql = "INSERT INTO CUSTOMERS (ID, FIRST_NAME, LAST_NAME) VALUES (?,?,?)"; + try (var connection = dataSource.getConnection(); + var preparedStatement = connection.prepareStatement(sql)) { + preparedStatement.setInt(1, customer.id()); + preparedStatement.setString(2, customer.firstName()); + preparedStatement.setString(3, customer.lastName()); + var inserted = preparedStatement.executeUpdate(); + LOGGER.info("Inserted customer with id {}", customer.id()); + return inserted; + } + } + + /** + * Finds a customer by id. + * + * @param id customer identifier + * @return the customer if a matching row exists, otherwise an empty optional + * @throws SQLException if the database operation fails + */ + public Optional findById(final int id) throws SQLException { + var sql = "SELECT ID, FIRST_NAME, LAST_NAME FROM CUSTOMERS WHERE ID=?"; + try (var connection = dataSource.getConnection(); + var preparedStatement = connection.prepareStatement(sql)) { + preparedStatement.setInt(1, id); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + if (resultSet.next()) { + return Optional.of(mapCustomer(resultSet)); + } + return Optional.empty(); + } + } + } + + /** + * Returns all customer rows ordered by id. + * + * @return all customers in the table + * @throws SQLException if the database operation fails + */ + public List findAll() throws SQLException { + var customers = new ArrayList(); + var sql = "SELECT ID, FIRST_NAME, LAST_NAME FROM CUSTOMERS ORDER BY ID"; + try (var connection = dataSource.getConnection(); + var statement = connection.createStatement(); + var resultSet = statement.executeQuery(sql)) { + while (resultSet.next()) { + customers.add(mapCustomer(resultSet)); + } + } + return customers; + } + + /** + * Updates a customer row. + * + * @param customer customer data with updated values + * @return number of rows updated, or zero if the row does not exist + * @throws SQLException if the database operation fails + */ + public int update(final Customer customer) throws SQLException { + Objects.requireNonNull(customer); + var sql = "UPDATE CUSTOMERS SET FIRST_NAME=?, LAST_NAME=? WHERE ID=?"; + try (var connection = dataSource.getConnection(); + var preparedStatement = connection.prepareStatement(sql)) { + preparedStatement.setString(1, customer.firstName()); + preparedStatement.setString(2, customer.lastName()); + preparedStatement.setInt(3, customer.id()); + var updated = preparedStatement.executeUpdate(); + LOGGER.info("Updated customer with id {}, affected rows: {}", customer.id(), updated); + return updated; + } + } + + /** + * Deletes a customer row. + * + * @param id customer identifier + * @return number of rows deleted, or zero if the row does not exist + * @throws SQLException if the database operation fails + */ + public int delete(final int id) throws SQLException { + var sql = "DELETE FROM CUSTOMERS WHERE ID=?"; + try (var connection = dataSource.getConnection(); + var preparedStatement = connection.prepareStatement(sql)) { + preparedStatement.setInt(1, id); + var deleted = preparedStatement.executeUpdate(); + LOGGER.info("Deleted customer with id {}, affected rows: {}", id, deleted); + return deleted; + } + } + + private static Customer mapCustomer(final ResultSet resultSet) throws SQLException { + return new Customer( + resultSet.getInt("ID"), + resultSet.getString("FIRST_NAME"), + resultSet.getString("LAST_NAME")); + } +} diff --git a/table-data-gateway/src/test/java/com/iluwatar/tabledatagateway/AppTest.java b/table-data-gateway/src/test/java/com/iluwatar/tabledatagateway/AppTest.java new file mode 100644 index 000000000000..617fe456c98b --- /dev/null +++ b/table-data-gateway/src/test/java/com/iluwatar/tabledatagateway/AppTest.java @@ -0,0 +1,42 @@ +/* + * This project is licensed under the MIT license. Module table-data-gateway uses H2 database licensed under EPL (see epl-1.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.tabledatagateway; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +class AppTest { + + @Test + void applicationShouldBeConstructible() { + assertDoesNotThrow(App::new); + } + + @Test + void mainShouldRunSuccessfully() { + assertDoesNotThrow(() -> App.main(new String[0])); + } +} diff --git a/table-data-gateway/src/test/java/com/iluwatar/tabledatagateway/CustomerTableGatewayTest.java b/table-data-gateway/src/test/java/com/iluwatar/tabledatagateway/CustomerTableGatewayTest.java new file mode 100644 index 000000000000..ee7d8f0c3251 --- /dev/null +++ b/table-data-gateway/src/test/java/com/iluwatar/tabledatagateway/CustomerTableGatewayTest.java @@ -0,0 +1,117 @@ +/* + * This project is licensed under the MIT license. Module table-data-gateway uses H2 database licensed under EPL (see epl-1.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.tabledatagateway; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.List; +import javax.sql.DataSource; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CustomerTableGatewayTest { + + private static final String DB_URL = "jdbc:h2:mem:tabledatagatewaytest;DB_CLOSE_DELAY=-1"; + + private DataSource dataSource; + private CustomerTableGateway gateway; + + @BeforeEach + void setUp() throws SQLException { + dataSource = createDataSource(); + execute(CustomerTableGateway.DELETE_SCHEMA_SQL); + execute(CustomerTableGateway.CREATE_SCHEMA_SQL); + gateway = new CustomerTableGateway(dataSource); + } + + @AfterEach + void tearDown() throws SQLException { + execute(CustomerTableGateway.DELETE_SCHEMA_SQL); + } + + private static DataSource createDataSource() { + var source = new JdbcDataSource(); + source.setURL(DB_URL); + return source; + } + + private static void execute(final String sql) throws SQLException { + try (var connection = DriverManager.getConnection(DB_URL); + var statement = connection.createStatement()) { + statement.execute(sql); + } + } + + @Test + void insertAndFindByIdShouldReturnPersistedRow() throws SQLException { + var customer = new Customer(1, "Alice", "Smith"); + + assertEquals(1, gateway.insert(customer)); + assertEquals(customer, gateway.findById(1).orElseThrow()); + } + + @Test + void findByIdShouldReturnEmptyWhenRowDoesNotExist() throws SQLException { + assertTrue(gateway.findById(99).isEmpty()); + } + + @Test + void findAllShouldReturnRowsInIdOrder() throws SQLException { + gateway.insert(new Customer(2, "Bob", "Jones")); + gateway.insert(new Customer(1, "Alice", "Smith")); + + assertEquals( + List.of(new Customer(1, "Alice", "Smith"), new Customer(2, "Bob", "Jones")), + gateway.findAll()); + } + + @Test + void findAllShouldReturnEmptyWhenTableHasNoRows() throws SQLException { + assertTrue(gateway.findAll().isEmpty()); + } + + @Test + void updateShouldPersistNewValuesAndReportMissingRows() throws SQLException { + gateway.insert(new Customer(1, "Alice", "Smith")); + + assertEquals(1, gateway.update(new Customer(1, "Alice", "Jones"))); + assertEquals("Jones", gateway.findById(1).orElseThrow().lastName()); + assertEquals(0, gateway.update(new Customer(99, "No", "Row"))); + } + + @Test + void deleteShouldRemoveRowAndReportMissingRows() throws SQLException { + gateway.insert(new Customer(1, "Alice", "Smith")); + + assertEquals(1, gateway.delete(1)); + assertEquals(0, gateway.delete(1)); + assertTrue(gateway.findById(1).isEmpty()); + } +}