diff --git a/active-record/README.md b/active-record/README.md new file mode 100644 index 000000000000..9f1bf27f8cad --- /dev/null +++ b/active-record/README.md @@ -0,0 +1,324 @@ +--- +title: "Active Record Pattern in Java: A straightforward coupling of object design to database design" +shortTitle: Active Record +description: "Learn how the Active Record design pattern in Java simplifies data access and abstraction by coupling of object design to database design. Ideal for Java developers seeking a quick solution to data management in smaller-scale applications." +category: Architectural +language: en +tag: + - Data access + - Decoupling + - Persistence +--- + + +## Intent of Active Record Design Pattern + +The Active Record design pattern encapsulates database access within an object that represents a row in a database table or view. + +This pattern simplifies data management by coupling object design directly to database design, making it ideal for smaller-scale applications. + + +## Detailed Explanation of Active Record Pattern with Real-World Examples + +Real-world example + +> Imagine managing an online store and having each product stored as a row inside a spreadsheet; unlike a typical spreadsheet, using the active record pattern not only lets you add information about the products on each row (such as pricing, quantity etc.), but also allows you to attach to each of these products capabilities over themselves, such as updating their quantity or their price and even properties over the whole spreadsheet, such as finding a different product by its ID. + +In plain words + +> The Active Record pattern enables each row to have certain capabilities over itself, not just store data. Active Record combines data and behavior, making it easier for developers to manage database records in an object-oriented way. + +Wikipedia says + +> In software engineering, the active record pattern is an architectural pattern. It is found in software that stores in-memory object data in relational databases. The interface of an object conforming to this pattern would include functions such as Insert, Update, and Delete, plus properties that correspond more or less directly to the columns in the underlying database table. + +## Programmatic Example of Active Record Pattern in Java + +Let's first look at the user entity that we need to persist. + + +```java + +public class User { + + private Integer id; + private String name; + private String email; + + /** + * User constructor. + * + * @param userId the unique identifier of the user + * @param userName the name of the user + * @param userEmail the email address of the user + */ + public User( + final Integer userId, + final String userName, + final String userEmail) { + this.id = userId; + this.name = userName; + this.email = userEmail; + } + + /** + * Getters and setters + */ + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(final String userName) { + this.name = userName; + } + + public String getEmail() { + return email; + } + public void setEmail(final String userEmail) { + this.email = userEmail; + } + + } +``` + +For convenience, we are storing the database configuration logic inside the same User class: + +```java + + // Credentials for in-memory H2 database. + + private static final String JDBC_URL = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"; + + + // Establish a database connection. + + private static Connection connect() throws SQLException { + return DriverManager.getConnection(JDBC_URL); + } + + // Initialize the table (required each time program runs + // as we are using an in-memory DB solution). + + public static void initializeTable() throws SQLException { + String sql = "CREATE TABLE IF NOT EXISTS users (\n" + + " id INTEGER PRIMARY KEY AUTO_INCREMENT,\n" + + " name VARCHAR(255),\n" + + " email VARCHAR(255)\n" + + ");"; + try (Connection conn = connect(); + Statement stmt = conn.createStatement()) { + stmt.execute(sql); + } + } +``` + +After configuring the database, our User class will contain methods thar mimic the typical CRUD operations performed on a database entry: + +```java + +/** + * Insert a new record into the database. + */ + +public void save() throws SQLException { + String sql; + if (this.id == null) { // New record + sql = "INSERT INTO users(name, email) VALUES(?, ?)"; + } else { // Update existing record + sql = "UPDATE users SET name = ?, email = ? WHERE id = ?"; + } + try (Connection conn = connect(); + PreparedStatement pstmt = conn.prepareStatement( + sql, Statement.RETURN_GENERATED_KEYS)) { + pstmt.setString(1, this.name); + pstmt.setString(2, this.email); + if (this.id != null) { + pstmt.setInt(3, this.id); + } + pstmt.executeUpdate(); + if (this.id == null) { + try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) { + if (generatedKeys.next()) { + this.id = generatedKeys.getInt(1); + } + } + } + } +} + +/** + * Find a user by ID. + */ + +public static Optional findById(final int id) { + String sql = "SELECT * FROM users WHERE id = ?"; + try (Connection conn = connect(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setInt(1, id); + ResultSet rs = pstmt.executeQuery(); + if (rs.next()) { + return Optional.of(new User( + rs.getInt("id"), + rs.getString("name"), + rs.getString("email"))); + } + } catch (SQLException e) { + LOGGER.error("SQL error: {}", e.getMessage(), e); + } + return Optional.empty(); +} +/** + * Get all users. + */ + +public static List findAll() throws SQLException { + String sql = "SELECT * FROM users"; + List users = new ArrayList<>(); + try (Connection conn = connect(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + while (rs.next()) { + users.add(new User( + rs.getInt("id"), + rs.getString("name"), + rs.getString("email"))); + } + } + return users; +} + +/** + * Delete the user from the database. + */ + +public void delete() throws SQLException { + if (this.id == null) { + return; + } + + String sql = "DELETE FROM users WHERE id = ?"; + try (Connection conn = connect(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setInt(1, this.id); + pstmt.executeUpdate(); + this.id = null; + } +} +``` + +Finally, here is the Active Record Pattern in action: + +```java +public static void main(final String[] args) { + try { + // Initialize the database and create the users table + User.initializeTable(); + LOGGER.info("Database and table initialized."); + + // Create a new user and save it to the database + User user1 = new User( + null, + "John Doe", + "john.doe@example.com"); + user1.save(); + LOGGER.info("New user saved: {} with ID {}", + user1.getName(), user1.getId()); + + // Retrieve and display the user by ID + Optional foundUser = User.findById(user1.getId()); + foundUser.ifPresentOrElse( + user -> LOGGER.info("User found: {} with email {}", + user.getName(), user.getEmail()), + () -> LOGGER.info("User not found.") + ); + + // Update the user’s details + Optional foundUserOpt = User.findById(user1.getId()); + foundUserOpt.ifPresent(user -> { + user.setName("John Updated"); + user.setEmail("john.updated@example.com"); + try { + user.save(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + LOGGER.info("User updated: {} with email {}", + user.getName(), user.getEmail()); + }); + + // Retrieve all users + List users = User.findAll(); + LOGGER.info("All users in the database:"); + for (User user : users) { + LOGGER.info("ID: {}, Name: {}, Email: {}", + user.getId(), user.getName(), user.getEmail()); + } + + // Delete the user + foundUserOpt.ifPresentOrElse(user -> { + try { + LOGGER.info("Deleting user with ID: {}", user.getId()); + user.delete(); + LOGGER.info("User successfully deleted!"); + } catch (Exception e) { + LOGGER.error("Error deleting user with ID: {}", user.getId(), e); + } + }, () -> LOGGER.info("User not found to delete.")); + + } catch (SQLException e) { + LOGGER.error("SQL error: {}", e.getMessage(), e); + } +} +``` + +The program outputs: + +``` +21:32:55.119 [main] INFO com.iluwatar.activerecord.App -- Database and table initialized. +21:32:55.128 [main] INFO com.iluwatar.activerecord.App -- New user saved: John Doe with ID 1 +21:32:55.141 [main] INFO com.iluwatar.activerecord.App -- User found: John Doe with email john.doe@example.com +21:32:55.145 [main] INFO com.iluwatar.activerecord.App -- User updated: John Updated with email john.updated@example.com +21:32:55.145 [main] INFO com.iluwatar.activerecord.App -- All users in the database: +21:32:55.145 [main] INFO com.iluwatar.activerecord.App -- ID: 1, Name: John Updated, Email: john.updated@example.com +21:32:55.146 [main] INFO com.iluwatar.activerecord.App -- Deleting user with ID: 1 +21:32:55.147 [main] INFO com.iluwatar.activerecord.App -- User successfully deleted! +``` + +## When to Use the Active Record Pattern in Java + +Use the Active Record pattern in Java when + +* You need to simplify database interactions in an object-oriented way +* You want to reduce boilerplate code for basic database operations +* The database schema is relatively simple and relationships between tables are simple (like one-to-many or many-to-one relationships) +* Your app needs to fetch, manipulate, and save records frequently in a way that matches closely with the application's main logic + +## Active Record Pattern Java Tutorials + +* [A Beginner's Guide to Active Record](https://dev.to/jjpark987/a-beginners-guide-to-active-record-pnf) +* [Overview of the Active Record Pattern](https://blog.savetchuk.com/overview-of-the-active-record-pattern) + +## Benefits and Trade-offs of Active Record Pattern + +The active record pattern can a feasible choice for smaller-scale applications involving CRUD operations or prototyping quick database solutions. It is also a good pattern to transition to when dealing with the Transaction Script pattern. + +On the other hand, it can bring about drawbacks regarding the risk of tight coupling, the lack of separation of concerns and performance constraints if working with large amounts of data, cases in which the Data Mapper pattern may be a more reliable option. + +## Related Java Design Patterns + +* [Data Mapper](https://java-design-patterns.com/patterns/data-mapper/): Data Mapper pattern separates database logic entirely from business entities, promoting loose coupling. +* [Transaction Script](https://martinfowler.com/eaaCatalog/transactionScript.html/): Transaction Script focuses on procedural logic, organizing each transaction as a distinct script to handle business operations directly without embedding them in objects. + + +## References and Credits + +* [Design Patterns: Elements of Reusable Object-Oriented Software](https://amzn.to/3w0pvKI) +* [Effective Java](https://amzn.to/4cGk2Jz) +* [Head First Design Patterns: Building Extensible and Maintainable Object-Oriented Software](https://amzn.to/49NGldq) +* [J2EE Design Patterns](https://amzn.to/4dpzgmx) +* [Refactoring to Patterns](https://amzn.to/3VOO4F5) diff --git a/active-record/etc/active-record.urm.png b/active-record/etc/active-record.urm.png new file mode 100644 index 000000000000..db30fb5d5883 Binary files /dev/null and b/active-record/etc/active-record.urm.png differ diff --git a/active-record/etc/active-record.urm.puml b/active-record/etc/active-record.urm.puml new file mode 100644 index 000000000000..bbff898fa1b6 --- /dev/null +++ b/active-record/etc/active-record.urm.puml @@ -0,0 +1,27 @@ +@startuml +package com.iluwatar.activerecord { + class App { + - LOGGER : Logger {static} + - App() + + main(args : String[]) {static} + } + class User { + - DB_URL : String {static} + - email : String + - id : Integer + - name : String + + User(id : Integer, name : String, email : String) + - connect() : Connection {static} + + delete() + + findAll() : List {static} + + findById(id : int) : User {static} + + getEmail() : String + + getId() : Integer + + getName() : String + + initializeTable() {static} + + save() + + setEmail(email : String) + + setName(name : String) + } +} +@enduml \ No newline at end of file diff --git a/active-record/pom.xml b/active-record/pom.xml new file mode 100644 index 000000000000..fe66c28e3087 --- /dev/null +++ b/active-record/pom.xml @@ -0,0 +1,89 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + active-record + active-record + active-record + + + + + org.springframework.boot + spring-boot-dependencies + pom + 3.2.4 + import + + + + + + + org.projectlombok + lombok + true + + + com.h2database + h2 + 2.1.214 + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.activerecord.App + + + + + + + + + diff --git a/active-record/src/main/java/com/iluwatar/activerecord/App.java b/active-record/src/main/java/com/iluwatar/activerecord/App.java new file mode 100644 index 000000000000..7d4cc89c9ab0 --- /dev/null +++ b/active-record/src/main/java/com/iluwatar/activerecord/App.java @@ -0,0 +1,118 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel + * is using ZK framework licensed under LGPL (see lgpl-3.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.activerecord; +import java.sql.SQLException; +import java.util.List; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; + +/** + * The Active Record pattern is an architectural pattern that simplifies + * database interactions by encapsulating database access logic within + * the domain model. This pattern allows objects to be responsible for + * their own persistence, providing methods for CRUD operations directly + * within the model class. + * + *

In this example, we demonstrate the Active Record pattern + * by creating, updating, retrieving, and deleting user records in + * an H2 in-memory database. The User class contains methods to perform + * these operations, ensuring that the database interactions are + * straightforward and intuitive. + */ + +@Slf4j +public final class App { + + private App() { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Entry Point. + * + * @param args the command line arguments - not used + */ + public static void main(final String[] args) { + try { + // Initialize the database and create the users table + User.initializeTable(); + LOGGER.info("Database and table initialized."); + + // Create a new user and save it to the database + User user1 = new User( + null, + "John Doe", + "john.doe@example.com"); + user1.save(); + LOGGER.info("New user saved: {} with ID {}", + user1.getName(), user1.getId()); + + // Retrieve and display the user by ID + Optional foundUser = User.findById(user1.getId()); + foundUser.ifPresentOrElse( + user -> LOGGER.info("User found: {} with email {}", + user.getName(), user.getEmail()), + () -> LOGGER.info("User not found.") + ); + + // Update the user’s details + Optional foundUserOpt = User.findById(user1.getId()); + foundUserOpt.ifPresent(user -> { + user.setName("John Updated"); + user.setEmail("john.updated@example.com"); + try { + user.save(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + LOGGER.info("User updated: {} with email {}", + user.getName(), user.getEmail()); + }); + + // Retrieve all users + List users = User.findAll(); + LOGGER.info("All users in the database:"); + for (User user : users) { + LOGGER.info("ID: {}, Name: {}, Email: {}", + user.getId(), user.getName(), user.getEmail()); + } + + // Delete the user + foundUserOpt.ifPresentOrElse(user -> { + try { + LOGGER.info("Deleting user with ID: {}", user.getId()); + user.delete(); + LOGGER.info("User successfully deleted!"); + } catch (Exception e) { + LOGGER.error("Error deleting user with ID: {}", user.getId(), e); + } + }, () -> LOGGER.info("User not found to delete.")); + + } catch (SQLException e) { + LOGGER.error("SQL error: {}", e.getMessage(), e); + } + } +} diff --git a/active-record/src/main/java/com/iluwatar/activerecord/User.java b/active-record/src/main/java/com/iluwatar/activerecord/User.java new file mode 100644 index 000000000000..4f8b77837f9f --- /dev/null +++ b/active-record/src/main/java/com/iluwatar/activerecord/User.java @@ -0,0 +1,251 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel + * is using ZK framework licensed under LGPL (see lgpl-3.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.activerecord; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; + +/** + * Implementation of active record pattern. + */ + +@Slf4j +public class User { + + /** + * Credentials for in-memory H2 database. + */ + private static final String JDBC_URL = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"; + + /** + * User ID. + */ + private Integer id; + + /** + * User name. + */ + private String name; + + /** + * User email. + */ + private String email; + + /** + * User constructor. + * + * @param userId the unique identifier of the user + * @param userName the name of the user + * @param userEmail the email address of the user + */ + public User( + final Integer userId, + final String userName, + final String userEmail) { + this.id = userId; + this.name = userName; + this.email = userEmail; + } + + /** + * Establish a database connection. + * + * @return a {@link Connection} object to interact with the database + * @throws SQLException if a database access error occurs + */ + + private static Connection connect() throws SQLException { + return DriverManager.getConnection(JDBC_URL); + } + + + /** + * Initialize the table (required each time program runs + * as we are using an in-memory DB solution). + */ + + public static void initializeTable() throws SQLException { + String sql = "CREATE TABLE IF NOT EXISTS users (\n" + + " id INTEGER PRIMARY KEY AUTO_INCREMENT,\n" + + " name VARCHAR(255),\n" + + " email VARCHAR(255)\n" + + ");"; + try (Connection conn = connect(); + Statement stmt = conn.createStatement()) { + stmt.execute(sql); + } + } + + /** + * Insert a new record into the database. + */ + + public void save() throws SQLException { + String sql; + if (this.id == null) { // New record + sql = "INSERT INTO users(name, email) VALUES(?, ?)"; + } else { // Update existing record + sql = "UPDATE users SET name = ?, email = ? WHERE id = ?"; + } + try (Connection conn = connect(); + PreparedStatement pstmt = conn.prepareStatement( + sql, Statement.RETURN_GENERATED_KEYS)) { + pstmt.setString(1, this.name); + pstmt.setString(2, this.email); + if (this.id != null) { + pstmt.setInt(3, this.id); + } + pstmt.executeUpdate(); + if (this.id == null) { + try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) { + if (generatedKeys.next()) { + this.id = generatedKeys.getInt(1); + } + } + } + } + } + + /** + * Find a user by ID. + * + * @param id userID + * @return the found user if a user is found, or an empty {@link Optional} + * if no user is found or an exception occurs + */ + + public static Optional findById(final int id) { + String sql = "SELECT * FROM users WHERE id = ?"; + try (Connection conn = connect(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setInt(1, id); + ResultSet rs = pstmt.executeQuery(); + if (rs.next()) { + return Optional.of(new User( + rs.getInt("id"), + rs.getString("name"), + rs.getString("email"))); + } + } catch (SQLException e) { + LOGGER.error("SQL error: {}", e.getMessage(), e); + } + return Optional.empty(); + } + + /** + * Get all users. + * + * @return all users from the database; + */ + + public static List findAll() throws SQLException { + String sql = "SELECT * FROM users"; + List users = new ArrayList<>(); + try (Connection conn = connect(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + while (rs.next()) { + users.add(new User( + rs.getInt("id"), + rs.getString("name"), + rs.getString("email"))); + } + } + return users; + } + + /** + * Delete the user from the database. + */ + + public void delete() throws SQLException { + if (this.id == null) { + return; + } + + String sql = "DELETE FROM users WHERE id = ?"; + try (Connection conn = connect(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setInt(1, this.id); + pstmt.executeUpdate(); + this.id = null; + } + } + + /** + * Gets the ID of the user. + * + * @return the unique identifier of the user, + * or null if the user is not yet saved to the database + */ + public Integer getId() { + return id; + } + + /** + * Gets the name of the user. + * + * @return the name of the user + */ + public String getName() { + return name; + } + + /** + * Sets the name of the user. + * + * @param userName the name to set for the user + */ + public void setName(final String userName) { + this.name = userName; + } + + /** + * Gets the email address of the user. + * + * @return the email address of the user + */ + public String getEmail() { + return email; + } + + /** + * Sets the email address of the user. + * + * @param userEmail the email address to set for the user + */ + public void setEmail(final String userEmail) { + this.email = userEmail; + } +} diff --git a/active-record/src/main/java/com/iluwatar/activerecord/package-info.java b/active-record/src/main/java/com/iluwatar/activerecord/package-info.java new file mode 100644 index 000000000000..261fd418e8fe --- /dev/null +++ b/active-record/src/main/java/com/iluwatar/activerecord/package-info.java @@ -0,0 +1,58 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel + * is using ZK framework licensed under LGPL (see lgpl-3.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. + */ +/** + * Context and problem + * Most applications require a way to interact with a database to perform + * CRUD (Create, Read, Update, Delete) operations. + * Traditionally, this involves writing a lot of boilerplate code + * for database access, which can be error-prone and hard to maintain. + * This is especially true in applications with + * complex data models and relationships. + * + *

Often, developers need to write separate classes for database access and + * domain models, leading to a separation of concerns + * that can make the codebase more complex and harder to understand. + * Additionally, changes to the database schema can require significant + * changes to the data access code, increasing the maintenance burden. + * + *

Maintaining this separation can force the application to + * adhere to at least some of the database's APIs or other semantics. + * When these database features have quality issues, supporting them "corrupts" + * what might otherwise be a cleanly designed application. + * Similar issues can arise with any external system that your + * development team doesn't control, not just databases. + * + *

Solution Simplify database interactions by using + * the Active Record pattern. + * This pattern encapsulates database access logic within the + * domain model itself, allowing objects to be responsible + * for their own persistence. + * The Active Record pattern provides methods for + * CRUD operations directly within the model class, + * making database interactions straightforward and intuitive. + */ + +package com.iluwatar.activerecord; diff --git a/active-record/src/test/java/com/iluwatar/activerecord/AppTest.java b/active-record/src/test/java/com/iluwatar/activerecord/AppTest.java new file mode 100644 index 000000000000..2ab4a458936e --- /dev/null +++ b/active-record/src/test/java/com/iluwatar/activerecord/AppTest.java @@ -0,0 +1,44 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.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.activerecord; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +/** + * Application test + */ + +class AppTest { + + @Test + void assertExecuteApplicationWithoutException() { + + assertDoesNotThrow(() -> App.main(new String[]{})); + } +} + diff --git a/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java b/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java new file mode 100644 index 000000000000..369b54913c9d --- /dev/null +++ b/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java @@ -0,0 +1,121 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.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.activerecord; + +import org.junit.jupiter.api.*; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class UserTest { + + private static final String JDBC_URL = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"; + + @BeforeAll + static void setupDatabase() throws SQLException { + User.initializeTable(); + } + + @BeforeEach + void clearDatabase() throws SQLException { + // Clean up table before each test + try (Connection conn = DriverManager.getConnection(JDBC_URL); + Statement stmt = conn.createStatement()) { + stmt.execute("DELETE FROM users"); + } + } + + @Test + void testSaveNewUser() throws SQLException { + User user = new User(null, "Alice", "alice@example.com"); + user.save(); + assertNotNull(user.getId(), "User ID should be generated upon saving"); + } + + @Test + void testFindById() throws SQLException { + // Create and save a new user + User user = new User(null, "Bob", "bob@example.com"); + user.save(); + + Optional foundUserOpt = User.findById(user.getId()); + + assertTrue(foundUserOpt.isPresent(), "User should be found by ID"); + + foundUserOpt.ifPresent(foundUser -> { + assertEquals("Bob", foundUser.getName()); + assertEquals("bob@example.com", foundUser.getEmail()); + }); + } + + @Test + void testFindAll() throws SQLException { + User user1 = new User(null, "Charlie", "charlie@example.com"); + User user2 = new User(null, "Diana", "diana@example.com"); + user1.save(); + user2.save(); + + List users = User.findAll(); + assertEquals(2, users.size(), "There should be two users in the database"); + assertTrue(users.stream().anyMatch(u -> "Charlie".equals(u.getName())), "User Charlie should exist"); + assertTrue(users.stream().anyMatch(u -> "Diana".equals(u.getName())), "User Diana should exist"); + } + + @Test + void testUpdateUser() throws SQLException { + User user = new User(null, "Eve", "eve@example.com"); + user.save(); + + user.setName("Eve Updated"); + user.setEmail("eve.updated@example.com"); + user.save(); + + Optional updatedUserOpt = User.findById(user.getId()); + + assertTrue(updatedUserOpt.isPresent(), "Updated user should be found by ID"); + + updatedUserOpt.ifPresent(updatedUser -> { + assertEquals("Eve Updated", updatedUser.getName()); + assertEquals("eve.updated@example.com", updatedUser.getEmail()); + }); + } + + @Test + void testDeleteUser() throws SQLException { + User user = new User(null, "Frank", "frank@example.com"); + user.save(); + Integer userId = user.getId(); + + user.delete(); + + Optional deletedUserOpt = User.findById(userId); + assertTrue(deletedUserOpt.isEmpty(), "User should be deleted from the database"); + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 26b576ca0e8b..98653a613c57 100644 --- a/pom.xml +++ b/pom.xml @@ -217,6 +217,7 @@ virtual-proxy function-composition microservices-distributed-tracing + active-record diff --git a/update-header.sh b/update-header.sh deleted file mode 100755 index 48da4dcd6125..000000000000 --- a/update-header.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -# Find all README.md files in subdirectories one level deep -# and replace "### " with "## " at the beginning of lines -find . -maxdepth 2 -type f -name "README.md" -exec sed -i '' 's/^### /## /' {} \; - -echo "Headers updated in README.md files."