Repository: iluwatar/java-design-patterns
Issue: Table Module pattern
Labels: epic: pattern
type: feature

https://inviqa.com/blog/architecture-patterns-domain-model-and-friends

Issue comments:
Could I take this？ @iluwatar 
Sure thing @tao-sun2 

Please mention a timeline for when can we expect a PR against this issue.
I will finish this issue within a week.

Pull request review comments:
```suggestion
categories: Structural
```
Is there console output that we could show here?
```suggestion
- The application only uses a few shared common table-oriented data structures.
```
Add a comment to describe what happens in this section
Could we use try-with-resources statement for automatic closing? Check elsewhere too.
Having a separate class for a couple of constants feels unnecessary. Could we combine this to `UserTableModule`?

Patch that resolved the issue:
diff --git a/pom.xml b/pom.xml
index 1b8f30e92b83..8173972bd27a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -225,6 +225,7 @@
         <module>active-object</module>
         <module>model-view-viewmodel</module>
         <module>composite-entity</module>
+        <module>table-module</module>
         <module>presentation</module>
         <module>lockable-object</module>
     </modules>
diff --git a/table-module/README.md b/table-module/README.md
new file mode 100644
index 000000000000..318a2694da26
--- /dev/null
+++ b/table-module/README.md
@@ -0,0 +1,134 @@
+---
+layout: pattern
+title: Table Module
+folder: table-module
+permalink: /patterns/table-module/
+categories: Structural
+tags:
+ - Data access
+---
+## Intent
+Table Module organizes domain logic with one class per table in the database, and a single instance of a class contains the various procedures that will act on the data.
+
+## Explanation
+
+Real world example
+
+> When dealing with a user system, we need some operations on the user table. We can use the table module pattern in this scenario. We can create a class named UserTableModule and initialize a instance of that class to handle the business logic for all rows in the user table.
+
+In plain words
+
+> A single instance that handles the business logic for all rows in a database table or view.
+
+Programmatic Example
+
+In the example of the user system, we need to deal with the domain logic of user login and user registration. We can use the table module pattern and create an instance of the class `UserTableModule` to handle the business logic for all rows in the user table.
+
+Here is the basic `User` entity.
+
+```java
+@Setter
+@Getter
+@ToString
+@EqualsAndHashCode
+@AllArgsConstructor
+public class User {
+  private int id;
+  private String username;
+  private String password;
+}
+```
+
+Here is the `UserTableModule` class.
+
+```java
+public class UserTableModule {
+  private final DataSource dataSource;
+  private Connection connection = null;
+  private ResultSet resultSet = null;
+  private PreparedStatement preparedStatement = null;
+
+  public UserTableModule(final DataSource userDataSource) {
+    this.dataSource = userDataSource;
+  }
+  
+  /**
+   * Login using username and password.
+   *
+   * @param username the username of a user
+   * @param password the password of a user
+   * @return the execution result of the method
+   * @throws SQLException if any error
+   */
+  public int login(final String username, final String password) throws SQLException {
+  		// Method implementation.
+
+  }
+
+  /**
+   * Register a new user.
+   *
+   * @param user a user instance
+   * @return the execution result of the method
+   * @throws SQLException if any error
+   */
+  public int registerUser(final User user) throws SQLException {
+  		// Method implementation.
+  }
+}
+```
+
+In the class `App`, we use an instance of the `UserTableModule` to handle user login and registration.
+
+```java
+// Create data source and create the user table.
+final var dataSource = createDataSource();
+createSchema(dataSource);
+userTableModule = new UserTableModule(dataSource);
+
+//Initialize two users.
+var user1 = new User(1, "123456", "123456");
+var user2 = new User(2, "test", "password");
+
+//Login and register using the instance of userTableModule.
+userTableModule.registerUser(user1);
+userTableModule.login(user1.getUsername(), user1.getPassword());
+userTableModule.login(user2.getUsername(), user2.getPassword());
+userTableModule.registerUser(user2);
+userTableModule.login(user2.getUsername(), user2.getPassword());
+
+deleteSchema(dataSource);
+```
+
+The program output:
+
+```java
+12:22:13.095 [main] INFO com.iluwatar.tablemodule.UserTableModule - Register successfully!
+12:22:13.117 [main] INFO com.iluwatar.tablemodule.UserTableModule - Login successfully!
+12:22:13.128 [main] INFO com.iluwatar.tablemodule.UserTableModule - Fail to login!
+12:22:13.136 [main] INFO com.iluwatar.tablemodule.UserTableModule - Register successfully!
+12:22:13.144 [main] INFO com.iluwatar.tablemodule.UserTableModule - Login successfully!
+```
+
+## Class diagram
+
+![](./etc/table-module.urm.png "table module")
+
+## Applicability
+
+Use the Table Module Pattern when
+
+- Domain logic is simple and data is in tabular form.
+- The application only uses a few shared common table-oriented data structures.
+
+## Related patterns
+
+- [Transaction Script](https://java-design-patterns.com/patterns/transaction-script/)
+
+- Domain Model
+
+## Credits
+
+* [Table Module Pattern](http://wiki3.cosc.canterbury.ac.nz/index.php/Table_module_pattern)
+* [Patterns of Enterprise Application Architecture](https://www.amazon.com/gp/product/0321127420/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=0321127420&linkId=18acc13ba60d66690009505577c45c04)
+* [Architecture patterns: domain model and friends](https://inviqa.com/blog/architecture-patterns-domain-model-and-friends)
\ No newline at end of file
diff --git a/table-module/etc/table-module.urm.puml b/table-module/etc/table-module.urm.puml
new file mode 100644
index 000000000000..6cf75797f080
--- /dev/null
+++ b/table-module/etc/table-module.urm.puml
@@ -0,0 +1,38 @@
+@startuml
+package com.iluwatar.tablemodule {
+  class App {
+    - DB_URL : String {static}
+    - LOGGER : Logger {static}
+    - App()
+    - createDataSource() : DataSource {static}
+    - createSchema(dataSource : DataSource) {static}
+    - deleteSchema(dataSource : DataSource) {static}
+    + main(args : String[]) {static}
+  }
+  class User {
+    - id : int
+    - password : String
+    - username : String
+    + User(id : int, username : String, password : String)
+    # canEqual(other : Object) : boolean
+    + equals(o : Object) : boolean
+    + getId() : int
+    + getPassword() : String
+    + getUsername() : String
+    + hashCode() : int
+    + setId(id : int)
+    + setPassword(password : String)
+    + setUsername(username : String)
+    + toString() : String
+  }
+  class UserTableModule {
+    + CREATE_SCHEMA_SQL : String {static}
+    + DELETE_SCHEMA_SQL : String {static}
+    - LOGGER : Logger {static}
+    - dataSource : DataSource
+    + UserTableModule(userDataSource : DataSource)
+    + login(username : String, password : String) : int
+    + registerUser(user : User) : int
+  }
+}
+@enduml
\ No newline at end of file
diff --git a/table-module/pom.xml b/table-module/pom.xml
new file mode 100644
index 000000000000..eae698c0909e
--- /dev/null
+++ b/table-module/pom.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    The MIT License
+    Copyright © 2014-2021 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.
+
+-->
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>java-design-patterns</artifactId>
+        <groupId>com.iluwatar</groupId>
+        <version>1.25.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>table-module</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <configuration>
+                            <archive>
+                                <manifest>
+                                    <mainClass>com.iluwatar.tablemodule.App</mainClass>
+                                </manifest>
+                            </archive>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/table-module/src/main/java/com/iluwatar/tablemodule/App.java b/table-module/src/main/java/com/iluwatar/tablemodule/App.java
new file mode 100644
index 000000000000..7b22666e08eb
--- /dev/null
+++ b/table-module/src/main/java/com/iluwatar/tablemodule/App.java
@@ -0,0 +1,81 @@
+package com.iluwatar.tablemodule;
+
+import java.sql.SQLException;
+import javax.sql.DataSource;
+
+import lombok.extern.slf4j.Slf4j;
+import org.h2.jdbcx.JdbcDataSource;
+
+
+/**
+ * Table Module pattern is a domain logic pattern.
+ * In Table Module a single class encapsulates all the domain logic for all
+ * records stored in a table or view. It's important to note that there is no
+ * translation of data between objects and rows, as it happens in Domain Model,
+ * hence implementation is relatively simple when compared to the Domain
+ * Model pattern.
+ *
+ * <p>In this example we will use the Table Module pattern to implement register
+ * and login methods for the records stored in the user table. The main
+ * method will initialise an instance of {@link UserTableModule} and use it to
+ * handle the domain logic for the user table.</p>
+ */
+@Slf4j
+public final class App {
+  private static final String DB_URL = "jdbc:h2:~/test";
+
+  /**
+   * Private constructor.
+   */
+  private App() {
+
+  }
+
+  /**
+   * Program entry point.
+   *
+   * @param args command line args.
+   * @throws SQLException if any error occurs.
+   */
+  public static void main(final String[] args) throws SQLException {
+    // Create data source and create the user table.
+    final var dataSource = createDataSource();
+    createSchema(dataSource);
+    var userTableModule = new UserTableModule(dataSource);
+
+    // Initialize two users.
+    var user1 = new User(1, "123456", "123456");
+    var user2 = new User(2, "test", "password");
+
+    // Login and register using the instance of userTableModule.
+    userTableModule.registerUser(user1);
+    userTableModule.login(user1.getUsername(), user1.getPassword());
+    userTableModule.login(user2.getUsername(), user2.getPassword());
+    userTableModule.registerUser(user2);
+    userTableModule.login(user2.getUsername(), user2.getPassword());
+
+    deleteSchema(dataSource);
+  }
+
+  private static void deleteSchema(final DataSource dataSource)
+          throws SQLException {
+    try (var connection = dataSource.getConnection();
+         var statement = connection.createStatement()) {
+      statement.execute(UserTableModule.DELETE_SCHEMA_SQL);
+    }
+  }
+
+  private static void createSchema(final DataSource dataSource)
+          throws SQLException {
+    try (var connection = dataSource.getConnection();
+         var statement = connection.createStatement()) {
+      statement.execute(UserTableModule.CREATE_SCHEMA_SQL);
+    }
+  }
+
+  private static DataSource createDataSource() {
+    var dataSource = new JdbcDataSource();
+    dataSource.setURL(DB_URL);
+    return dataSource;
+  }
+}
diff --git a/table-module/src/main/java/com/iluwatar/tablemodule/User.java b/table-module/src/main/java/com/iluwatar/tablemodule/User.java
new file mode 100644
index 000000000000..ac307d4503b3
--- /dev/null
+++ b/table-module/src/main/java/com/iluwatar/tablemodule/User.java
@@ -0,0 +1,23 @@
+package com.iluwatar.tablemodule;
+
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+
+/**
+ * A user POJO that represents the data that will be read from the data source.
+ */
+@Setter
+@Getter
+@ToString
+@EqualsAndHashCode
+@AllArgsConstructor
+public class User {
+  private int id;
+  private String username;
+  private String password;
+
+}
diff --git a/table-module/src/main/java/com/iluwatar/tablemodule/UserTableModule.java b/table-module/src/main/java/com/iluwatar/tablemodule/UserTableModule.java
new file mode 100644
index 000000000000..f1a4d044454d
--- /dev/null
+++ b/table-module/src/main/java/com/iluwatar/tablemodule/UserTableModule.java
@@ -0,0 +1,96 @@
+package com.iluwatar.tablemodule;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import javax.sql.DataSource;
+
+import lombok.extern.slf4j.Slf4j;
+
+
+/**
+ * This class organizes domain logic with the user table in the
+ * database. A single instance of this class contains the various
+ * procedures that will act on the data.
+ */
+@Slf4j
+public class UserTableModule {
+  /**
+   * Public element for creating schema.
+   */
+  public static final String CREATE_SCHEMA_SQL =
+          "CREATE TABLE IF NOT EXISTS USERS (ID NUMBER, USERNAME VARCHAR(30) "
+                  + "UNIQUE,PASSWORD VARCHAR(30))";
+  /**
+   * Public element for deleting schema.
+   */
+  public static final String DELETE_SCHEMA_SQL = "DROP TABLE USERS IF EXISTS";
+  private final DataSource dataSource;
+
+
+  /**
+   * Public constructor.
+   *
+   * @param userDataSource the data source in the database
+   */
+  public UserTableModule(final DataSource userDataSource) {
+    this.dataSource = userDataSource;
+  }
+
+
+  /**
+   * Login using username and password.
+   *
+   * @param username the username of a user
+   * @param password the password of a user
+   * @return the execution result of the method
+   * @throws SQLException if any error
+   */
+  public int login(final String username, final String password)
+          throws SQLException {
+    var sql = "select count(*) from USERS where username=? and password=?";
+    ResultSet resultSet = null;
+    try (var connection = dataSource.getConnection();
+         var preparedStatement =
+                 connection.prepareStatement(sql)
+    ) {
+      var result = 0;
+      preparedStatement.setString(1, username);
+      preparedStatement.setString(2, password);
+      resultSet = preparedStatement.executeQuery();
+      while (resultSet.next()) {
+        result = resultSet.getInt(1);
+      }
+      if (result == 1) {
+        LOGGER.info("Login successfully!");
+      } else {
+        LOGGER.info("Fail to login!");
+      }
+      return result;
+    } finally {
+      if (resultSet != null) {
+        resultSet.close();
+      }
+    }
+  }
+
+  /**
+   * Register a new user.
+   *
+   * @param user a user instance
+   * @return the execution result of the method
+   * @throws SQLException if any error
+   */
+  public int registerUser(final User user) throws SQLException {
+    var sql = "insert into USERS (username, password) values (?,?)";
+    try (var connection = dataSource.getConnection();
+         var preparedStatement =
+                 connection.prepareStatement(sql)
+    ) {
+      preparedStatement.setString(1, user.getUsername());
+      preparedStatement.setString(2, user.getPassword());
+      var result = preparedStatement.executeUpdate();
+      LOGGER.info("Register successfully!");
+      return result;
+    }
+  }
+}


Test patch:
diff --git a/table-module/src/test/java/com/iluwatar/tablemodule/AppTest.java b/table-module/src/test/java/com/iluwatar/tablemodule/AppTest.java
new file mode 100644
index 000000000000..cafae7cdbe38
--- /dev/null
+++ b/table-module/src/test/java/com/iluwatar/tablemodule/AppTest.java
@@ -0,0 +1,16 @@
+package com.iluwatar.tablemodule;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+/**
+ * Tests that the table module example runs without errors.
+ */
+class AppTest {
+
+  @Test
+  void shouldExecuteWithoutException() {
+    assertDoesNotThrow(() -> App.main(new String[]{}));
+  }
+}
diff --git a/table-module/src/test/java/com/iluwatar/tablemodule/UserTableModuleTest.java b/table-module/src/test/java/com/iluwatar/tablemodule/UserTableModuleTest.java
new file mode 100644
index 000000000000..86685f58dd9c
--- /dev/null
+++ b/table-module/src/test/java/com/iluwatar/tablemodule/UserTableModuleTest.java
@@ -0,0 +1,78 @@
+package com.iluwatar.tablemodule;
+
+import org.h2.jdbcx.JdbcDataSource;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import javax.sql.DataSource;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class UserTableModuleTest {
+  private static final String DB_URL = "jdbc:h2:~/test";
+
+  private static DataSource createDataSource() {
+    var dataSource = new JdbcDataSource();
+    dataSource.setURL(DB_URL);
+    return dataSource;
+  }
+
+  @BeforeEach
+  void setUp() throws SQLException {
+    try (var connection = DriverManager.getConnection(DB_URL);
+         var statement = connection.createStatement()) {
+      statement.execute(UserTableModule.DELETE_SCHEMA_SQL);
+      statement.execute(UserTableModule.CREATE_SCHEMA_SQL);
+    }
+  }
+
+  @AfterEach
+  void tearDown() throws SQLException {
+    try (var connection = DriverManager.getConnection(DB_URL);
+         var statement = connection.createStatement()) {
+      statement.execute(UserTableModule.DELETE_SCHEMA_SQL);
+    }
+  }
+
+  @Test
+  void loginShouldFail() throws SQLException {
+    var dataSource = createDataSource();
+    var userTableModule = new UserTableModule(dataSource);
+    var user = new User(1, "123456", "123456");
+    assertEquals(0, userTableModule.login(user.getUsername(),
+            user.getPassword()));
+  }
+
+  @Test
+  void loginShouldSucceed() throws SQLException {
+    var dataSource = createDataSource();
+    var userTableModule = new UserTableModule(dataSource);
+    var user = new User(1, "123456", "123456");
+    userTableModule.registerUser(user);
+    assertEquals(1, userTableModule.login(user.getUsername(),
+            user.getPassword()));
+  }
+
+  @Test
+  void registerShouldFail() throws SQLException {
+    var dataSource = createDataSource();
+    var userTableModule = new UserTableModule(dataSource);
+    var user = new User(1, "123456", "123456");
+    userTableModule.registerUser(user);
+    assertThrows(SQLException.class, () -> {
+      userTableModule.registerUser(user);
+    });
+  }
+
+  @Test
+  void registerShouldSucceed() throws SQLException {
+    var dataSource = createDataSource();
+    var userTableModule = new UserTableModule(dataSource);
+    var user = new User(1, "123456", "123456");
+    assertEquals(1, userTableModule.registerUser(user));
+  }
+}
\ No newline at end of file
diff --git a/table-module/src/test/java/com/iluwatar/tablemodule/UserTest.java b/table-module/src/test/java/com/iluwatar/tablemodule/UserTest.java
new file mode 100644
index 000000000000..669d86035f01
--- /dev/null
+++ b/table-module/src/test/java/com/iluwatar/tablemodule/UserTest.java
@@ -0,0 +1,131 @@
+package com.iluwatar.tablemodule;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class UserTest {
+  @Test
+  void testCanEqual() {
+    assertFalse((new User(1, "janedoe", "iloveyou"))
+            .canEqual("Other"));
+  }
+
+  @Test
+  void testCanEqual2() {
+    var user = new User(1, "janedoe", "iloveyou");
+    assertTrue(user.canEqual(new User(1, "janedoe",
+            "iloveyou")));
+  }
+
+  @Test
+  void testEquals1() {
+    var user = new User(1, "janedoe", "iloveyou");
+    assertNotEquals("42", user);
+  }
+
+  @Test
+  void testEquals2() {
+    var user = new User(1, "janedoe", "iloveyou");
+    assertEquals(user, new User(1, "janedoe",
+            "iloveyou"));
+  }
+
+  @Test
+  void testEquals3() {
+    var user = new User(123, "janedoe", "iloveyou");
+    assertNotEquals(user, new User(1, "janedoe",
+            "iloveyou"));
+  }
+
+  @Test
+  void testEquals4() {
+    var user = new User(1, null, "iloveyou");
+    assertNotEquals(user, new User(1, "janedoe",
+            "iloveyou"));
+  }
+
+  @Test
+  void testEquals5() {
+    var user = new User(1, "iloveyou", "iloveyou");
+    assertNotEquals(user, new User(1, "janedoe",
+            "iloveyou"));
+  }
+
+  @Test
+  void testEquals6() {
+    var user = new User(1, "janedoe", "janedoe");
+    assertNotEquals(user, new User(1, "janedoe",
+            "iloveyou"));
+  }
+
+  @Test
+  void testEquals7() {
+    var user = new User(1, "janedoe", null);
+    assertNotEquals(user, new User(1, "janedoe",
+            "iloveyou"));
+  }
+
+  @Test
+  void testEquals8() {
+    var user = new User(1, null, "iloveyou");
+    assertEquals(user, new User(1, null,
+            "iloveyou"));
+  }
+
+  @Test
+  void testEquals9() {
+    var user = new User(1, "janedoe", null);
+    assertEquals(user, new User(1, "janedoe",
+            null));
+  }
+
+  @Test
+  void testHashCode1() {
+    assertEquals(-1758941372, (new User(1, "janedoe",
+            "iloveyou")).hashCode());
+
+  }
+
+  @Test
+  void testHashCode2() {
+    assertEquals(-1332207447, (new User(1, null,
+            "iloveyou")).hashCode());
+  }
+
+  @Test
+  void testHashCode3() {
+    assertEquals(-426522485, (new User(1, "janedoe",
+            null)).hashCode());
+  }
+
+  @Test
+  void testSetId() {
+    var user = new User(1, "janedoe", "iloveyou");
+    user.setId(2);
+    assertEquals(2, user.getId());
+  }
+
+  @Test
+  void testSetPassword() {
+    var user = new User(1, "janedoe", "tmp");
+    user.setPassword("iloveyou");
+    assertEquals("iloveyou", user.getPassword());
+  }
+
+  @Test
+  void testSetUsername() {
+    var user = new User(1, "tmp", "iloveyou");
+    user.setUsername("janedoe");
+    assertEquals("janedoe", user.getUsername());
+  }
+
+  @Test
+  void testToString() {
+    var user = new User(1, "janedoe", "iloveyou");
+    assertEquals(String.format("User(id=%s, username=%s, password=%s)",
+            user.getId(), user.getUsername(), user.getPassword()),
+            user.toString());
+  }
+}
+