From 26a31de27442194b8b47428a2bd8472a7c2937b5 Mon Sep 17 00:00:00 2001 From: ipaulaandreea Date: Tue, 5 Nov 2024 21:22:21 +0200 Subject: [PATCH] finished task #79 --- active-record/README.md | 277 ++++++++++++++++++ active-record/database.db | Bin 0 -> 12288 bytes active-record/etc/active-record.urm.png | Bin 0 -> 43167 bytes active-record/etc/active-record.urm.puml | 27 ++ active-record/pom.xml | 34 ++- .../java/com/iluwatar/activerecord/App.java | 38 +-- .../java/com/iluwatar/activerecord/User.java | 110 ++++++- .../iluwatar/activerecord/package-info.java | 5 +- .../com/iluwatar/activerecord/UserTest.java | 24 ++ database.db | Bin 0 -> 12288 bytes update-header.sh | 25 ++ 11 files changed, 495 insertions(+), 45 deletions(-) create mode 100644 active-record/README.md create mode 100644 active-record/database.db create mode 100644 active-record/etc/active-record.urm.png create mode 100644 active-record/etc/active-record.urm.puml create mode 100644 database.db diff --git a/active-record/README.md b/active-record/README.md new file mode 100644 index 000000000000..5fc2159e9e4e --- /dev/null +++ b/active-record/README.md @@ -0,0 +1,277 @@ +--- +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; + + public User(Integer id, String name, String email) { + this.id = id; + this.name = name; + this.email = email; + } + public Integer getId() { + return id; + } + public String getName() { + return name; + } + public String getEmail() { + return email; + } + + public void setName(String name) { + this.name = name; + } + + public void setEmail(String email) { + this.email = email; + } +} +``` + +For convenience, we are storing the database configuration logic inside the same User class: + +```java + + private static final String DB_URL = "jdbc:sqlite:database.db"; + + // Establish a database connection. + + private static Connection connect() throws SQLException { + return DriverManager.getConnection(DB_URL); + } + + // Initialize the table (if not exists). + + public static void initializeTable() throws SQLException { + String sql = "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)"; + 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 User findById(int id) throws SQLException { + 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 new User(rs.getInt("id"), rs.getString("name"), rs.getString("email")); + } + } + return null; +} +/** + * 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 if it doesn't exist + 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 + User foundUser = User.findById(user1.getId()); + if (foundUser != null) { + LOGGER.info("User found: {} with email {}", foundUser.getName(), foundUser.getEmail()); + } else { + LOGGER.info("User not found."); + } + + // Update the user’s details + assert foundUser != null; + foundUser.setName("John Updated"); + foundUser.setEmail("john.updated@example.com"); + foundUser.save(); + LOGGER.info("User updated: {} with email {}", foundUser.getName(), foundUser.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 + try { + LOGGER.info("Deleting user with ID: {}", foundUser.getId()); + foundUser.delete(); + LOGGER.info("User successfully deleted!"); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + } + + } catch (SQLException e) { + LOGGER.error("SQL error: {}", e.getMessage(), e); + } +} +``` + +The program outputs: + +``` +19:34:53.731 [main] INFO com.iluwatar.activerecord.App -- Database and table initialized. +19:34:53.755 [main] INFO com.iluwatar.activerecord.App -- New user saved: John Doe with ID 1 +19:34:53.759 [main] INFO com.iluwatar.activerecord.App -- User found: John Doe with email john.doe@example.com +19:34:53.762 [main] INFO com.iluwatar.activerecord.App -- User updated: John Updated with email john.updated@example.com +19:34:53.764 [main] INFO com.iluwatar.activerecord.App -- All users in the database: +19:34:53.764 [main] INFO com.iluwatar.activerecord.App -- ID: 1, Name: John Updated, Email: john.updated@example.com +19:34:53.764 [main] INFO com.iluwatar.activerecord.App -- Deleting user with ID: 1 +19:34:53.768 [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/database.db b/active-record/database.db new file mode 100644 index 0000000000000000000000000000000000000000..831ad85866ffb8328086c549c30d1262add5a4d1 GIT binary patch literal 12288 zcmeI&&r8EF6bJBR8;S?12c=`D2ZC&D&V%gaWmSU=w>oPF_9StOQfT+9orpL83jZPh zl>H04n^bmi7tf>LKwe*(ywHAbfjr!I!&K4BWFE_u&e;LuoVAG4^sIYsbm)E z6ZKP~W(zeQZVroAo^X6YzH`|Xw6V52ma%H+%GxX6wQZY!o@QGv5;ae+~Eipj#1;Gw|;^t}f^a*Kd&7n|$%8Qx(fF%JP~yn{#W)tflq6t^D|I z2tWV=5P$##AOHafKmY;|fIuDuDu*RzpIwJ?EJr4^)w?`7DzW1W@up}nt(48Y6NN)1 zO)&HEG*ZpsB>p}Bub8#UgB1P|0uX=z1Rwwb2tWV=5P$##Adn-0JzlhVc6p%u1AoU| AL;wH) literal 0 HcmV?d00001 diff --git a/active-record/etc/active-record.urm.png b/active-record/etc/active-record.urm.png new file mode 100644 index 0000000000000000000000000000000000000000..db30fb5d588310c8c900bd10d1d663dba00d89c4 GIT binary patch literal 43167 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 {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 index 7b2b0aa2b330..5efadafc693e 100644 --- a/active-record/pom.xml +++ b/active-record/pom.xml @@ -1,4 +1,30 @@ + @@ -8,10 +34,14 @@ java-design-patterns 1.26.0-SNAPSHOT - active-record - + + org.projectlombok + lombok + + 1.18.30 + org.junit.jupiter junit-jupiter-engine diff --git a/active-record/src/main/java/com/iluwatar/activerecord/App.java b/active-record/src/main/java/com/iluwatar/activerecord/App.java index 2e393bedca60..35f49a974925 100644 --- a/active-record/src/main/java/com/iluwatar/activerecord/App.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/App.java @@ -1,7 +1,5 @@ /* - * This project is licensed under the MIT license. - * Module model-view-viewmodel is using ZK framework - * licensed under LGPL (see lgpl-3.0.txt). + * 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ä @@ -24,11 +22,10 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ - package com.iluwatar.activerecord; -import lombok.extern.slf4j.Slf4j; import java.sql.SQLException; import java.util.List; +import lombok.extern.slf4j.Slf4j; /** * The Active Record pattern is an architectural pattern that simplifies @@ -56,27 +53,23 @@ private App() { * * @param args the command line arguments - not used */ - - public static void main(final String[] args) { try { // Initialize the database and create the users table if it doesn't exist User.initializeTable(); - System.out.println("Database and table initialized."); + 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(); - System.out.println("New user saved: " + user1.getName() - + " with ID " + user1.getId()); + LOGGER.info("New user saved: {} with ID {}", user1.getName(), user1.getId()); // Retrieve and display the user by ID User foundUser = User.findById(user1.getId()); if (foundUser != null) { - System.out.println("User found: " + foundUser.getName() - + " with email " + foundUser.getEmail()); + LOGGER.info("User found: {} with email {}", foundUser.getName(), foundUser.getEmail()); } else { - System.out.println("User not found."); + LOGGER.info("User not found."); } // Update the user’s details @@ -84,29 +77,26 @@ public static void main(final String[] args) { foundUser.setName("John Updated"); foundUser.setEmail("john.updated@example.com"); foundUser.save(); - System.out.println("User updated: " + foundUser.getName() - + " with email " + foundUser.getEmail()); + LOGGER.info("User updated: {} with email {}", foundUser.getName(), foundUser.getEmail()); // Retrieve all users List users = User.findAll(); - System.out.println("All users in the database:"); + LOGGER.info("All users in the database:"); for (User user : users) { - System.out.println("ID: " + user.getId() - + ", Name: " + user.getName() - + ", Email: " + user.getEmail()); + LOGGER.info("ID: {}, Name: {}, Email: {}", user.getId(), user.getName(), user.getEmail()); } // Delete the user try { - System.out.println("Deleting user with ID: " + foundUser.getId()); + LOGGER.info("Deleting user with ID: {}", foundUser.getId()); foundUser.delete(); - System.out.println("User successfully deleted!"); + LOGGER.info("User successfully deleted!"); } catch (Exception e) { - System.out.println(e.getMessage()); + LOGGER.error(e.getMessage(), e); } } catch (SQLException e) { - System.err.println("SQL error: " + e.getMessage()); + LOGGER.error("SQL error: {}", e.getMessage(), e); } } -} +} \ No newline at end of file diff --git a/active-record/src/main/java/com/iluwatar/activerecord/User.java b/active-record/src/main/java/com/iluwatar/activerecord/User.java index 2684feb9e5b6..795b688eccf4 100644 --- a/active-record/src/main/java/com/iluwatar/activerecord/User.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/User.java @@ -1,3 +1,27 @@ +/* + * 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; @@ -9,28 +33,56 @@ import java.util.ArrayList; import java.util.List; +/** + * Implementation of active record pattern. + */ + public class User { + /** + * DB_URL. + */ + private static final String DB_URL = "jdbc:sqlite:database.db"; + /** + * User ID. + */ private Integer id; + + /** + * User name. + */ private String name; + + /** + * User email. + */ private String email; - public User() { } + /** + * User constructor. + */ public User(Integer id, String name, String email) { this.id = id; this.name = name; this.email = email; } - // Establish a database connection + /** + * Establish a database connection. + */ + private static Connection connect() throws SQLException { return DriverManager.getConnection(DB_URL); } - // Initialize the table (if not exists) + + /** + * Initialize the table (if not exists). + */ + public static void initializeTable() throws SQLException { String sql = "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)"; try (Connection conn = connect(); @@ -39,7 +91,10 @@ public static void initializeTable() throws SQLException { } } - // Insert a new record into the database + /** + * Insert a new record into the database. + */ + public void save() throws SQLException { String sql; if (this.id == null) { // New record @@ -51,7 +106,9 @@ public void save() throws SQLException { 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); + if (this.id != null) { + pstmt.setInt(3, this.id); + } pstmt.executeUpdate(); if (this.id == null) { try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) { @@ -63,7 +120,10 @@ public void save() throws SQLException { } } - // Find a user by ID + /** + * Find a user by ID. + */ + public static User findById(int id) throws SQLException { String sql = "SELECT * FROM users WHERE id = ?"; try (Connection conn = connect(); @@ -76,8 +136,10 @@ public static User findById(int id) throws SQLException { } return null; } + /** + * Get all users. + */ - // Get all users public static List findAll() throws SQLException { String sql = "SELECT * FROM users"; List users = new ArrayList<>(); @@ -91,9 +153,15 @@ public static List findAll() throws SQLException { return users; } - // Delete the user from the database + /** + * Delete the user from the database. + */ + public void delete() throws SQLException { - if (this.id == null) return; + if (this.id == null) { + return; + } + String sql = "DELETE FROM users WHERE id = ?"; try (Connection conn = connect(); PreparedStatement pstmt = conn.prepareStatement(sql)) { @@ -103,10 +171,22 @@ public void delete() throws SQLException { } } - // Getters and Setters - public Integer getId() { return id; } - public String getName() { return name; } - public void setName(String name) { this.name = name; } - public String getEmail() { return email; } - public void setEmail(String email) { this.email = email; } + /** + * Getters and setters. + */ + public Integer getId() { + return id; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public String getEmail() { + return email; + } + public void setEmail(String email) { + this.email = email; + } } 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 index 3d8ebdedd113..f2c56eb6db0a 100644 --- a/active-record/src/main/java/com/iluwatar/activerecord/package-info.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/package-info.java @@ -1,7 +1,5 @@ /* - * This project is licensed under the MIT license. - * Module model-view-viewmodel is using - * ZK framework licensed under LGPL (see lgpl-3.0.txt). + * 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ä @@ -24,7 +22,6 @@ * 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 diff --git a/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java b/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java index 96555dfcb749..a5a94af019f2 100644 --- a/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java +++ b/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java @@ -1,3 +1,27 @@ +/* + * 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.*; diff --git a/database.db b/database.db new file mode 100644 index 0000000000000000000000000000000000000000..c828abf50816134f6d3e23a2cc2e5325cd4b0c88 GIT binary patch literal 12288 zcmeI&F-yZh6bJCTq*xpzZduP{Xd7!x!A=sJQzJ>5HYtdkpTckEH*xnR zrGs6ZtN%yt^6t2Igx~fcw>QJ2()2iAW>G~a;!sE_21G)LHV>1>CL|}V&4S ze73zb&jaO#ibD5nsA%hKCyz4S=gRJrY}>Xi-;bLsSEXK-&1C#2Z?>qD#N_#*x=;a) z18?L8Q@T=9awp-n=X1eG`C*^xyD3z6oa!t}(kAcLy*W4i!us4*e0(