diff --git a/metadata-mapping/README.md b/metadata-mapping/README.md new file mode 100644 index 0000000000000000000000000000000000000000..5dcd932a760e2d005d1b5c4f95211dba43c83261 --- /dev/null +++ b/metadata-mapping/README.md @@ -0,0 +1,182 @@ +--- +layout: pattern +title: Metadata Mapping +folder: metadata-mapping +permalink: /patterns/metadata-mapping/ +categories: Architectural +language: en +tags: + - Data access +--- + +## Intent + +Holds details of object-relational mapping in the metadata. + +## Explanation + +Real world example + +> Hibernate ORM Tool uses Metadata Mapping Pattern to specify the mapping between classes and tables either using XML or annotations in code. + +In plain words + +> Metadata Mapping specifies the mapping between classes and tables so that we could treat a table of any database like a Java class. + +Wikipedia says + +> Create a "virtual [object database](https://en.wikipedia.org/wiki/Object_database)" that can be used from within the programming language. + +**Programmatic Example** + +We give an example about visiting the information of `USER` table in `h2` database. Firstly, we create `USER` table with `h2`: + +```java +@Slf4j +public class DatabaseUtil { + private static final String DB_URL = "jdbc:h2:mem:metamapping"; + private static final String CREATE_SCHEMA_SQL = "DROP TABLE IF EXISTS `user`;" + + "CREATE TABLE `user` (\n" + + " `id` int(11) NOT NULL AUTO_INCREMENT,\n" + + " `username` varchar(255) NOT NULL,\n" + + " `password` varchar(255) NOT NULL,\n" + + " PRIMARY KEY (`id`)\n" + + ");"; + + /** + * Create database. + */ + static { + LOGGER.info("create h2 database"); + var source = new JdbcDataSource(); + source.setURL(DB_URL); + try (var statement = source.getConnection().createStatement()) { + statement.execute(CREATE_SCHEMA_SQL); + } catch (SQLException e) { + LOGGER.error("unable to create h2 data source", e); + } + } +} +``` + +Correspondingly, here's the basic `User` entity. + +```java +@Setter +@Getter +@ToString +public class User { + private Integer id; + private String username; + private String password; + + /** + * Get a user. + * @param username user name + * @param password user password + */ + public User(String username, String password) { + this.username = username; + this.password = password; + } +} +``` + +Then we write a `xml` file to show the mapping between the table and the object: + +```xml + + + + + + + + + + + + +``` + +We use `Hibernate` to resolve the mapping and connect to our database, here's its configuration: + +```xml + + + + + + jdbc:h2:mem:metamapping + org.h2.Driver + + 1 + + org.hibernate.dialect.H2Dialect + + false + + create-drop + + + +``` + +Then we can get access to the table just like an object with `Hibernate`, here's some CRUDs: + +```java +@Slf4j +public class UserService { + private static final SessionFactory factory = HibernateUtil.getSessionFactory(); + + /** + * List all users. + * @return list of users + */ + public List listUser() { + LOGGER.info("list all users."); + List users = new ArrayList<>(); + try (var session = factory.openSession()) { + var tx = session.beginTransaction(); + List userIter = session.createQuery("FROM User").list(); + for (var iterator = userIter.iterator(); iterator.hasNext();) { + users.add(iterator.next()); + } + tx.commit(); + } catch (HibernateException e) { + LOGGER.debug("fail to get users", e); + } + return users; + } + + // other CRUDs -> + ... + + public void close() { + HibernateUtil.shutdown(); + } +} +``` + +## Class diagram + +![metamapping](etc/metamapping.png) + +## Applicability + +Use the Metadata Mapping when: + +- you want reduce the amount of work needed to handle database mapping. + +## Known uses + +[Hibernate](https://hibernate.org/), [EclipseLink](https://www.eclipse.org/eclipselink/), [MyBatis](https://blog.mybatis.org/)...... + +## Credits + +- [J2EE Design Patterns](https://www.amazon.com/gp/product/0596004273/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0596004273&linkCode=as2&tag=javadesignpat-20&linkId=48d37c67fb3d845b802fa9b619ad8f31) + diff --git a/metadata-mapping/etc/metamapping.png b/metadata-mapping/etc/metamapping.png new file mode 100644 index 0000000000000000000000000000000000000000..b1e89f8af56f44afafb5d272a526d730797d7b39 Binary files /dev/null and b/metadata-mapping/etc/metamapping.png differ diff --git a/metadata-mapping/etc/metamapping.puml b/metadata-mapping/etc/metamapping.puml new file mode 100644 index 0000000000000000000000000000000000000000..daa8c37de36e781f47d1fa979b91ade30388fd68 --- /dev/null +++ b/metadata-mapping/etc/metamapping.puml @@ -0,0 +1,32 @@ +@startuml +interface com.iluwatar.metamapping.service.UserService { ++ List listUser() ++ int createUser(User) ++ void updateUser(Integer,User) ++ void deleteUser(Integer) ++ User getUser(Integer) ++ void close() +} +class com.iluwatar.metamapping.utils.DatabaseUtil { ++ {static} void createDataSource() +} +class com.iluwatar.metamapping.model.User { +- Integer id +- String username +- String password ++ User(String username, String password) +} +class com.iluwatar.metamapping.utils.HibernateUtil { ++ {static} SessionFactory getSessionFactory() ++ {static} void shutdown() +} +class com.iluwatar.metamapping.App { ++ {static} void main(String[]) ++ {static} List generateSampleUsers() +} + +com.iluwatar.metamapping.service.UserService <.. com.iluwatar.metamapping.App +com.iluwatar.metamapping.model.User <.. com.iluwatar.metamapping.service.UserService +com.iluwatar.metamapping.utils.HibernateUtil <.. com.iluwatar.metamapping.service.UserService +com.iluwatar.metamapping.utils.DatabaseUtil <-- com.iluwatar.metamapping.utils.HibernateUtil +@enduml \ No newline at end of file diff --git a/metadata-mapping/pom.xml b/metadata-mapping/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..43c9621dfacacdabe5cff9ac402ab7d9633cdb6e --- /dev/null +++ b/metadata-mapping/pom.xml @@ -0,0 +1,87 @@ + + + + + java-design-patterns + com.iluwatar + 1.26.0-SNAPSHOT + + 4.0.0 + + metadata-mapping + + + org.junit.jupiter + junit-jupiter-engine + test + + + com.h2database + h2 + + + org.hibernate + hibernate-core + + + com.h2database + h2 + + + javax.xml.bind + jaxb-api + + + com.sun.xml.bind + jaxb-impl + + + com.sun.istack + istack-commons-runtime + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.metamapping.App + + + + + + + + + \ No newline at end of file diff --git a/metadata-mapping/src/main/java/com/iluwatar/metamapping/App.java b/metadata-mapping/src/main/java/com/iluwatar/metamapping/App.java new file mode 100644 index 0000000000000000000000000000000000000000..ff0377590dcdc60c6510921e8815be4aee3aa9f7 --- /dev/null +++ b/metadata-mapping/src/main/java/com/iluwatar/metamapping/App.java @@ -0,0 +1,72 @@ +package com.iluwatar.metamapping; + +import com.iluwatar.metamapping.model.User; +import com.iluwatar.metamapping.service.UserService; +import com.iluwatar.metamapping.utils.DatabaseUtil; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.service.ServiceRegistry; + +/** + * Metadata Mapping specifies the mapping + * between classes and tables so that + * we could treat a table of any database like a Java class. + * + *

With hibernate, we achieve list/create/update/delete/get operations: + * 1)Create the H2 Database in {@link DatabaseUtil}. + * 2)Hibernate resolve hibernate.cfg.xml and generate service like save/list/get/delete. + * For learning metadata mapping pattern, we go deeper into Hibernate here: + * a)read properties from hibernate.cfg.xml and mapping from *.hbm.xml + * b)create session factory to generate session interacting with database + * c)generate session with factory pattern + * d)create query object or use basic api with session, + * hibernate will convert all query to database query according to metadata + * 3)We encapsulate hibernate service in {@link UserService} for our use. + * @see org.hibernate.cfg.Configuration#configure(String) + * @see org.hibernate.cfg.Configuration#buildSessionFactory(ServiceRegistry) + * @see org.hibernate.internal.SessionFactoryImpl#openSession() + */ +@Slf4j +public class App { + /** + * Program entry point. + * + * @param args command line args. + * @throws Exception if any error occurs. + */ + public static void main(String[] args) throws Exception { + // get service + var userService = new UserService(); + // use create service to add users + for (var user: generateSampleUsers()) { + var id = userService.createUser(user); + LOGGER.info("Add user" + user + "at" + id + "."); + } + // use list service to get users + var users = userService.listUser(); + LOGGER.info(String.valueOf(users)); + // use get service to get a user + var user = userService.getUser(1); + LOGGER.info(String.valueOf(user)); + // change password of user 1 + user.setPassword("new123"); + // use update service to update user 1 + userService.updateUser(1, user); + // use delete service to delete user 2 + userService.deleteUser(2); + // close service + userService.close(); + } + + /** + * Generate users. + * + * @return list of users. + */ + public static List generateSampleUsers() { + final var user1 = new User("ZhangSan", "zhs123"); + final var user2 = new User("LiSi", "ls123"); + final var user3 = new User("WangWu", "ww123"); + return List.of(user1, user2, user3); + } +} \ No newline at end of file diff --git a/metadata-mapping/src/main/java/com/iluwatar/metamapping/model/User.java b/metadata-mapping/src/main/java/com/iluwatar/metamapping/model/User.java new file mode 100644 index 0000000000000000000000000000000000000000..0bf2575b151c003a17375bdcc7d487732c437c0e --- /dev/null +++ b/metadata-mapping/src/main/java/com/iluwatar/metamapping/model/User.java @@ -0,0 +1,29 @@ +package com.iluwatar.metamapping.model; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * User Entity. + */ +@Setter +@Getter +@ToString +public class User { + private Integer id; + private String username; + private String password; + + public User() {} + + /** + * Get a user. + * @param username user name + * @param password user password + */ + public User(String username, String password) { + this.username = username; + this.password = password; + } +} \ No newline at end of file diff --git a/metadata-mapping/src/main/java/com/iluwatar/metamapping/service/UserService.java b/metadata-mapping/src/main/java/com/iluwatar/metamapping/service/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..1f85be0d51b3925dd0c327c2de49c741834bf1a3 --- /dev/null +++ b/metadata-mapping/src/main/java/com/iluwatar/metamapping/service/UserService.java @@ -0,0 +1,114 @@ +package com.iluwatar.metamapping.service; + +import com.iluwatar.metamapping.model.User; +import com.iluwatar.metamapping.utils.HibernateUtil; +import java.util.ArrayList; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.HibernateException; +import org.hibernate.SessionFactory; + +/** + * Service layer for user. + */ +@Slf4j +public class UserService { + private static final SessionFactory factory = HibernateUtil.getSessionFactory(); + + /** + * List all users. + * @return list of users + */ + public List listUser() { + LOGGER.info("list all users."); + List users = new ArrayList<>(); + try (var session = factory.openSession()) { + var tx = session.beginTransaction(); + List userIter = session.createQuery("FROM User").list(); + for (var iterator = userIter.iterator(); iterator.hasNext();) { + users.add(iterator.next()); + } + tx.commit(); + } catch (HibernateException e) { + LOGGER.debug("fail to get users", e); + } + return users; + } + + /** + * Add a user. + * @param user user entity + * @return user id + */ + public int createUser(User user) { + LOGGER.info("create user: " + user.getUsername()); + var id = -1; + try (var session = factory.openSession()) { + var tx = session.beginTransaction(); + id = (Integer) session.save(user); + tx.commit(); + } catch (HibernateException e) { + LOGGER.debug("fail to create user", e); + } + LOGGER.info("create user " + user.getUsername() + " at " + id); + return id; + } + + /** + * Update user. + * @param id user id + * @param user new user entity + */ + public void updateUser(Integer id, User user) { + LOGGER.info("update user at " + id); + try (var session = factory.openSession()) { + var tx = session.beginTransaction(); + user.setId(id); + session.update(user); + tx.commit(); + } catch (HibernateException e) { + LOGGER.debug("fail to update user", e); + } + } + + /** + * Delete user. + * @param id user id + */ + public void deleteUser(Integer id) { + LOGGER.info("delete user at: " + id); + try (var session = factory.openSession()) { + var tx = session.beginTransaction(); + var user = session.get(User.class, id); + session.delete(user); + tx.commit(); + } catch (HibernateException e) { + LOGGER.debug("fail to delete user", e); + } + } + + /** + * Get user. + * @param id user id + * @return deleted user + */ + public User getUser(Integer id) { + LOGGER.info("get user at: " + id); + User user = null; + try (var session = factory.openSession()) { + var tx = session.beginTransaction(); + user = session.get(User.class, id); + tx.commit(); + } catch (HibernateException e) { + LOGGER.debug("fail to get user", e); + } + return user; + } + + /** + * Close hibernate. + */ + public void close() { + HibernateUtil.shutdown(); + } +} \ No newline at end of file diff --git a/metadata-mapping/src/main/java/com/iluwatar/metamapping/utils/DatabaseUtil.java b/metadata-mapping/src/main/java/com/iluwatar/metamapping/utils/DatabaseUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..b6d0200e825954262a80c5cb3ef2a6847fd9289f --- /dev/null +++ b/metadata-mapping/src/main/java/com/iluwatar/metamapping/utils/DatabaseUtil.java @@ -0,0 +1,39 @@ +package com.iluwatar.metamapping.utils; + +import java.sql.SQLException; +import lombok.extern.slf4j.Slf4j; +import org.h2.jdbcx.JdbcDataSource; + +/** + * Create h2 database. + */ +@Slf4j +public class DatabaseUtil { + private static final String DB_URL = "jdbc:h2:mem:metamapping"; + private static final String CREATE_SCHEMA_SQL = "DROP TABLE IF EXISTS `user`;" + + "CREATE TABLE `user` (\n" + + " `id` int(11) NOT NULL AUTO_INCREMENT,\n" + + " `username` varchar(255) NOT NULL,\n" + + " `password` varchar(255) NOT NULL,\n" + + " PRIMARY KEY (`id`)\n" + + ");"; + + /** + * Hide constructor. + */ + private DatabaseUtil() {} + + /** + * Create database. + */ + static { + LOGGER.info("create h2 database"); + var source = new JdbcDataSource(); + source.setURL(DB_URL); + try (var statement = source.getConnection().createStatement()) { + statement.execute(CREATE_SCHEMA_SQL); + } catch (SQLException e) { + LOGGER.error("unable to create h2 data source", e); + } + } +} \ No newline at end of file diff --git a/metadata-mapping/src/main/java/com/iluwatar/metamapping/utils/HibernateUtil.java b/metadata-mapping/src/main/java/com/iluwatar/metamapping/utils/HibernateUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..ba405eb744504a21feda6b675854e2c4f133cfcb --- /dev/null +++ b/metadata-mapping/src/main/java/com/iluwatar/metamapping/utils/HibernateUtil.java @@ -0,0 +1,45 @@ +package com.iluwatar.metamapping.utils; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.SessionFactory; +import org.hibernate.cfg.Configuration; + +/** + * Manage hibernate. + */ +@Slf4j +public class HibernateUtil { + + private static final SessionFactory sessionFactory = buildSessionFactory(); + + /** + * Hide constructor. + */ + private HibernateUtil() {} + + /** + * Build session factory. + * @return session factory + */ + private static SessionFactory buildSessionFactory() { + // Create the SessionFactory from hibernate.cfg.xml + return new Configuration().configure().buildSessionFactory(); + } + + /** + * Get session factory. + * @return session factory + */ + public static SessionFactory getSessionFactory() { + return sessionFactory; + } + + /** + * Close session factory. + */ + public static void shutdown() { + // Close caches and connection pools + getSessionFactory().close(); + } + +} \ No newline at end of file diff --git a/metadata-mapping/src/main/resources/com/iluwatar/metamapping/model/User.hbm.xml b/metadata-mapping/src/main/resources/com/iluwatar/metamapping/model/User.hbm.xml new file mode 100644 index 0000000000000000000000000000000000000000..cd63c552d65d4b6bbd4670a2abe4723d7735b16e --- /dev/null +++ b/metadata-mapping/src/main/resources/com/iluwatar/metamapping/model/User.hbm.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/metadata-mapping/src/main/resources/hibernate.cfg.xml b/metadata-mapping/src/main/resources/hibernate.cfg.xml new file mode 100644 index 0000000000000000000000000000000000000000..18dc198e84ba3d89799b0e9f2fae770a61ffd0d7 --- /dev/null +++ b/metadata-mapping/src/main/resources/hibernate.cfg.xml @@ -0,0 +1,20 @@ + + + + + + jdbc:h2:mem:metamapping + org.h2.Driver + + 1 + + org.hibernate.dialect.H2Dialect + + false + + create-drop + + + \ No newline at end of file diff --git a/metadata-mapping/src/test/java/com/iluwatar/metamapping/AppTest.java b/metadata-mapping/src/test/java/com/iluwatar/metamapping/AppTest.java new file mode 100644 index 0000000000000000000000000000000000000000..127ddad0f94fedccb9853c91e59f964ecce53385 --- /dev/null +++ b/metadata-mapping/src/test/java/com/iluwatar/metamapping/AppTest.java @@ -0,0 +1,20 @@ +package com.iluwatar.metamapping; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +/** + * Tests that metadata mapping example runs without errors. + */ +class AppTest { + /** + * Issue: Add at least one assertion to this test case. + * + * Solution: Inserted assertion to check whether the execution of the main method in {@link App#main(String[])} + * throws an exception. + */ + @Test + void shouldExecuteMetaMappingWithoutException() { + assertDoesNotThrow(() -> App.main(new String[]{})); + } +} diff --git a/pom.xml b/pom.xml index 5c3634b95aa217db13f9ecadd251065c2b975676..e1573a7377676d292d36c57f929730776d73e052 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,7 @@ 3.0 1.4.8 2.7 + 4.0.1 https://sonarcloud.io iluwatar @@ -227,6 +228,7 @@ lockable-object fanout-fanin domain-model + metadata-mapping @@ -377,6 +379,11 @@ commons-io ${commons-io.version} + + com.sun.istack + istack-commons-runtime + ${istack-commons-runtime.version} +