From 309fceae7e2204038dc347549518e29c16c0dbfa Mon Sep 17 00:00:00 2001 From: ipaulaandreea Date: Wed, 16 Oct 2024 08:33:56 +0300 Subject: [PATCH 1/9] added classes with fields and logic --- active-record/pom.xml | 45 ++++++++ .../main/java/com/iluwatar/ActiveRecord.java | 88 ++++++++++++++++ .../src/main/java/com/iluwatar/App.java | 34 ++++++ .../src/main/java/com/iluwatar/User.java | 97 ++++++++++++++++++ my_database.db | Bin 0 -> 12288 bytes pom.xml | 1 + 6 files changed, 265 insertions(+) create mode 100644 active-record/pom.xml create mode 100644 active-record/src/main/java/com/iluwatar/ActiveRecord.java create mode 100644 active-record/src/main/java/com/iluwatar/App.java create mode 100644 active-record/src/main/java/com/iluwatar/User.java create mode 100644 my_database.db diff --git a/active-record/pom.xml b/active-record/pom.xml new file mode 100644 index 000000000000..7b2b0aa2b330 --- /dev/null +++ b/active-record/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + active-record + + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.xerial + sqlite-jdbc + 3.46.0.0 + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.activerecord.App + + + + + + + + + diff --git a/active-record/src/main/java/com/iluwatar/ActiveRecord.java b/active-record/src/main/java/com/iluwatar/ActiveRecord.java new file mode 100644 index 000000000000..1893f699b7f4 --- /dev/null +++ b/active-record/src/main/java/com/iluwatar/ActiveRecord.java @@ -0,0 +1,88 @@ +package com.iluwatar; + +import java.sql.*; + +public abstract class ActiveRecord { + // Database connection configuration + private static final String DB_URL = "jdbc:sqlite:my_database.db"; + + protected abstract String getTableName(); + protected abstract String getPrimaryKey(); + + protected Connection getConnection() throws SQLException { + return DriverManager.getConnection(DB_URL); + } + + //We are creating a table using SQLlite + protected void createTable(String createTableSQL) { + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute(createTableSQL); + System.out.println("Table created or already exists."); + } catch (SQLException e) { + System.out.println("Failed to create table: " + e.getMessage()); + } + } + + // Find record by ID + public static T find(Class clazz, int id) { + T instance; + try { + instance = clazz.getDeclaredConstructor().newInstance(); + String sql = "SELECT * FROM " + instance.getTableName() + + " WHERE " + instance.getPrimaryKey() + " = ?"; + try (Connection conn = instance.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setInt(1, id); + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + instance.loadFromResultSet(rs); + return instance; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + // Save the current record (insert or update) + public void save() { + try (Connection conn = getConnection()) { + if (isNewRecord()) { + insert(conn); + } else { + update(conn); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // Delete the current record + public void delete() { + String sql = "DELETE FROM " + getTableName() + " WHERE " + getPrimaryKey() + " = ?"; + try (Connection conn = getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setInt(1, getId()); + stmt.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // Insert a new record into the database + protected abstract void insert(Connection conn) throws SQLException; + + // Update an existing record + protected abstract void update(Connection conn) throws SQLException; + + // Check if this is a new record (i.e., the primary key is null) + protected abstract boolean isNewRecord(); + + // Load an object's fields from a result set + protected abstract void loadFromResultSet(ResultSet rs) throws SQLException; + + // Get the primary key value + protected abstract int getId(); +} diff --git a/active-record/src/main/java/com/iluwatar/App.java b/active-record/src/main/java/com/iluwatar/App.java new file mode 100644 index 000000000000..994dbe0a57af --- /dev/null +++ b/active-record/src/main/java/com/iluwatar/App.java @@ -0,0 +1,34 @@ +package com.iluwatar; + +public class App { + public static void main(String[] args) { + + //Create a temporary table + User.createTable(); + + // Create a new user + User newUser = new User("John Doe", "john@example.com"); + newUser.save(); + System.out.println("User created with ID: " + newUser.getId()); + + // Find a user by ID + User foundUser = User.find(User.class, newUser.getId()); + if (foundUser != null) { + System.out.println("Found user: " + foundUser.getName() + + ", " + foundUser.getEmail()); + } + + // Update the user + if (foundUser != null) { + foundUser.setName("John Smith"); + foundUser.save(); + System.out.println("User updated to: " + foundUser.getName()); + } + + // Delete the user + if (foundUser != null) { + foundUser.delete(); + System.out.println("User deleted."); + } + } +} diff --git a/active-record/src/main/java/com/iluwatar/User.java b/active-record/src/main/java/com/iluwatar/User.java new file mode 100644 index 000000000000..2ef750a776d3 --- /dev/null +++ b/active-record/src/main/java/com/iluwatar/User.java @@ -0,0 +1,97 @@ +package com.iluwatar; + +import java.sql.*; + +public class User extends ActiveRecord { + private Integer id; + private String name; + private String email; + + public User() { + } + + public User(String name, String email) { + this.name = name; + this.email = email; + } + + @Override + protected String getTableName() { + return "users"; + } + + @Override + protected String getPrimaryKey() { + return "id"; + } + + @Override + protected void insert(Connection conn) throws SQLException { + String sql = "INSERT INTO users (name, email) VALUES (?, ?)"; + try (PreparedStatement stmt = conn.prepareStatement( + sql, Statement.RETURN_GENERATED_KEYS)) { + stmt.setString(1, name); + stmt.setString(2, email); + stmt.executeUpdate(); + ResultSet generatedKeys = stmt.getGeneratedKeys(); + if (generatedKeys.next()) { + this.id = generatedKeys.getInt(1); + } + } + } + + @Override + protected void update(Connection conn) throws SQLException { + String sql = "UPDATE users SET name = ?, email = ? WHERE id = ?"; + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, name); + stmt.setString(2, email); + stmt.setInt(3, id); + stmt.executeUpdate(); + } + } + + @Override + protected boolean isNewRecord() { + return this.id == null; + } + + @Override + protected void loadFromResultSet(ResultSet rs) throws SQLException { + this.id = rs.getInt("id"); + this.name = rs.getString("name"); + this.email = rs.getString("email"); + } + + @Override + protected int getId() { + return this.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; + } + + public static void createTable() { + String sql = "CREATE TABLE IF NOT EXISTS users (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "name TEXT NOT NULL, " + + "email TEXT NOT NULL)"; + + // Call the method from the ActiveRecord base class + new User().createTable(sql); + } +} diff --git a/my_database.db b/my_database.db new file mode 100644 index 0000000000000000000000000000000000000000..bdf0ad14d5e4e921b76c024969fcfdaa9fa38558 GIT binary patch literal 12288 zcmeI&zfZzI6bJCTLTnsN-MX3Q1_4?42dL&^s--|V5}8cJGt{*FYDv`1!T-~L!rfO4 z12{NizAwG&z018z^Vwd~;UGu~Lr?Qn78leOr$S26AreB^JSsf4A$d{VCj2h*zrq%+ zx5WvcvYfVXK6pYv00Izz00bZa0SG_<0uX=z1P()>ZymXfh8!31Lu&G6%Kh_wZkB5^ zo9w5Rt4MiTk@hYFMSEv!vp6$NuB^XG+jU*}KG`rA%AY?Bz5$t_So#0} literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml index 743c8ea30122..58d94847a96b 100644 --- a/pom.xml +++ b/pom.xml @@ -217,6 +217,7 @@ virtual-proxy function-composition microservices-distributed-tracing + active-record From 58f4fb4a9b5c86fb9d2555d1350c7b66d34fab15 Mon Sep 17 00:00:00 2001 From: ipaulaandreea Date: Mon, 21 Oct 2024 08:34:50 +0300 Subject: [PATCH 2/9] added exception handling, started working on tests --- active-record/my_database.db | Bin 0 -> 12288 bytes .../{ => activerecord}/ActiveRecord.java | 19 ++++++--- .../com/iluwatar/{ => activerecord}/App.java | 9 ++-- .../com/iluwatar/{ => activerecord}/User.java | 2 +- .../activerecord/ActiveRecordTest.java | 39 ++++++++++++++++++ .../com/iluwatar/activerecord/AppTest.java | 14 +++++++ .../com/iluwatar/activerecord/UserTest.java | 4 ++ my_database.db | Bin 12288 -> 12288 bytes 8 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 active-record/my_database.db rename active-record/src/main/java/com/iluwatar/{ => activerecord}/ActiveRecord.java (79%) rename active-record/src/main/java/com/iluwatar/{ => activerecord}/App.java (90%) rename active-record/src/main/java/com/iluwatar/{ => activerecord}/User.java (98%) create mode 100644 active-record/src/test/java/com/iluwatar/activerecord/ActiveRecordTest.java create mode 100644 active-record/src/test/java/com/iluwatar/activerecord/AppTest.java create mode 100644 active-record/src/test/java/com/iluwatar/activerecord/UserTest.java diff --git a/active-record/my_database.db b/active-record/my_database.db new file mode 100644 index 0000000000000000000000000000000000000000..142143e783a320157a68e9bd92a68724e26037f5 GIT binary patch literal 12288 zcmeI&y-ve05C?EOsrVRB*s`HxYt_)DFVKK26iCxHbtO8XP>dQB_baJX*cfIw`2l&Kc*dL&O-XnPHo;4Q>`?ZX4zPuc)!s z`{K-;vYa+^KFx%H00bZa0SG_<0uX=z1Rwwb2pon$-#T)eO+GG?r%dI`%=FKXxmvE( zY_h+#Z(`v~LDIht1?`=!&l0UXQ`vaq$FA%0k3n_iI#;W_nyj7jDn<45nSw})TM<(~ z4!V9krh73aek{*?JlN1k T find(Class clazz, int id) { PreparedStatement stmt = conn.prepareStatement(sql)) { stmt.setInt(1, id); ResultSet rs = stmt.executeQuery(); + if (rs.next()) { instance.loadFromResultSet(rs); return instance; + } else { + throw new Exception("No record found with id: " + id); } + } catch (SQLException e) { + throw new RuntimeException("Database exception occurred while finding record with id: " + id, e); } } catch (Exception e) { - e.printStackTrace(); + System.out.println(e.getMessage()); } return null; } @@ -55,7 +63,7 @@ public void save() { update(conn); } } catch (SQLException e) { - e.printStackTrace(); + throw new RuntimeException("Database exception occurred while saving record", e); } } @@ -67,7 +75,8 @@ public void delete() { stmt.setInt(1, getId()); stmt.executeUpdate(); } catch (SQLException e) { - e.printStackTrace(); + throw new RuntimeException("Database exception occurred while deleting record", e); + } } diff --git a/active-record/src/main/java/com/iluwatar/App.java b/active-record/src/main/java/com/iluwatar/activerecord/App.java similarity index 90% rename from active-record/src/main/java/com/iluwatar/App.java rename to active-record/src/main/java/com/iluwatar/activerecord/App.java index 994dbe0a57af..0041e4d98859 100644 --- a/active-record/src/main/java/com/iluwatar/App.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/App.java @@ -1,10 +1,13 @@ -package com.iluwatar; +package com.iluwatar.activerecord; public class App { - public static void main(String[] args) { - //Create a temporary table + public App(){ + //create a temporary table User.createTable(); + } + public static void main(String[] args) { + // Create a new user User newUser = new User("John Doe", "john@example.com"); diff --git a/active-record/src/main/java/com/iluwatar/User.java b/active-record/src/main/java/com/iluwatar/activerecord/User.java similarity index 98% rename from active-record/src/main/java/com/iluwatar/User.java rename to active-record/src/main/java/com/iluwatar/activerecord/User.java index 2ef750a776d3..2977b48878a2 100644 --- a/active-record/src/main/java/com/iluwatar/User.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/User.java @@ -1,4 +1,4 @@ -package com.iluwatar; +package com.iluwatar.activerecord; import java.sql.*; diff --git a/active-record/src/test/java/com/iluwatar/activerecord/ActiveRecordTest.java b/active-record/src/test/java/com/iluwatar/activerecord/ActiveRecordTest.java new file mode 100644 index 000000000000..8736a02c3ff0 --- /dev/null +++ b/active-record/src/test/java/com/iluwatar/activerecord/ActiveRecordTest.java @@ -0,0 +1,39 @@ +package com.iluwatar.activerecord; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class ActiveRecordTest { + private App app; + + /** + * Setup of application test includes: initializing DB connection. + */ + @BeforeEach + void setUp() { + + app = new App(); + } + + @Test + void testCreateUser() { + + } + + @Test + void testUpdateUser() { + + } + + @Test + void testDeleteUser() { + + } + + @Test + void testFindUserById() { + + } +} 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..5f7e135f7fc6 --- /dev/null +++ b/active-record/src/test/java/com/iluwatar/activerecord/AppTest.java @@ -0,0 +1,14 @@ +package com.iluwatar.activerecord; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + + +import org.junit.jupiter.api.Test; + + +public class AppTest { + @Test + void shouldExecuteApplicationWithoutException() { + + 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..ad667d47ce0a --- /dev/null +++ b/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java @@ -0,0 +1,4 @@ +package com.iluwatar.activerecord; + +public class UserTest { +} diff --git a/my_database.db b/my_database.db index bdf0ad14d5e4e921b76c024969fcfdaa9fa38558..a4a69bc476fd0ec99563c915f0af8d0d6ac6f67f 100644 GIT binary patch delta 214 zcmZojXh@hK%_uQZ#+gxKW5N=C0apGj2L4C Date: Mon, 21 Oct 2024 21:54:23 +0300 Subject: [PATCH 3/9] wip unit tests --- active-record/my_database.db | Bin 12288 -> 16384 bytes .../iluwatar/activerecord/ActiveRecord.java | 7 +- .../java/com/iluwatar/activerecord/App.java | 1 - .../java/com/iluwatar/activerecord/User.java | 24 +++++-- .../activerecord/ActiveRecordTest.java | 67 +++++++++++++----- .../com/iluwatar/activerecord/UserTest.java | 63 ++++++++++++++++ 6 files changed, 138 insertions(+), 24 deletions(-) diff --git a/active-record/my_database.db b/active-record/my_database.db index 142143e783a320157a68e9bd92a68724e26037f5..807c37f11df343e6325fb92ecaa993ee1722cb4c 100644 GIT binary patch literal 16384 zcmeI&&uSAv90%~3?Aln(%*6U<)do7bH8ccm?$RiW1h?BZouYw)#JWNxX>E6Vu(uop z@d$AURHxqf~|4YI5Q~=!7j5O4Yg_g>rIRF880ycC5mOVMlIu z+Uu=OS3U~6vbCc&+OaNL4`Wr|?+p{F!Y4|`8+zM`qPk3mz5ZbKW^J=D7p$z%?syXE z;XtpSpQg#-ak9UcoVxU8CUh!X&IhdP%GzsM41$1;wx-WDo_t}Ze0=MS&mzzIF!|bi zvGkpYAL5JnAl{0XA`%Tz=D+wi{+Yk$Cw!OROtSmay+s^csNt=_No70|e zn$$XOU{Y)C4U<}Hubb3bd(EWQ+N;iV!I@q$sde14Nv-1wXUa`#?U|X>+H=X7UN)(< z=SwEF_B`)QFFMnrGo5p$7fiZ3d9PNd@0D*->zqB4T5Ag?wQ1z*e=7bE{X>HQ1Rwwb z2tWV=5P$##AOHafK;S$H%u_F*-i*uG1rxq0h02z5582|tP diff --git a/active-record/src/main/java/com/iluwatar/activerecord/ActiveRecord.java b/active-record/src/main/java/com/iluwatar/activerecord/ActiveRecord.java index 066e9432c006..c009ce3bccbc 100644 --- a/active-record/src/main/java/com/iluwatar/activerecord/ActiveRecord.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/ActiveRecord.java @@ -12,7 +12,7 @@ public abstract class ActiveRecord { protected abstract String getTableName(); protected abstract String getPrimaryKey(); - protected Connection getConnection() throws SQLException { + protected static Connection getConnection() throws SQLException { return DriverManager.getConnection(DB_URL); } @@ -55,12 +55,15 @@ public static T find(Class clazz, int id) { } // Save the current record (insert or update) - public void save() { + public String save() { try (Connection conn = getConnection()) { if (isNewRecord()) { insert(conn); + return "Inserted new entry successfully"; } else { update(conn); + return "Updated existing entry successfully"; + } } catch (SQLException e) { throw new RuntimeException("Database exception occurred while saving record", e); 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 0041e4d98859..a17503178ead 100644 --- a/active-record/src/main/java/com/iluwatar/activerecord/App.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/App.java @@ -8,7 +8,6 @@ public App(){ } public static void main(String[] args) { - // Create a new user User newUser = new User("John Doe", "john@example.com"); newUser.save(); 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 2977b48878a2..ccc672abae2f 100644 --- a/active-record/src/main/java/com/iluwatar/activerecord/User.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/User.java @@ -6,9 +6,23 @@ public class User extends ActiveRecord { private Integer id; private String name; private String email; + private String tableName; public User() { } + public User(Integer id, String name, String email){ + this.id = id; + this.name = name; + this.email = email; + this.tableName = "users"; + } + + public User(Integer id, String name, String email, String tableName){ + this.id = id; + this.name = name; + this.email = email; + this.tableName = tableName; + } public User(String name, String email) { this.name = name; @@ -17,7 +31,7 @@ public User(String name, String email) { @Override protected String getTableName() { - return "users"; + return this.tableName; } @Override @@ -27,11 +41,12 @@ protected String getPrimaryKey() { @Override protected void insert(Connection conn) throws SQLException { - String sql = "INSERT INTO users (name, email) VALUES (?, ?)"; + String sql = "INSERT INTO ? (name, email) VALUES (?, ?)"; try (PreparedStatement stmt = conn.prepareStatement( sql, Statement.RETURN_GENERATED_KEYS)) { - stmt.setString(1, name); - stmt.setString(2, email); + stmt.setString(1, tableName); + stmt.setString(2, name); + stmt.setString(3, email); stmt.executeUpdate(); ResultSet generatedKeys = stmt.getGeneratedKeys(); if (generatedKeys.next()) { @@ -61,6 +76,7 @@ protected void loadFromResultSet(ResultSet rs) throws SQLException { this.id = rs.getInt("id"); this.name = rs.getString("name"); this.email = rs.getString("email"); + } @Override diff --git a/active-record/src/test/java/com/iluwatar/activerecord/ActiveRecordTest.java b/active-record/src/test/java/com/iluwatar/activerecord/ActiveRecordTest.java index 8736a02c3ff0..e9bde893bb88 100644 --- a/active-record/src/test/java/com/iluwatar/activerecord/ActiveRecordTest.java +++ b/active-record/src/test/java/com/iluwatar/activerecord/ActiveRecordTest.java @@ -1,39 +1,72 @@ package com.iluwatar.activerecord; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.sql.Connection; +import java.sql.Statement; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; public class ActiveRecordTest { - private App app; - /** - * Setup of application test includes: initializing DB connection. - */ - @BeforeEach - void setUp() { + User testUser1 = new User( + null, + "Test1 Name", + "test1@test.com", + "usersTEST" + ); + User testUser2 = new User( + 1, + "Test2 Name", + "test2@test.com", + "usersTEST" - app = new App(); - } + ); + User testUser3 = new User( + null, + "Test3 Name", + "test3@test.com", + "usersTEST" + ); + + private final String sqlStatement = "" + + "CREATE TABLE IF NOT EXISTS usersTEST (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "name TEXT NOT NULL, email TEXT NOT NULL)"; - @Test - void testCreateUser() { + @BeforeEach + public void setup() { + assertDoesNotThrow(() -> { + Connection conn = ActiveRecord.getConnection(); + assertNotNull(conn); + try (Statement stmt = conn.createStatement()) { + stmt.execute(sqlStatement); + } + }); } @Test - void testUpdateUser() { - + public void testSaveUser() { + assertEquals(testUser1.save(), ("Inserted new entry successfully")); + assertEquals(testUser3.save(), ("Inserted new entry successfully")); + assertEquals(testUser2.save(), ("Updated existing entry successfully")); } - @Test - void testDeleteUser() { +// TODO: Fix + @Test + public void testFindUserByID(){ + System.out.println(testUser2.getTableName()); + testUser2.save(); + User foundUser = ActiveRecord.find(User.class, 1); + System.out.println(foundUser.toString()); + assertNotNull(foundUser, "User should not be null when found by ID 1"); } @Test - void testFindUserById() { - + public void testDeleteUser() { + assertDoesNotThrow(testUser2::delete); } } 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 ad667d47ce0a..630c8e175a30 100644 --- a/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java +++ b/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java @@ -1,4 +1,67 @@ package com.iluwatar.activerecord; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.sql.Connection; +import java.sql.Statement; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + public class UserTest { + User testUser1 = new User( + null, + "Test1 Name", + "test1@test.com" + ); + User testUser2 = new User( + 1, + "Test2 Name", + "test2@test.com" + + ); + User testUser3 = new User( + null, + "Test3 Name", + "test3@test.com" + ); + + private final String sqlStatement = "" + + "CREATE TABLE IF NOT EXISTS usersTEST (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "name TEXT NOT NULL, email TEXT NOT NULL)"; + + /** + * Setup of application test includes initializing DB connection. + */ + @BeforeEach + public void setup() { + assertDoesNotThrow(() -> { + Connection conn = ActiveRecord.getConnection(); + assertNotNull(conn); + try (Statement stmt = conn.createStatement()) { + stmt.execute(sqlStatement); + } + }); + } + + @Test + void testInsertUser(){ + + } + + @Test + void testUpdateUser() { + + } + + @Test + void testDeleteUser() { + + } + + @Test + void testFindUserById() { + + } } From f6908f0fb8e9bdf7cf957530f9811ddb088b2636 Mon Sep 17 00:00:00 2001 From: ipaulaandreea Date: Mon, 4 Nov 2024 08:26:32 +0200 Subject: [PATCH 4/9] fixed db bugs --- .../iluwatar/activerecord/ActiveRecord.java | 100 ----------- .../java/com/iluwatar/activerecord/App.java | 65 ++++--- .../java/com/iluwatar/activerecord/User.java | 169 +++++++++--------- .../activerecord/ActiveRecordTest.java | 72 -------- .../com/iluwatar/activerecord/AppTest.java | 14 -- .../com/iluwatar/activerecord/UserTest.java | 95 +++++----- active-record/my_database.db => database.db | Bin 16384 -> 12288 bytes 7 files changed, 178 insertions(+), 337 deletions(-) delete mode 100644 active-record/src/main/java/com/iluwatar/activerecord/ActiveRecord.java delete mode 100644 active-record/src/test/java/com/iluwatar/activerecord/ActiveRecordTest.java delete mode 100644 active-record/src/test/java/com/iluwatar/activerecord/AppTest.java rename active-record/my_database.db => database.db (71%) diff --git a/active-record/src/main/java/com/iluwatar/activerecord/ActiveRecord.java b/active-record/src/main/java/com/iluwatar/activerecord/ActiveRecord.java deleted file mode 100644 index c009ce3bccbc..000000000000 --- a/active-record/src/main/java/com/iluwatar/activerecord/ActiveRecord.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.iluwatar.activerecord; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.sql.*; - -public abstract class ActiveRecord { - // Database connection configuration - private static final String DB_URL = "jdbc:sqlite:my_database.db"; - private static final Logger log = LoggerFactory.getLogger(ActiveRecord.class); - - protected abstract String getTableName(); - protected abstract String getPrimaryKey(); - - protected static Connection getConnection() throws SQLException { - return DriverManager.getConnection(DB_URL); - } - - //Create table - protected void createTable(String createTableSQL) { - try (Connection conn = getConnection(); - Statement stmt = conn.createStatement()) { - stmt.execute(createTableSQL); - System.out.println("Table created or already exists."); - } catch (SQLException e) { - System.out.println("Failed to create table: " + e.getMessage()); - } - } - - // Find record by ID - public static T find(Class clazz, int id) { - T instance; - try { - instance = clazz.getDeclaredConstructor().newInstance(); - String sql = "SELECT * FROM " + instance.getTableName() + - " WHERE " + instance.getPrimaryKey() + " = ?"; - try (Connection conn = instance.getConnection(); - PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setInt(1, id); - ResultSet rs = stmt.executeQuery(); - - if (rs.next()) { - instance.loadFromResultSet(rs); - return instance; - } else { - throw new Exception("No record found with id: " + id); - } - } catch (SQLException e) { - throw new RuntimeException("Database exception occurred while finding record with id: " + id, e); - } - } catch (Exception e) { - System.out.println(e.getMessage()); - } - return null; - } - - // Save the current record (insert or update) - public String save() { - try (Connection conn = getConnection()) { - if (isNewRecord()) { - insert(conn); - return "Inserted new entry successfully"; - } else { - update(conn); - return "Updated existing entry successfully"; - - } - } catch (SQLException e) { - throw new RuntimeException("Database exception occurred while saving record", e); - } - } - - // Delete the current record - public void delete() { - String sql = "DELETE FROM " + getTableName() + " WHERE " + getPrimaryKey() + " = ?"; - try (Connection conn = getConnection(); - PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setInt(1, getId()); - stmt.executeUpdate(); - } catch (SQLException e) { - throw new RuntimeException("Database exception occurred while deleting record", e); - - } - } - - // Insert a new record into the database - protected abstract void insert(Connection conn) throws SQLException; - - // Update an existing record - protected abstract void update(Connection conn) throws SQLException; - - // Check if this is a new record (i.e., the primary key is null) - protected abstract boolean isNewRecord(); - - // Load an object's fields from a result set - protected abstract void loadFromResultSet(ResultSet rs) throws SQLException; - - // Get the primary key value - protected abstract int getId(); -} 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 a17503178ead..f28be0821f37 100644 --- a/active-record/src/main/java/com/iluwatar/activerecord/App.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/App.java @@ -1,36 +1,55 @@ package com.iluwatar.activerecord; +import java.sql.SQLException; +import java.util.List; + public class App { - public App(){ - //create a temporary table - User.createTable(); - } public static void main(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."); - // Create a new user - User newUser = new User("John Doe", "john@example.com"); - newUser.save(); - System.out.println("User created with ID: " + newUser.getId()); + // 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()); - // Find a user by ID - User foundUser = User.find(User.class, newUser.getId()); - if (foundUser != null) { - System.out.println("Found user: " + foundUser.getName() - + ", " + foundUser.getEmail()); - } + // 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()); + } else { + System.out.println("User not found."); + } - // Update the user - if (foundUser != null) { - foundUser.setName("John Smith"); + // Update the user’s details + assert foundUser != null; + foundUser.setName("John Updated"); + foundUser.setEmail("john.updated@example.com"); foundUser.save(); - System.out.println("User updated to: " + foundUser.getName()); - } + System.out.println("User updated: " + foundUser.getName() + " with email " + foundUser.getEmail()); + + // Retrieve all users + List users = User.findAll(); + System.out.println("All users in the database:"); + for (User user : users) { + System.out.println("ID: " + user.getId() + ", Name: " + user.getName() + ", Email: " + user.getEmail()); + } + + // Delete the user + try { + System.out.println("Deleting user with ID: " + foundUser.getId()); + foundUser.delete(); + System.out.println("User successfully deleted!"); + } + catch (Exception e){ + System.out.println(e.getMessage()); + } - // Delete the user - if (foundUser != null) { - foundUser.delete(); - System.out.println("User deleted."); + } catch (SQLException e) { + System.err.println("SQL error: " + e.getMessage()); } } } 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 ccc672abae2f..c4dd5098cc37 100644 --- a/active-record/src/main/java/com/iluwatar/activerecord/User.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/User.java @@ -1,113 +1,108 @@ package com.iluwatar.activerecord; +import lombok.Data; import java.sql.*; +import java.util.ArrayList; +import java.util.List; + + +public class User { + private static final String DB_URL = "jdbc:sqlite:database.db"; -public class User extends ActiveRecord { private Integer id; private String name; private String email; - private String tableName; - public User() { - } - public User(Integer id, String name, String email){ - this.id = id; - this.name = name; - this.email = email; - this.tableName = "users"; - } + public User() { } - public User(Integer id, String name, String email, String tableName){ + public User(Integer id, String name, String email) { this.id = id; this.name = name; this.email = email; - this.tableName = tableName; } - public User(String name, String email) { - this.name = name; - this.email = email; + // Establish a database connection + private static Connection connect() throws SQLException { + return DriverManager.getConnection(DB_URL); } - @Override - protected String getTableName() { - return this.tableName; - } - - @Override - protected String getPrimaryKey() { - return "id"; + // 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); + } } - @Override - protected void insert(Connection conn) throws SQLException { - String sql = "INSERT INTO ? (name, email) VALUES (?, ?)"; - try (PreparedStatement stmt = conn.prepareStatement( - sql, Statement.RETURN_GENERATED_KEYS)) { - stmt.setString(1, tableName); - stmt.setString(2, name); - stmt.setString(3, email); - stmt.executeUpdate(); - ResultSet generatedKeys = stmt.getGeneratedKeys(); - if (generatedKeys.next()) { - this.id = generatedKeys.getInt(1); + // 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); + } + } } } } - @Override - protected void update(Connection conn) throws SQLException { - String sql = "UPDATE users SET name = ?, email = ? WHERE id = ?"; - try (PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setString(1, name); - stmt.setString(2, email); - stmt.setInt(3, id); - stmt.executeUpdate(); + // 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; } } - @Override - protected boolean isNewRecord() { - return this.id == null; - } - - @Override - protected void loadFromResultSet(ResultSet rs) throws SQLException { - this.id = rs.getInt("id"); - this.name = rs.getString("name"); - this.email = rs.getString("email"); - - } - - @Override - protected int getId() { - return this.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; - } - - public static void createTable() { - String sql = "CREATE TABLE IF NOT EXISTS users (" + - "id INTEGER PRIMARY KEY AUTOINCREMENT, " + - "name TEXT NOT NULL, " + - "email TEXT NOT NULL)"; - - // Call the method from the ActiveRecord base class - new User().createTable(sql); - } + // 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/test/java/com/iluwatar/activerecord/ActiveRecordTest.java b/active-record/src/test/java/com/iluwatar/activerecord/ActiveRecordTest.java deleted file mode 100644 index e9bde893bb88..000000000000 --- a/active-record/src/test/java/com/iluwatar/activerecord/ActiveRecordTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.iluwatar.activerecord; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import java.sql.Connection; -import java.sql.Statement; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -public class ActiveRecordTest { - - User testUser1 = new User( - null, - "Test1 Name", - "test1@test.com", - "usersTEST" - ); - User testUser2 = new User( - 1, - "Test2 Name", - "test2@test.com", - "usersTEST" - - ); - User testUser3 = new User( - null, - "Test3 Name", - "test3@test.com", - "usersTEST" - ); - - private final String sqlStatement = "" + - "CREATE TABLE IF NOT EXISTS usersTEST (" + - "id INTEGER PRIMARY KEY AUTOINCREMENT," + - "name TEXT NOT NULL, email TEXT NOT NULL)"; - - - @BeforeEach - public void setup() { - assertDoesNotThrow(() -> { - Connection conn = ActiveRecord.getConnection(); - assertNotNull(conn); - try (Statement stmt = conn.createStatement()) { - stmt.execute(sqlStatement); - } - }); - } - - @Test - public void testSaveUser() { - assertEquals(testUser1.save(), ("Inserted new entry successfully")); - assertEquals(testUser3.save(), ("Inserted new entry successfully")); - assertEquals(testUser2.save(), ("Updated existing entry successfully")); - } - - -// TODO: Fix - @Test - public void testFindUserByID(){ - System.out.println(testUser2.getTableName()); - testUser2.save(); - User foundUser = ActiveRecord.find(User.class, 1); - System.out.println(foundUser.toString()); - assertNotNull(foundUser, "User should not be null when found by ID 1"); - } - - @Test - public void testDeleteUser() { - assertDoesNotThrow(testUser2::delete); - } -} diff --git a/active-record/src/test/java/com/iluwatar/activerecord/AppTest.java b/active-record/src/test/java/com/iluwatar/activerecord/AppTest.java deleted file mode 100644 index 5f7e135f7fc6..000000000000 --- a/active-record/src/test/java/com/iluwatar/activerecord/AppTest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.iluwatar.activerecord; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; - - -import org.junit.jupiter.api.Test; - - -public class AppTest { - @Test - void shouldExecuteApplicationWithoutException() { - - 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 index 630c8e175a30..96555dfcb749 100644 --- a/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java +++ b/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java @@ -1,67 +1,80 @@ package com.iluwatar.activerecord; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +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 static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertNotNull; +class UserTest { -public class UserTest { - User testUser1 = new User( - null, - "Test1 Name", - "test1@test.com" - ); - User testUser2 = new User( - 1, - "Test2 Name", - "test2@test.com" - - ); - User testUser3 = new User( - null, - "Test3 Name", - "test3@test.com" - ); - - private final String sqlStatement = "" + - "CREATE TABLE IF NOT EXISTS usersTEST (" + - "id INTEGER PRIMARY KEY AUTOINCREMENT," + - "name TEXT NOT NULL, email TEXT NOT NULL)"; + @BeforeAll + static void setupDatabase() throws SQLException { + User.initializeTable(); + } - /** - * Setup of application test includes initializing DB connection. - */ @BeforeEach - public void setup() { - assertDoesNotThrow(() -> { - Connection conn = ActiveRecord.getConnection(); - assertNotNull(conn); - try (Statement stmt = conn.createStatement()) { - stmt.execute(sqlStatement); - } - }); + void clearDatabase() throws SQLException { + // Clean up table before each test + try (Connection conn = DriverManager.getConnection("jdbc:sqlite:database.db"); + Statement stmt = conn.createStatement()) { + stmt.execute("DELETE FROM users"); + } } @Test - void testInsertUser(){ + 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 { + User user = new User(null, "Bob", "bob@example.com"); + user.save(); + + User foundUser = User.findById(user.getId()); + assertNotNull(foundUser, "User should be found by ID"); + assertEquals("Bob", foundUser.getName()); + assertEquals("bob@example.com", foundUser.getEmail()); } @Test - void testUpdateUser() { + 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"); } @Test - void testDeleteUser() { + 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(); + User updatedUser = User.findById(user.getId()); + assert updatedUser != null; + assertEquals("Eve Updated", updatedUser.getName()); + assertEquals("eve.updated@example.com", updatedUser.getEmail()); } @Test - void testFindUserById() { + void testDeleteUser() throws SQLException { + User user = new User(null, "Frank", "frank@example.com"); + user.save(); + Integer userId = user.getId(); + user.delete(); + assertNull(User.findById(userId), "User should be deleted from the database"); } } diff --git a/active-record/my_database.db b/database.db similarity index 71% rename from active-record/my_database.db rename to database.db index 807c37f11df343e6325fb92ecaa993ee1722cb4c..3da40ca4f09b262838881d18467114d2b18a00a0 100644 GIT binary patch delta 144 zcmZo@U~EX3AT21sz`(!^#4x}(QO6i4s8?0M3lw7FcV*z8&hNTeP~j@yWMlpy&U8k0 zadC0RM%~GM{K4Wn3aPn?nK=p}t`Q-cAPo#a^Z2g;m7n2P(_m#`P_^~S&&X2E6Vu(uop z@d$AURHxqf~|4YI5Q~=!7j5O4Yg_g>rIRF880ycC5mOVMlIu z+Uu=OS3U~6vbCc&+OaNL4`Wr|?+p{F!Y4|`8+zM`qPk3mz5ZbKW^J=D7p$z%?syXE z;XtpSpQg#-ak9UcoVxU8CUh!X&IhdP%GzsM41$1;wx-WDo_t}Ze0=MS&mzzIF!|bi zvGkpYAL5JnAl{0XA`%Tz=D+wi{+Yk$Cw!OROtSmay+s^csNt=_No70|e zn$$XOU{Y)C4U<}Hubb3bd(EWQ+N;iV!I@q$sde14Nv-1wXUa`#?U|X>+H=X7UN)(< z=SwEF_B`)QFFMnrGo5p$7fiZ3d9PNd@0D*->zqB4T5Ag?wQ1z*e=7bE{X>HQ1Rwwb z2tWV=5P$##AOHafK;S$H%u_F* Date: Tue, 5 Nov 2024 08:49:30 +0200 Subject: [PATCH 5/9] fixed checkstyle for App Class, created package-info --- .../java/com/iluwatar/activerecord/App.java | 75 +++++++++++++++--- .../java/com/iluwatar/activerecord/User.java | 8 +- .../iluwatar/activerecord/package-info.java | 60 ++++++++++++++ database.db | Bin 12288 -> 0 bytes my_database.db | Bin 12288 -> 0 bytes 5 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 active-record/src/main/java/com/iluwatar/activerecord/package-info.java delete mode 100644 database.db delete mode 100644 my_database.db 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 f28be0821f37..2e393bedca60 100644 --- a/active-record/src/main/java/com/iluwatar/activerecord/App.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/App.java @@ -1,11 +1,64 @@ -package com.iluwatar.activerecord; +/* + * 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 lombok.extern.slf4j.Slf4j; import java.sql.SQLException; import java.util.List; -public class App { +/** + * 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 + * a SQLite 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(String[] args) { + + public static void main(final String[] args) { try { // Initialize the database and create the users table if it doesn't exist User.initializeTable(); @@ -14,12 +67,14 @@ public static void main(String[] args) { // 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()); + System.out.println("New user saved: " + user1.getName() + + " with ID " + 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()); + System.out.println("User found: " + foundUser.getName() + + " with email " + foundUser.getEmail()); } else { System.out.println("User not found."); } @@ -29,13 +84,16 @@ public static void main(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()); + System.out.println("User updated: " + foundUser.getName() + + " with email " + foundUser.getEmail()); // Retrieve all users List users = User.findAll(); System.out.println("All users in the database:"); for (User user : users) { - System.out.println("ID: " + user.getId() + ", Name: " + user.getName() + ", Email: " + user.getEmail()); + System.out.println("ID: " + user.getId() + + ", Name: " + user.getName() + + ", Email: " + user.getEmail()); } // Delete the user @@ -43,8 +101,7 @@ public static void main(String[] args) { System.out.println("Deleting user with ID: " + foundUser.getId()); foundUser.delete(); System.out.println("User successfully deleted!"); - } - catch (Exception e){ + } catch (Exception e) { System.out.println(e.getMessage()); } 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 c4dd5098cc37..2684feb9e5b6 100644 --- a/active-record/src/main/java/com/iluwatar/activerecord/User.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/User.java @@ -1,7 +1,11 @@ package com.iluwatar.activerecord; -import lombok.Data; -import java.sql.*; +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; 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..3d8ebdedd113 --- /dev/null +++ b/active-record/src/main/java/com/iluwatar/activerecord/package-info.java @@ -0,0 +1,60 @@ +/* + * 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/database.db b/database.db deleted file mode 100644 index 3da40ca4f09b262838881d18467114d2b18a00a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI&zfXfe6bJCT(rO$G-B{mZLet9PY*I8AgKe=QO=L1yPQ~!61rp=tU*Uh|f8y?e z>0lS<>h}eX_sG2q`D`!Y=6V`ek{bTC{}z2_ zy%vXRS4S62e^(0v0uX=z1Rwwb2tWV=5P$##Ah4?f^Uj_z9P*$F?-Nr$@EcIo_RZ(oraN94G^xDX!A zJ!fWn0Zl|e_QJn%T-7iWu0NvYZ1TmeN@W_xNt+Lv*}R$b^^aY}$9F>j0uX=z1Rwwb z2tWV=5P$##{;Po5>$0QM%ls*$MG=LSjGk3))!+G8zJzI!NNbs=&HT^xPo^#e1Rwwb e2tWV=5P$##AOHafKmY=NN#KBM25)~J=zRf!XILcw diff --git a/my_database.db b/my_database.db deleted file mode 100644 index a4a69bc476fd0ec99563c915f0af8d0d6ac6f67f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI&!A`<37zgkcrbvjXw{cKYZxE0RFTm8rfMdWIiJZ*D8b&q-Y$WQ2c5T15S$;b;X}@QK~i!gnHGCkKR5(%_sBQj{et%POdBD65Wq z$Nk@;NNP{BeK{+`>V&<>3IPEKKmY;|fB*y_009U<00I!$4uNiFSFKd&Fmi4^5zak% ze!dCCd?}`*&2IMG(luM>wsvOdd}D5D>iD86SC$`XPE}R<+)K7xhGG#WRc38@GQ>;n z9dDYpeyLl$Yc<=NHRM}Q|00Izz00bZa0SG_<0ucBMf#ZTgPEK3Fc*^^}8;vKj z)esMkKl4Ow6!?dEg&ck7F9Pu+yO)~HrDk_ivr1YvKL1npN#uio00bZa0SG_<0uX=z V1Rwwb2teRJ3FK*3rO9=I!WTuOj1B+* From 26a31de27442194b8b47428a2bd8472a7c2937b5 Mon Sep 17 00:00:00 2001 From: ipaulaandreea Date: Tue, 5 Nov 2024 21:22:21 +0200 Subject: [PATCH 6/9] 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( Date: Fri, 15 Nov 2024 21:05:23 +0200 Subject: [PATCH 7/9] added changes after cr --- active-record/README.md | 11 +- active-record/database.db | Bin 12288 -> 0 bytes active-record/pom.xml | 28 +++-- .../java/com/iluwatar/activerecord/App.java | 66 +++++++---- .../java/com/iluwatar/activerecord/User.java | 110 ++++++++++++++---- .../iluwatar/activerecord/package-info.java | 3 +- .../com/iluwatar/activerecord/AppTest.java | 44 +++++++ .../com/iluwatar/activerecord/UserTest.java | 38 ++++-- database.db | Bin 12288 -> 0 bytes update-header.sh | 32 ----- 10 files changed, 234 insertions(+), 98 deletions(-) delete mode 100644 active-record/database.db create mode 100644 active-record/src/test/java/com/iluwatar/activerecord/AppTest.java delete mode 100644 database.db delete mode 100755 update-header.sh diff --git a/active-record/README.md b/active-record/README.md index 5fc2159e9e4e..f4af0e531e0c 100644 --- a/active-record/README.md +++ b/active-record/README.md @@ -73,16 +73,21 @@ public class User { 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 DB_URL = "jdbc:sqlite:database.db"; + private static final String JDBC_URL = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"; + private static final String USERNAME = "sa"; + private static final String PASSWORD = ""; // Establish a database connection. private static Connection connect() throws SQLException { - return DriverManager.getConnection(DB_URL); + return DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD); } - // Initialize the table (if not exists). + // 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 (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)"; diff --git a/active-record/database.db b/active-record/database.db deleted file mode 100644 index 831ad85866ffb8328086c549c30d1262add5a4d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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) diff --git a/active-record/pom.xml b/active-record/pom.xml index 5efadafc693e..fe66c28e3087 100644 --- a/active-record/pom.xml +++ b/active-record/pom.xml @@ -35,23 +35,37 @@ 1.26.0-SNAPSHOT active-record + active-record + active-record + + + + + org.springframework.boot + spring-boot-dependencies + pom + 3.2.4 + import + + + + org.projectlombok lombok - - 1.18.30 + true + + + com.h2database + h2 + 2.1.214 org.junit.jupiter junit-jupiter-engine test - - org.xerial - sqlite-jdbc - 3.46.0.0 - 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 35f49a974925..7d4cc89c9ab0 100644 --- a/active-record/src/main/java/com/iluwatar/activerecord/App.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/App.java @@ -1,5 +1,6 @@ /* - * 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ä @@ -25,6 +26,7 @@ package com.iluwatar.activerecord; import java.sql.SQLException; import java.util.List; +import java.util.Optional; import lombok.extern.slf4j.Slf4j; /** @@ -36,7 +38,7 @@ * *

In this example, we demonstrate the Active Record pattern * by creating, updating, retrieving, and deleting user records in - * a SQLite database. The User class contains methods to perform + * an H2 in-memory database. The User class contains methods to perform * these operations, ensuring that the database interactions are * straightforward and intuitive. */ @@ -55,48 +57,62 @@ private App() { */ public static void main(final String[] args) { try { - // Initialize the database and create the users table if it doesn't exist + // 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"); + User user1 = new User( + null, + "John Doe", + "john.doe@example.com"); user1.save(); - LOGGER.info("New user saved: {} with ID {}", user1.getName(), 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) { - LOGGER.info("User found: {} with email {}", foundUser.getName(), foundUser.getEmail()); - } else { - LOGGER.info("User not found."); - } + 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 - 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()); + 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()); + 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); - } + 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); } } -} \ 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 795b688eccf4..099e468de7dd 100644 --- a/active-record/src/main/java/com/iluwatar/activerecord/User.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/User.java @@ -1,5 +1,6 @@ /* - * 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ä @@ -32,18 +33,30 @@ 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"; + /** - * DB_URL. + * Database username. */ + private static final String USERNAME = "sa"; - private static final String DB_URL = "jdbc:sqlite:database.db"; + /** + * Database password. + */ + private static final String PASSWORD = ""; /** * User ID. @@ -60,31 +73,45 @@ public class User { */ 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(Integer id, String name, String email) { - this.id = id; - this.name = name; - this.email = email; + 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(DB_URL); + return DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD); } /** - * Initialize the table (if not exists). + * 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 (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)"; + 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); @@ -103,7 +130,8 @@ public void save() throws SQLException { sql = "UPDATE users SET name = ?, email = ? WHERE id = ?"; } try (Connection conn = connect(); - PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + PreparedStatement pstmt = conn.prepareStatement( + sql, Statement.RETURN_GENERATED_KEYS)) { pstmt.setString(1, this.name); pstmt.setString(2, this.email); if (this.id != null) { @@ -122,22 +150,34 @@ public void save() throws SQLException { /** * 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 User findById(int id) throws SQLException { + 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 new User(rs.getInt("id"), rs.getString("name"), rs.getString("email")); + 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 null; + return Optional.empty(); } + /** * Get all users. + * + * @return all users from the database; */ public static List findAll() throws SQLException { @@ -147,7 +187,10 @@ public static List findAll() throws SQLException { 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"))); + users.add(new User( + rs.getInt("id"), + rs.getString("name"), + rs.getString("email"))); } } return users; @@ -172,21 +215,48 @@ public void delete() throws SQLException { } /** - * Getters and setters. + * 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; } - public void setName(String name) { + + /** + * Sets the name of the user. + * + * @param userName the name to set for the user + */ + public void setName(final String userName) { this.name = name; } + + /** + * Gets the email address of the user. + * + * @return the email address of the user + */ public String getEmail() { return email; } - public void setEmail(String 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 = 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 f2c56eb6db0a..261fd418e8fe 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,5 +1,6 @@ /* - * 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ä 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 index a5a94af019f2..4f8f24d584d2 100644 --- a/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java +++ b/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java @@ -30,10 +30,16 @@ 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"; + private static final String USERNAME = "sa"; + private static final String PASSWORD = ""; + @BeforeAll static void setupDatabase() throws SQLException { User.initializeTable(); @@ -42,7 +48,7 @@ static void setupDatabase() throws SQLException { @BeforeEach void clearDatabase() throws SQLException { // Clean up table before each test - try (Connection conn = DriverManager.getConnection("jdbc:sqlite:database.db"); + try (Connection conn = DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD); Statement stmt = conn.createStatement()) { stmt.execute("DELETE FROM users"); } @@ -57,13 +63,18 @@ void testSaveNewUser() throws SQLException { @Test void testFindById() throws SQLException { + // Create and save a new user User user = new User(null, "Bob", "bob@example.com"); user.save(); - User foundUser = User.findById(user.getId()); - assertNotNull(foundUser, "User should be found by ID"); - assertEquals("Bob", foundUser.getName()); - assertEquals("bob@example.com", foundUser.getEmail()); + 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 @@ -86,10 +97,14 @@ void testUpdateUser() throws SQLException { user.setEmail("eve.updated@example.com"); user.save(); - User updatedUser = User.findById(user.getId()); - assert updatedUser != null; - assertEquals("Eve Updated", updatedUser.getName()); - assertEquals("eve.updated@example.com", updatedUser.getEmail()); + 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 @@ -99,6 +114,9 @@ void testDeleteUser() throws SQLException { Integer userId = user.getId(); user.delete(); - assertNull(User.findById(userId), "User should be deleted from the database"); + + Optional deletedUserOpt = User.findById(userId); + assertTrue(deletedUserOpt.isEmpty(), "User should be deleted from the database"); } + } diff --git a/database.db b/database.db deleted file mode 100644 index c828abf50816134f6d3e23a2cc2e5325cd4b0c88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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( Date: Fri, 15 Nov 2024 21:36:52 +0200 Subject: [PATCH 8/9] made changes following code review --- active-record/README.md | 164 +++++++++++------- .../java/com/iluwatar/activerecord/User.java | 4 +- .../com/iluwatar/activerecord/UserTest.java | 5 +- 3 files changed, 109 insertions(+), 64 deletions(-) diff --git a/active-record/README.md b/active-record/README.md index f4af0e531e0c..071d38441024 100644 --- a/active-record/README.md +++ b/active-record/README.md @@ -44,30 +44,46 @@ 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; + + /** + * 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 setName(String name) { - this.name = name; + public void setEmail(final String userEmail) { + this.email = userEmail; } - public void setEmail(String email) { - this.email = email; } -} ``` For convenience, we are storing the database configuration logic inside the same User class: @@ -76,26 +92,31 @@ For convenience, we are storing the database configuration logic inside the same // Credentials for in-memory H2 database. - private static final String JDBC_URL = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"; - private static final String USERNAME = "sa"; - private static final String PASSWORD = ""; - + private static final String JDBC_URL = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"; + private static final String USERNAME = "sa"; + private static final String PASSWORD = ""; + + // Establish a database connection. - private static Connection connect() throws SQLException { - return DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD); - } + private static Connection connect() throws SQLException { + return DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD); + } // 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 (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)"; - try (Connection conn = connect(); - Statement stmt = conn.createStatement()) { - stmt.execute(sql); - } - } + 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: @@ -114,7 +135,8 @@ public void save() throws SQLException { sql = "UPDATE users SET name = ?, email = ? WHERE id = ?"; } try (Connection conn = connect(); - PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + PreparedStatement pstmt = conn.prepareStatement( + sql, Statement.RETURN_GENERATED_KEYS)) { pstmt.setString(1, this.name); pstmt.setString(2, this.email); if (this.id != null) { @@ -135,17 +157,22 @@ public void save() throws SQLException { * Find a user by ID. */ -public static User findById(int id) throws SQLException { +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 new User(rs.getInt("id"), rs.getString("name"), rs.getString("email")); + 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 null; + return Optional.empty(); } /** * Get all users. @@ -158,7 +185,10 @@ public static List findAll() throws SQLException { 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"))); + users.add(new User( + rs.getInt("id"), + rs.getString("name"), + rs.getString("email"))); } } return users; @@ -188,45 +218,59 @@ 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 + // 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"); + User user1 = new User( + null, + "John Doe", + "john.doe@example.com"); user1.save(); - LOGGER.info("New user saved: {} with ID {}", user1.getName(), 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) { - LOGGER.info("User found: {} with email {}", foundUser.getName(), foundUser.getEmail()); - } else { - LOGGER.info("User not found."); - } + 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 - 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()); + 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()); + 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); - } + 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); @@ -237,14 +281,14 @@ public static void main(final String[] args) { 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! +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 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 099e468de7dd..2193c84ba70f 100644 --- a/active-record/src/main/java/com/iluwatar/activerecord/User.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/User.java @@ -239,7 +239,7 @@ public String getName() { * @param userName the name to set for the user */ public void setName(final String userName) { - this.name = name; + this.name = userName; } /** @@ -257,6 +257,6 @@ public String getEmail() { * @param userEmail the email address to set for the user */ public void setEmail(final String userEmail) { - this.email = email; + this.email = userEmail; } } 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 4f8f24d584d2..eadb1e4c3a30 100644 --- a/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java +++ b/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java @@ -86,6 +86,8 @@ void testFindAll() throws SQLException { 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 @@ -118,5 +120,4 @@ void testDeleteUser() throws SQLException { Optional deletedUserOpt = User.findById(userId); assertTrue(deletedUserOpt.isEmpty(), "User should be deleted from the database"); } - -} +} \ No newline at end of file From 6ea426ed1ccd8bdfc97f584c6bc56831bce3d4c8 Mon Sep 17 00:00:00 2001 From: ipaulaandreea Date: Fri, 15 Nov 2024 22:50:34 +0200 Subject: [PATCH 9/9] removed db password --- active-record/README.md | 6 ++---- .../main/java/com/iluwatar/activerecord/User.java | 13 +------------ .../java/com/iluwatar/activerecord/UserTest.java | 4 +--- 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/active-record/README.md b/active-record/README.md index 071d38441024..9f1bf27f8cad 100644 --- a/active-record/README.md +++ b/active-record/README.md @@ -93,14 +93,12 @@ For convenience, we are storing the database configuration logic inside the same // Credentials for in-memory H2 database. private static final String JDBC_URL = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"; - private static final String USERNAME = "sa"; - private static final String PASSWORD = ""; - + // Establish a database connection. private static Connection connect() throws SQLException { - return DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD); + return DriverManager.getConnection(JDBC_URL); } // Initialize the table (required each time program runs 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 2193c84ba70f..4f8b77837f9f 100644 --- a/active-record/src/main/java/com/iluwatar/activerecord/User.java +++ b/active-record/src/main/java/com/iluwatar/activerecord/User.java @@ -24,7 +24,6 @@ * THE SOFTWARE. */ package com.iluwatar.activerecord; - import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; @@ -48,16 +47,6 @@ public class User { */ private static final String JDBC_URL = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"; - /** - * Database username. - */ - private static final String USERNAME = "sa"; - - /** - * Database password. - */ - private static final String PASSWORD = ""; - /** * User ID. */ @@ -97,7 +86,7 @@ public User( */ private static Connection connect() throws SQLException { - return DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD); + return DriverManager.getConnection(JDBC_URL); } 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 eadb1e4c3a30..369b54913c9d 100644 --- a/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java +++ b/active-record/src/test/java/com/iluwatar/activerecord/UserTest.java @@ -37,8 +37,6 @@ class UserTest { private static final String JDBC_URL = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"; - private static final String USERNAME = "sa"; - private static final String PASSWORD = ""; @BeforeAll static void setupDatabase() throws SQLException { @@ -48,7 +46,7 @@ static void setupDatabase() throws SQLException { @BeforeEach void clearDatabase() throws SQLException { // Clean up table before each test - try (Connection conn = DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD); + try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) { stmt.execute("DELETE FROM users"); }