diff --git a/src/main/java/jdbc/RowMapper.java b/src/main/java/jdbc/RowMapper.java index 1dd7310c0..c44445bf2 100644 --- a/src/main/java/jdbc/RowMapper.java +++ b/src/main/java/jdbc/RowMapper.java @@ -5,5 +5,6 @@ @FunctionalInterface public interface RowMapper { - T mapRow(final ResultSet resultSet) throws SQLException; + T mapRow(final ResultSet resultSet) + throws SQLException, InstantiationException, IllegalAccessException; } diff --git a/src/main/java/persistence/Application.java b/src/main/java/persistence/Application.java index 9c29423ba..33b4833ea 100644 --- a/src/main/java/persistence/Application.java +++ b/src/main/java/persistence/Application.java @@ -7,7 +7,7 @@ import jdbc.RowMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import persistence.sql.QueryTranslator; +import persistence.sql.QueryBuilder; import persistence.sql.ddl.entity.Person; public class Application { @@ -28,19 +28,19 @@ public static void main(String[] args) { final JdbcTemplate jdbcTemplate = new JdbcTemplate(server.getConnection()); - QueryTranslator queryTranslator = new QueryTranslator(); + QueryBuilder queryBuilder = new QueryBuilder(); - jdbcTemplate.execute(queryTranslator.getCreateTableQuery(Person.class)); + jdbcTemplate.execute(queryBuilder.getCreateTableQuery(Person.class)); - executeInitializedQuery(jdbcTemplate, queryTranslator); + executeInitializedQuery(jdbcTemplate, queryBuilder); - querySelectAll(jdbcTemplate, queryTranslator); + querySelectAll(jdbcTemplate, queryBuilder); - querySelectById(jdbcTemplate, queryTranslator); + querySelectById(jdbcTemplate, queryBuilder); - jdbcTemplate.execute(queryTranslator.getDeleteByIdQuery(Person.class, 2L)); + jdbcTemplate.execute(queryBuilder.getDeleteByIdQuery(Person.class, 2L)); - querySelectAll(jdbcTemplate, queryTranslator); + querySelectAll(jdbcTemplate, queryBuilder); server.stop(); } catch (Exception e) { @@ -50,25 +50,25 @@ public static void main(String[] args) { } } - private static void querySelectById(JdbcTemplate jdbcTemplate, QueryTranslator queryTranslator) { + private static void querySelectById(JdbcTemplate jdbcTemplate, QueryBuilder queryBuilder) { Person person = jdbcTemplate.queryForObject( - queryTranslator.getSelectByIdQuery(Person.class, 2L), + queryBuilder.getSelectByIdQuery(Person.class, 2L), rowMapper ); logger.info("Person: {}", person); } - private static void querySelectAll(JdbcTemplate jdbcTemplate, QueryTranslator queryTranslator) { + private static void querySelectAll(JdbcTemplate jdbcTemplate, QueryBuilder queryBuilder) { List persons = jdbcTemplate.query( - queryTranslator.getSelectAllQuery(Person.class), + queryBuilder.getSelectAllQuery(Person.class), rowMapper ); persons.forEach(person -> logger.info("Person: {}", person)); } - private static void executeInitializedQuery(JdbcTemplate jdbcTemplate, QueryTranslator queryTranslator) { + private static void executeInitializedQuery(JdbcTemplate jdbcTemplate, QueryBuilder queryBuilder) { List persons = List.of( new Person("John", 23, "john@gmail.com"), new Person("Smith", 33, "smith@gmail.com"), @@ -76,7 +76,7 @@ private static void executeInitializedQuery(JdbcTemplate jdbcTemplate, QueryTran ); persons.stream() - .map(queryTranslator::getInsertQuery) + .map(queryBuilder::getInsertQuery) .forEach(jdbcTemplate::execute); } diff --git a/src/main/java/persistence/entity/EntityManager.java b/src/main/java/persistence/entity/EntityManager.java new file mode 100644 index 000000000..7f57775b0 --- /dev/null +++ b/src/main/java/persistence/entity/EntityManager.java @@ -0,0 +1,29 @@ +package persistence.entity; + +public interface EntityManager { + /** + * Finds an entity by its id + * + * @param entityClass the entity class + * @param id the id of the entity + * @return the entity + * + * @param the entity type + */ + T find(Class entityClass, Long id); + + /** + * Persists the entity object + * + * @param entity entity object to persist + * @return the persisted entity object + */ + Object persist(Object entity); + + /** + * Removes the entity object + * + * @param entity entity object to remove + */ + void remove(Object entity); +} diff --git a/src/main/java/persistence/entity/EntityRowMapperFactory.java b/src/main/java/persistence/entity/EntityRowMapperFactory.java new file mode 100644 index 000000000..7fd1ce4b3 --- /dev/null +++ b/src/main/java/persistence/entity/EntityRowMapperFactory.java @@ -0,0 +1,54 @@ +package persistence.entity; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.stream.Collectors; +import jdbc.RowMapper; +import persistence.exception.ReflectionRuntimeException; +import persistence.sql.ddl.ColumnQueryBuilder; + +public class EntityRowMapperFactory { + + private EntityRowMapperFactory() { + // Do nothing + } + + public static class CacheEntityRowMapperFactory { + private CacheEntityRowMapperFactory() { + // Do nothing + } + + private static final EntityRowMapperFactory INSTANCE = new EntityRowMapperFactory(); + } + + public static EntityRowMapperFactory getInstance() { + return CacheEntityRowMapperFactory.INSTANCE; + } + + public RowMapper getRowMapper(Class entityClass) { + return resultSet -> { + try { + ColumnQueryBuilder columnQueryBuilder = new ColumnQueryBuilder(); + Constructor declaredConstructor = entityClass.getDeclaredConstructor(); + declaredConstructor.setAccessible(true); + T entity = entityClass.getDeclaredConstructor().newInstance(); + + List columnFieldList = columnQueryBuilder.getColumnFieldStream(entityClass) + .collect(Collectors.toList()); + + for (Field field : columnFieldList) { + field.setAccessible(true); + String columnName = columnQueryBuilder.getColumnNameFrom(field); + field.set(entity, resultSet.getObject(columnName)); + } + + return entity; + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | + InvocationTargetException e) { + throw new ReflectionRuntimeException(entityClass, e); + } + }; + } +} diff --git a/src/main/java/persistence/entity/impl/EntityManagerImpl.java b/src/main/java/persistence/entity/impl/EntityManagerImpl.java new file mode 100644 index 000000000..319e01b86 --- /dev/null +++ b/src/main/java/persistence/entity/impl/EntityManagerImpl.java @@ -0,0 +1,48 @@ +package persistence.entity.impl; + +import jdbc.JdbcTemplate; +import persistence.entity.EntityManager; +import persistence.entity.EntityRowMapperFactory; +import persistence.sql.QueryBuilder; + +public class EntityManagerImpl implements EntityManager { + private final JdbcTemplate jdbcTemplate; + + private final QueryBuilder queryBuilder; + + public EntityManagerImpl(JdbcTemplate jdbcTemplate) { + this(jdbcTemplate, new QueryBuilder()); + } + + public EntityManagerImpl(JdbcTemplate jdbcTemplate, QueryBuilder queryBuilder) { + this.jdbcTemplate = jdbcTemplate; + this.queryBuilder = queryBuilder; + } + + + @Override + public T find(Class entityClass, Long id) { + String selectByIdQuery = queryBuilder.getSelectByIdQuery(entityClass, id); + + return jdbcTemplate.queryForObject( + selectByIdQuery, + EntityRowMapperFactory.getInstance().getRowMapper(entityClass) + ); + } + + @Override + public Object persist(Object entity) { + String insertQuery = queryBuilder.getInsertQuery(entity); + + jdbcTemplate.execute(insertQuery); + + return entity; + } + + @Override + public void remove(Object entity) { + String deleteQueryFromEntity = queryBuilder.getDeleteQueryFromEntity(entity); + + jdbcTemplate.execute(deleteQueryFromEntity); + } +} diff --git a/src/main/java/persistence/exception/ReflectionRuntimeException.java b/src/main/java/persistence/exception/ReflectionRuntimeException.java new file mode 100644 index 000000000..5ef8b726c --- /dev/null +++ b/src/main/java/persistence/exception/ReflectionRuntimeException.java @@ -0,0 +1,7 @@ +package persistence.exception; + +public class ReflectionRuntimeException extends RuntimeException { + public ReflectionRuntimeException(Class clazz, Exception e) { + super("Reflection error on class: " + clazz.getName(), e); + } +} diff --git a/src/main/java/persistence/sql/exception/UnsupportedClassException.java b/src/main/java/persistence/exception/UnsupportedClassException.java similarity index 84% rename from src/main/java/persistence/sql/exception/UnsupportedClassException.java rename to src/main/java/persistence/exception/UnsupportedClassException.java index b4b143027..2d5bc48e3 100644 --- a/src/main/java/persistence/sql/exception/UnsupportedClassException.java +++ b/src/main/java/persistence/exception/UnsupportedClassException.java @@ -1,4 +1,4 @@ -package persistence.sql.exception; +package persistence.exception; public class UnsupportedClassException extends RuntimeException { diff --git a/src/main/java/persistence/sql/exception/constraints/UnsupportedFieldException.java b/src/main/java/persistence/exception/UnsupportedFieldException.java similarity index 82% rename from src/main/java/persistence/sql/exception/constraints/UnsupportedFieldException.java rename to src/main/java/persistence/exception/UnsupportedFieldException.java index f252d1ac6..7ab2fd96a 100644 --- a/src/main/java/persistence/sql/exception/constraints/UnsupportedFieldException.java +++ b/src/main/java/persistence/exception/UnsupportedFieldException.java @@ -1,4 +1,4 @@ -package persistence.sql.exception.constraints; +package persistence.exception; import java.lang.reflect.Field; diff --git a/src/main/java/persistence/sql/AbstractQueryBuilder.java b/src/main/java/persistence/sql/AbstractQueryBuilder.java new file mode 100644 index 000000000..6e5e40dd3 --- /dev/null +++ b/src/main/java/persistence/sql/AbstractQueryBuilder.java @@ -0,0 +1,70 @@ +package persistence.sql; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Transient; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Comparator; +import java.util.stream.Stream; +import persistence.exception.UnsupportedClassException; + +public abstract class AbstractQueryBuilder { + + protected AbstractQueryBuilder() { + + } + + /** + * Get the stream of fields that are not annotated with @Transient from the entity class + * @param entityClass Entity class + * @return Stream of fields that are annotated with @Column or @Id + * @see Column + * @see Id + * @see Transient + */ + protected Stream getColumnFieldStream(Class entityClass) { + return Arrays.stream(entityClass.getDeclaredFields()) + .filter(field -> !field.isAnnotationPresent(Transient.class)) + .sorted(Comparator.comparing(field -> field.isAnnotationPresent(Id.class) ? 0 : 1)); + } + + /** + * Get the column name from the field + * @param field Field + * @return Column name from the field + */ + protected String getColumnNameFrom(Field field) { + if (!field.isAnnotationPresent(Column.class)) { + return field.getName(); + } + + Column column = field.getAnnotation(Column.class); + + if (column.name().isEmpty()) { + return field.getName(); + } + + return column.name(); + } + + /** + * Get the column value from the object + * @param columnValue Column value object + * @return Column value from the object + */ + protected String getColumnValueFromObject(Object columnValue) { + // TODO: remove this else-if statement + if (columnValue.getClass().equals(Boolean.class)) { + return columnValue == Boolean.TRUE ? "1" : "0"; + } else if (columnValue.getClass().equals(String.class)) { + return String.format("'%s'", columnValue); + } else if (columnValue.getClass().equals(Integer.class)) { + return columnValue.toString(); + } else if (columnValue.getClass().equals(Long.class)) { + return columnValue.toString(); + } + + throw new UnsupportedClassException(columnValue.getClass()); + } +} diff --git a/src/main/java/persistence/sql/QueryBuilder.java b/src/main/java/persistence/sql/QueryBuilder.java new file mode 100644 index 000000000..79246615e --- /dev/null +++ b/src/main/java/persistence/sql/QueryBuilder.java @@ -0,0 +1,86 @@ +package persistence.sql; + + +import java.lang.reflect.Field; +import persistence.sql.ddl.CreateQueryBuilder; +import persistence.sql.ddl.DropQueryBuilder; +import persistence.sql.ddl.TableQueryBuilder; +import persistence.sql.dml.DeleteQueryBuilder; +import persistence.sql.dml.InsertQueryBuilder; +import persistence.sql.dml.SelectQueryBuilder; + +public class QueryBuilder extends AbstractQueryBuilder { + + private final TableQueryBuilder tableQueryBuilder; + + private final SelectQueryBuilder selectQueryBuilder; + + private final DeleteQueryBuilder deleteQueryBuilder; + + private final InsertQueryBuilder insertQueryTranslator; + + private final DropQueryBuilder dropQueryBuilder; + + private final CreateQueryBuilder createQueryBuilder; + + public QueryBuilder() { + this(new TableQueryBuilder()); + } + + public QueryBuilder(TableQueryBuilder tableQueryBuilder) { + this.tableQueryBuilder = tableQueryBuilder; + this.selectQueryBuilder = new SelectQueryBuilder(tableQueryBuilder); + this.deleteQueryBuilder = new DeleteQueryBuilder(tableQueryBuilder); + this.insertQueryTranslator = new InsertQueryBuilder(tableQueryBuilder); + this.dropQueryBuilder = new DropQueryBuilder(tableQueryBuilder); + this.createQueryBuilder = new CreateQueryBuilder(tableQueryBuilder); + } + + public String getCreateTableQuery(final Class entityClass) { + return createQueryBuilder.getCreateTableQuery(entityClass); + } + + public String getDropTableQuery(Class entityClass) { + return dropQueryBuilder.getDropTableQuery(entityClass); + } + + public String getInsertQuery(Object entity) { + return insertQueryTranslator.getInsertQuery(entity); + } + + public String getSelectAllQuery(Class entityClass) { + return selectQueryBuilder.getSelectAllQuery(entityClass); + } + + public String getSelectByIdQuery(Class entityClass, Object id) { + return selectQueryBuilder.getSelectByIdQuery(entityClass, id); + } + + public String getSelectCountQuery(Class entityClass) { + return selectQueryBuilder.getSelectCountQuery(entityClass); + } + + public String getDeleteAllQuery(Class entityClass) { + return deleteQueryBuilder.getDeleteAllQuery(entityClass); + } + + public String getDeleteByIdQuery(Class entityClass, Object id) { + return deleteQueryBuilder.getDeleteByIdQuery(entityClass, id); + } + + public String getDeleteQueryFromEntity(Object entity) { + return deleteQueryBuilder.getDeleteQueryFromEntity(entity); + } + + public String getTableNameFrom(Class entityClass) { + return tableQueryBuilder.getTableNameFrom(entityClass); + } + + public String getColumnDefinitionFrom(Field field) { + return createQueryBuilder.getColumnDefinitionFrom(field); + } + + public String getColumnDefinitionsFrom(Class entityClass) { + return createQueryBuilder.getColumnDefinitionsFrom(entityClass); + } +} diff --git a/src/main/java/persistence/sql/QueryTranslator.java b/src/main/java/persistence/sql/QueryTranslator.java deleted file mode 100644 index 0c04686f9..000000000 --- a/src/main/java/persistence/sql/QueryTranslator.java +++ /dev/null @@ -1,229 +0,0 @@ -package persistence.sql; - -import static persistence.sql.ddl.common.StringConstants.COLUMN_DEFINITION_DELIMITER; -import static persistence.sql.ddl.common.StringConstants.SCHEMA_TABLE_DELIMITER; - -import jakarta.persistence.Column; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import jakarta.persistence.Transient; -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.Comparator; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import persistence.sql.ddl.common.StringConstants; -import persistence.sql.ddl.constraints.strategy.ConstraintsStrategy; -import persistence.sql.ddl.constraints.strategy.DefaultConstraintsStrategy; -import persistence.sql.ddl.type.DataTypeMapping; -import persistence.sql.ddl.type.impl.DefaultDataTypeMapping; -import persistence.sql.dml.ColumnValueTranslator; - -public class QueryTranslator { - - protected final ConstraintsStrategy constraintsStrategy; - - protected final DataTypeMapping dataTypeMapping; - - protected final ColumnValueTranslator columnValueTranslator; - - public QueryTranslator() { - this( - new DefaultConstraintsStrategy(), - new DefaultDataTypeMapping(), - new ColumnValueTranslator() - ); - } - - protected QueryTranslator( - ConstraintsStrategy constraintsStrategy, - DataTypeMapping dataTypeMapping, - ColumnValueTranslator columnValueTranslator - ) { - this.constraintsStrategy = constraintsStrategy; - this.dataTypeMapping = dataTypeMapping; - this.columnValueTranslator = columnValueTranslator; - } - - public String getCreateTableQuery(final Class entityClass) { - return String.format( - "CREATE TABLE %s (%s)", - getTableNameFrom(entityClass), - getTableColumnDefinitionFrom(entityClass) - ); - } - - public String getDropTableQuery(Class entityClass) { - return String.format( - "DROP TABLE %s", - getTableNameFrom(entityClass) - ); - } - - public String getInsertQuery(Object entity) { - Class entityClass = entity.getClass(); - - return String.format( - "INSERT INTO %s (%s) VALUES (%s)", - getTableNameFrom(entityClass), - getColumnNamesClauseWithoutPrimaryKey(entityClass), - getColumnValueClause(entity) - ); - } - - public String getSelectAllQuery(Class entityClass) { - return String.format( - "SELECT %s FROM %s", - getColumnNamesClause(entityClass), - getTableNameFrom(entityClass) - ); - } - - public String getSelectByIdQuery(Class entityClass, Object id) { - return String.format( - "SELECT %s FROM %s WHERE %s = %s", - getColumnNamesClause(entityClass), - getTableNameFrom(entityClass), - getPrimaryKeyColumnName(entityClass), - columnValueTranslator.getPrimaryKeyValueClauseFromEntityClassAndId(entityClass, id) - ); - } - - public String getDeleteAllQuery(Class entityClass) { - return String.format( - "DELETE FROM %s", - getTableNameFrom(entityClass) - ); - } - - public String getDeleteByIdQuery(Class entityClass, Object id) { - return String.format( - "DELETE FROM %s WHERE %s = %s", - getTableNameFrom(entityClass), - getPrimaryKeyColumnName(entityClass), - columnValueTranslator.getPrimaryKeyValueClauseFromEntityClassAndId(entityClass, id) - ); - } - - public String getDeleteQueryFromEntity(Class entityClass, Object entity) { - return String.format( - "DELETE FROM %s WHERE %s = %s", - getTableNameFrom(entityClass), - getPrimaryKeyColumnName(entityClass), - columnValueTranslator.getPrimaryKeyValueClauseFromEntityClassAndEntityObject(entityClass, entity) - ); - } - - - private String getPrimaryKeyColumnName(Class entityClass) { - return Arrays.stream(entityClass.getDeclaredFields()) - .filter(field -> field.isAnnotationPresent(Id.class)) - .findFirst() - .map(this::getColumnNameFrom) - .orElseThrow(() -> new IllegalStateException("Primary key not found")); - } - - public String getTableNameFrom(Class entityClass) { - return Stream.of( - getSchemaNameFrom(entityClass), - getOnlyTableNameFrom(entityClass) - ) - .filter(s -> !s.isBlank()) - .collect(Collectors.joining(SCHEMA_TABLE_DELIMITER)); - } - - public String getTableColumnDefinitionFrom(Class entityClass) { - return getColumnFieldStream(entityClass) - .map(this::getColumnDefinitionFrom) - .collect(Collectors.joining(COLUMN_DEFINITION_DELIMITER)); - } - - public String getColumnDefinitionFrom(Field field) { - return Stream.of( - getColumnNameFrom(field), - getColumnDataTypeDefinitionFrom(field), - getColumnConstraintsFrom(field) - ) - .filter(s -> !s.isBlank()) - .collect(Collectors.joining(" ")); - } - - public String getColumnNameFrom(Field field) { - if (!field.isAnnotationPresent(Column.class)) { - return field.getName(); - } - - Column column = field.getAnnotation(Column.class); - - if (column.name().isEmpty()) { - return field.getName(); - } - - return column.name(); - } - - public String getColumnDataTypeDefinitionFrom(Field field) { - return dataTypeMapping.getDataTypeDefinitionFrom(field); - } - - public String getColumnConstraintsFrom(Field field) { - return constraintsStrategy.getConstraintsFrom(field); - } - - public String getColumnValueClause(Object entity) { - return columnValueTranslator.getColumnValueClause(entity); - } - - protected String getColumnNamesClause(Class entityClass) { - return getColumnFieldStream(entityClass) - .map(this::getColumnNameFrom) - .collect(Collectors.joining(COLUMN_DEFINITION_DELIMITER)); - } - - protected String getColumnNamesClauseWithoutPrimaryKey(Class entityClass) { - return getColumnFieldStream(entityClass) - .filter(field -> !field.isAnnotationPresent(Id.class)) - .map(this::getColumnNameFrom) - .collect(Collectors.joining(COLUMN_DEFINITION_DELIMITER)); - } - - protected static Stream getColumnFieldStream(Class entityClass) { - return Arrays.stream(entityClass.getDeclaredFields()) - .filter(field -> !field.isAnnotationPresent(Transient.class)) - .sorted(Comparator.comparing(field -> field.isAnnotationPresent(Id.class) ? 0 : 1)); - } - - protected String getOnlyTableNameFrom(final Class entityClass) { - if (entityClass.isAnnotationPresent(Table.class)) { - Table table = entityClass.getAnnotation(Table.class); - return getOnlyTableNameFrom(table); - } - - return entityClass.getSimpleName(); - } - - protected String getOnlyTableNameFrom(Table table) { - if (!table.name().isEmpty()) { - return table.name(); - } - - return StringConstants.EMPTY_STRING; - } - - protected String getSchemaNameFrom(Class entityClass) { - if (entityClass.isAnnotationPresent(Table.class)) { - Table table = entityClass.getAnnotation(Table.class); - return getSchemaNameFrom(table); - } - - return StringConstants.EMPTY_STRING; - } - - protected String getSchemaNameFrom(Table table) { - if (!table.schema().isEmpty()) { - return table.schema(); - } - - return StringConstants.EMPTY_STRING; - } -} diff --git a/src/main/java/persistence/sql/ddl/ColumnQueryBuilder.java b/src/main/java/persistence/sql/ddl/ColumnQueryBuilder.java new file mode 100644 index 000000000..3f591be97 --- /dev/null +++ b/src/main/java/persistence/sql/ddl/ColumnQueryBuilder.java @@ -0,0 +1,32 @@ +package persistence.sql.ddl; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Transient; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Comparator; +import java.util.stream.Stream; + +public class ColumnQueryBuilder { + + public String getColumnNameFrom(Field field) { + if (!field.isAnnotationPresent(Column.class)) { + return field.getName(); + } + + Column column = field.getAnnotation(Column.class); + + if (column.name().isEmpty()) { + return field.getName(); + } + + return column.name(); + } + + public Stream getColumnFieldStream(Class entityClass) { + return Arrays.stream(entityClass.getDeclaredFields()) + .filter(field -> !field.isAnnotationPresent(Transient.class)) + .sorted(Comparator.comparing(field -> field.isAnnotationPresent(Id.class) ? 0 : 1)); + } +} diff --git a/src/main/java/persistence/sql/ddl/CreateQueryBuilder.java b/src/main/java/persistence/sql/ddl/CreateQueryBuilder.java new file mode 100644 index 000000000..d17085660 --- /dev/null +++ b/src/main/java/persistence/sql/ddl/CreateQueryBuilder.java @@ -0,0 +1,63 @@ +package persistence.sql.ddl; + +import static persistence.sql.ddl.common.StringConstants.COLUMN_DEFINITION_DELIMITER; + +import java.lang.reflect.Field; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import persistence.sql.AbstractQueryBuilder; +import persistence.sql.ddl.common.StringConstants; +import persistence.sql.ddl.constraints.strategy.ConstraintsStrategy; +import persistence.sql.ddl.constraints.strategy.DefaultConstraintsStrategy; +import persistence.sql.ddl.type.DataTypeMapping; +import persistence.sql.ddl.type.impl.DefaultDataTypeMapping; + +public class CreateQueryBuilder extends AbstractQueryBuilder { + private final TableQueryBuilder tableQueryBuilder; + + private final DataTypeMapping dataTypeMapping; + + private final ConstraintsStrategy constraintsStrategy; + + public CreateQueryBuilder(TableQueryBuilder tableQueryBuilder) { + this( + tableQueryBuilder, + new DefaultDataTypeMapping(), + new DefaultConstraintsStrategy() + ); + } + + public CreateQueryBuilder( + TableQueryBuilder tableQueryBuilder, + DataTypeMapping dataTypeMapping, + ConstraintsStrategy constraintsStrategy + ) { + this.tableQueryBuilder = tableQueryBuilder; + this.dataTypeMapping = dataTypeMapping; + this.constraintsStrategy = constraintsStrategy; + } + + public String getCreateTableQuery(final Class entityClass) { + return String.format( + "CREATE TABLE %s (%s)", + tableQueryBuilder.getTableNameFrom(entityClass), + getColumnDefinitionsFrom(entityClass) + ); + } + + public String getColumnDefinitionsFrom(Class entityClass) { + return getColumnFieldStream(entityClass) + .map(this::getColumnDefinitionFrom) + .collect(Collectors.joining(COLUMN_DEFINITION_DELIMITER)); + } + + public String getColumnDefinitionFrom(Field field) { + return Stream.of( + getColumnNameFrom(field), + dataTypeMapping.getDataTypeDefinitionFrom(field), + constraintsStrategy.getConstraintsFrom(field) + ) + .filter(s -> !s.isBlank()) + .collect(Collectors.joining(StringConstants.SPACE)); + } +} diff --git a/src/main/java/persistence/sql/ddl/DropQueryBuilder.java b/src/main/java/persistence/sql/ddl/DropQueryBuilder.java new file mode 100644 index 000000000..455a82fe6 --- /dev/null +++ b/src/main/java/persistence/sql/ddl/DropQueryBuilder.java @@ -0,0 +1,16 @@ +package persistence.sql.ddl; + +public class DropQueryBuilder { + private final TableQueryBuilder tableQueryBuilder; + + public DropQueryBuilder(TableQueryBuilder tableQueryBuilder) { + this.tableQueryBuilder = tableQueryBuilder; + } + + public String getDropTableQuery(Class entityClass) { + return String.format( + "DROP TABLE %s", + tableQueryBuilder.getTableNameFrom(entityClass) + ); + } +} diff --git a/src/main/java/persistence/sql/ddl/TableQueryBuilder.java b/src/main/java/persistence/sql/ddl/TableQueryBuilder.java new file mode 100644 index 000000000..9a3637102 --- /dev/null +++ b/src/main/java/persistence/sql/ddl/TableQueryBuilder.java @@ -0,0 +1,53 @@ +package persistence.sql.ddl; + +import static persistence.sql.ddl.common.StringConstants.SCHEMA_TABLE_DELIMITER; + +import jakarta.persistence.Table; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import persistence.sql.ddl.common.StringConstants; + +public class TableQueryBuilder { + public String getTableNameFrom(Class entityClass) { + return Stream.of( + getSchemaNameFrom(entityClass), + getOnlyTableNameFrom(entityClass) + ) + .filter(s -> !s.isBlank()) + .collect(Collectors.joining(SCHEMA_TABLE_DELIMITER)); + } + + public String getSchemaNameFrom(Class entityClass) { + if (entityClass.isAnnotationPresent(Table.class)) { + Table table = entityClass.getAnnotation(Table.class); + return getSchemaNameFrom(table); + } + + return StringConstants.EMPTY_STRING; + } + + public String getSchemaNameFrom(Table table) { + if (!table.schema().isEmpty()) { + return table.schema(); + } + + return StringConstants.EMPTY_STRING; + } + + public String getOnlyTableNameFrom(final Class entityClass) { + if (entityClass.isAnnotationPresent(Table.class)) { + Table table = entityClass.getAnnotation(Table.class); + return getOnlyTableNameFrom(table); + } + + return entityClass.getSimpleName(); + } + + public String getOnlyTableNameFrom(Table table) { + if (!table.name().isEmpty()) { + return table.name(); + } + + return StringConstants.EMPTY_STRING; + } +} diff --git a/src/main/java/persistence/sql/ddl/common/StringConstants.java b/src/main/java/persistence/sql/ddl/common/StringConstants.java index 0f644b8ec..9338935ca 100644 --- a/src/main/java/persistence/sql/ddl/common/StringConstants.java +++ b/src/main/java/persistence/sql/ddl/common/StringConstants.java @@ -14,4 +14,6 @@ private StringConstants() { public static final String COLUMN_DEFINITION_DELIMITER = ", "; + public static final String PRIMARY_KEY_NOT_FOUND = "Primary key not found"; + } diff --git a/src/main/java/persistence/sql/ddl/constraints/impl/AutoIncrementConstraintsTranslator.java b/src/main/java/persistence/sql/ddl/constraints/impl/AutoIncrementConstraintsTranslator.java index 5ebc1055d..58d454e88 100644 --- a/src/main/java/persistence/sql/ddl/constraints/impl/AutoIncrementConstraintsTranslator.java +++ b/src/main/java/persistence/sql/ddl/constraints/impl/AutoIncrementConstraintsTranslator.java @@ -3,7 +3,7 @@ import jakarta.persistence.Id; import java.lang.reflect.Field; import persistence.sql.ddl.constraints.ConstraintsTranslator; -import persistence.sql.exception.constraints.UnsupportedFieldException; +import persistence.exception.UnsupportedFieldException; public class AutoIncrementConstraintsTranslator implements ConstraintsTranslator { private static final String AUTO_INCREMENT = "AUTO_INCREMENT"; diff --git a/src/main/java/persistence/sql/ddl/constraints/impl/NotNullConstraintsTranslator.java b/src/main/java/persistence/sql/ddl/constraints/impl/NotNullConstraintsTranslator.java index e3ae8b5d1..772a24a6b 100644 --- a/src/main/java/persistence/sql/ddl/constraints/impl/NotNullConstraintsTranslator.java +++ b/src/main/java/persistence/sql/ddl/constraints/impl/NotNullConstraintsTranslator.java @@ -4,7 +4,7 @@ import java.lang.reflect.Field; import persistence.sql.ddl.common.StringConstants; import persistence.sql.ddl.constraints.ConstraintsTranslator; -import persistence.sql.exception.constraints.UnsupportedFieldException; +import persistence.exception.UnsupportedFieldException; public class NotNullConstraintsTranslator implements ConstraintsTranslator { diff --git a/src/main/java/persistence/sql/ddl/constraints/impl/UniqueConstraintsTranslator.java b/src/main/java/persistence/sql/ddl/constraints/impl/UniqueConstraintsTranslator.java index 14560f459..51933066c 100644 --- a/src/main/java/persistence/sql/ddl/constraints/impl/UniqueConstraintsTranslator.java +++ b/src/main/java/persistence/sql/ddl/constraints/impl/UniqueConstraintsTranslator.java @@ -4,7 +4,7 @@ import java.lang.reflect.Field; import persistence.sql.ddl.common.StringConstants; import persistence.sql.ddl.constraints.ConstraintsTranslator; -import persistence.sql.exception.constraints.UnsupportedFieldException; +import persistence.exception.UnsupportedFieldException; public class UniqueConstraintsTranslator implements ConstraintsTranslator { diff --git a/src/main/java/persistence/sql/ddl/constraints/strategy/DefaultConstraintsStrategy.java b/src/main/java/persistence/sql/ddl/constraints/strategy/DefaultConstraintsStrategy.java index 711fcce92..9feeb29a0 100644 --- a/src/main/java/persistence/sql/ddl/constraints/strategy/DefaultConstraintsStrategy.java +++ b/src/main/java/persistence/sql/ddl/constraints/strategy/DefaultConstraintsStrategy.java @@ -4,8 +4,8 @@ import java.util.List; import java.util.stream.Collectors; import persistence.sql.ddl.common.StringConstants; -import persistence.sql.ddl.constraints.impl.AutoIncrementConstraintsTranslator; import persistence.sql.ddl.constraints.ConstraintsTranslator; +import persistence.sql.ddl.constraints.impl.AutoIncrementConstraintsTranslator; import persistence.sql.ddl.constraints.impl.NotNullConstraintsTranslator; import persistence.sql.ddl.constraints.impl.UniqueConstraintsTranslator; diff --git a/src/main/java/persistence/sql/ddl/entity/Person.java b/src/main/java/persistence/sql/ddl/entity/Person.java index e491b06d6..7c214d134 100644 --- a/src/main/java/persistence/sql/ddl/entity/Person.java +++ b/src/main/java/persistence/sql/ddl/entity/Person.java @@ -7,6 +7,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.persistence.Transient; +import java.util.Objects; @Table(name = "users") @Entity @@ -27,7 +28,7 @@ public class Person { @Transient private Integer index; - protected Person() { + public Person() { } @@ -44,6 +45,26 @@ public Person(Long id, String name, Integer age, String email) { this.email = email; } + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Integer getAge() { + return age; + } + + public String getEmail() { + return email; + } + + public Integer getIndex() { + return index; + } + @Override public String toString() { return "Person{" + @@ -53,4 +74,23 @@ public String toString() { ", email='" + email + '\'' + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Person person = (Person) o; + return Objects.equals(id, person.id) && Objects.equals(name, person.name) + && Objects.equals(age, person.age) && Objects.equals(email, + person.email); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, age, email); + } } diff --git a/src/main/java/persistence/sql/dml/ColumnValueTranslator.java b/src/main/java/persistence/sql/dml/ColumnValueTranslator.java deleted file mode 100644 index c3a7f58a2..000000000 --- a/src/main/java/persistence/sql/dml/ColumnValueTranslator.java +++ /dev/null @@ -1,81 +0,0 @@ -package persistence.sql.dml; - -import static persistence.sql.ddl.common.StringConstants.COLUMN_DEFINITION_DELIMITER; - -import jakarta.persistence.Id; -import jakarta.persistence.Transient; -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.Comparator; -import java.util.stream.Collectors; -import persistence.sql.exception.UnsupportedClassException; - -public class ColumnValueTranslator { - - public String getColumnValueClause(Object entity) { - Class clazz = entity.getClass(); - return Arrays.stream(clazz.getDeclaredFields()) - .filter(field -> !field.isAnnotationPresent(Transient.class)) - .filter(field -> !field.isAnnotationPresent(Id.class)) - .sorted(Comparator.comparing(field -> field.isAnnotationPresent(Id.class) ? 0 : 1)) - .map(field -> getNativeColumnValueStringFromEntity(entity, field)) - .collect(Collectors.joining(COLUMN_DEFINITION_DELIMITER)); - } - - public String getPrimaryKeyValueClauseFromEntityClassAndId(Class entityClass, Object id) { - Field primaryKeyField = Arrays.stream(entityClass.getDeclaredFields()) - .filter(field -> field.isAnnotationPresent(Id.class)) - .findFirst() - .orElseThrow(() -> new IllegalStateException("Primary key not found")); - - if (!primaryKeyField.getType().equals(id.getClass())) { - throw new IllegalStateException("Primary key type mismatch"); - } - - return getNativeColumnValueString(id); - } - - public String getPrimaryKeyValueClauseFromEntityClassAndEntityObject(Class entityClass, Object entity) { - try { - Field primaryKeyField = Arrays.stream(entityClass.getDeclaredFields()) - .filter(field -> field.isAnnotationPresent(Id.class)) - .findFirst() - .orElseThrow(() -> new IllegalStateException("Primary key not found")); - - primaryKeyField.setAccessible(true); - - Object id = primaryKeyField.get(entity); - - return getNativeColumnValueString(id); - } catch (IllegalAccessException e) { - throw new IllegalStateException(e); - } - } - - private String getNativeColumnValueStringFromEntity(Object entity, Field field) { - try { - field.setAccessible(true); - Object columnValue = field.get(entity); - - return getNativeColumnValueString(columnValue); - } catch (IllegalAccessException e) { - throw new IllegalStateException(e); - } - } - - private String getNativeColumnValueString(Object columnValue) { - // TODO: remove this else-if statement - if (columnValue.getClass().equals(Boolean.class)) { - return columnValue == Boolean.TRUE ? "1" : "0"; - } else if (columnValue.getClass().equals(String.class)) { - return String.format("'%s'", columnValue); - } else if (columnValue.getClass().equals(Integer.class)) { - return columnValue.toString(); - } else if (columnValue.getClass().equals(Long.class)) { - return columnValue.toString(); - } - - throw new UnsupportedClassException(columnValue.getClass()); - } - -} diff --git a/src/main/java/persistence/sql/dml/DeleteQueryBuilder.java b/src/main/java/persistence/sql/dml/DeleteQueryBuilder.java new file mode 100644 index 000000000..5acdcc057 --- /dev/null +++ b/src/main/java/persistence/sql/dml/DeleteQueryBuilder.java @@ -0,0 +1,86 @@ +package persistence.sql.dml; + +import static persistence.sql.ddl.common.StringConstants.*; + +import jakarta.persistence.Id; +import java.lang.reflect.Field; +import java.util.Arrays; +import persistence.sql.AbstractQueryBuilder; +import persistence.sql.ddl.TableQueryBuilder; + +public class DeleteQueryBuilder extends AbstractQueryBuilder { + private final TableQueryBuilder tableQueryBuilder; + + public DeleteQueryBuilder( + TableQueryBuilder tableQueryBuilder + ) { + this.tableQueryBuilder = tableQueryBuilder; + } + + public String getDeleteAllQuery(Class entityClass) { + return String.format( + "DELETE FROM %s", + tableQueryBuilder.getTableNameFrom(entityClass) + ); + } + + public String getDeleteByIdQuery(Class entityClass, Object id) { + return String.format( + "DELETE FROM %s WHERE %s = %s", + tableQueryBuilder.getTableNameFrom(entityClass), + getPrimaryKeyColumnName(entityClass), + getPrimaryKeyValueQueryFromEntityClassAndId(entityClass, id) + ); + } + + public String getDeleteQueryFromEntity(Object entity) { + return getDeleteQueryFromEntity(entity.getClass(), entity); + } + + private String getDeleteQueryFromEntity(Class entityClass, Object entity) { + return String.format( + "DELETE FROM %s WHERE %s = %s", + tableQueryBuilder.getTableNameFrom(entityClass), + getPrimaryKeyColumnName(entityClass), + getPrimaryKeyValueQueryFromEntityClassAndEntityObject(entityClass, entity) + ); + } + + private String getPrimaryKeyColumnName(Class entityClass) { + return Arrays.stream(entityClass.getDeclaredFields()) + .filter(field -> field.isAnnotationPresent(Id.class)) + .findFirst() + .map(this::getColumnNameFrom) + .orElseThrow(() -> new IllegalStateException(PRIMARY_KEY_NOT_FOUND)); + } + + private String getPrimaryKeyValueQueryFromEntityClassAndId(Class entityClass, Object id) { + Field primaryKeyField = Arrays.stream(entityClass.getDeclaredFields()) + .filter(field -> field.isAnnotationPresent(Id.class)) + .findFirst() + .orElseThrow(() -> new IllegalStateException(PRIMARY_KEY_NOT_FOUND)); + + if (!primaryKeyField.getType().equals(id.getClass())) { + throw new IllegalStateException("Primary key type mismatch"); + } + + return getColumnValueFromObject(id); + } + + private String getPrimaryKeyValueQueryFromEntityClassAndEntityObject(Class entityClass, Object entity) { + try { + Field primaryKeyField = Arrays.stream(entityClass.getDeclaredFields()) + .filter(field -> field.isAnnotationPresent(Id.class)) + .findFirst() + .orElseThrow(() -> new IllegalStateException(PRIMARY_KEY_NOT_FOUND)); + + primaryKeyField.setAccessible(true); + + Object id = primaryKeyField.get(entity); + + return getColumnValueFromObject(id); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/src/main/java/persistence/sql/dml/InsertQueryBuilder.java b/src/main/java/persistence/sql/dml/InsertQueryBuilder.java new file mode 100644 index 000000000..967278170 --- /dev/null +++ b/src/main/java/persistence/sql/dml/InsertQueryBuilder.java @@ -0,0 +1,59 @@ +package persistence.sql.dml; + +import static persistence.sql.ddl.common.StringConstants.COLUMN_DEFINITION_DELIMITER; + +import jakarta.persistence.Id; +import jakarta.persistence.Transient; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Comparator; +import java.util.stream.Collectors; +import persistence.sql.AbstractQueryBuilder; +import persistence.sql.ddl.TableQueryBuilder; + +public class InsertQueryBuilder extends AbstractQueryBuilder { + private final TableQueryBuilder tableQueryBuilder; + + public InsertQueryBuilder(TableQueryBuilder tableQueryBuilder) { + this.tableQueryBuilder = tableQueryBuilder; + } + + public String getInsertQuery(Object entity) { + Class entityClass = entity.getClass(); + + return String.format( + "INSERT INTO %s (%s) VALUES (%s)", + tableQueryBuilder.getTableNameFrom(entityClass), + getColumnNamesWithoutPrimaryKey(entityClass), + getColumnValuesQuery(entity) + ); + } + + private String getColumnValuesQuery(Object entity) { + Class clazz = entity.getClass(); + return Arrays.stream(clazz.getDeclaredFields()) + .filter(field -> !field.isAnnotationPresent(Transient.class)) + .filter(field -> !field.isAnnotationPresent(Id.class)) + .sorted(Comparator.comparing(field -> field.isAnnotationPresent(Id.class) ? 0 : 1)) + .map(field -> getColumnValueFromEntity(entity, field)) + .collect(Collectors.joining(COLUMN_DEFINITION_DELIMITER)); + } + + private String getColumnNamesWithoutPrimaryKey(Class entityClass) { + return getColumnFieldStream(entityClass) + .filter(field -> !field.isAnnotationPresent(Id.class)) + .map(this::getColumnNameFrom) + .collect(Collectors.joining(COLUMN_DEFINITION_DELIMITER)); + } + + private String getColumnValueFromEntity(Object entity, Field field) { + try { + field.setAccessible(true); + Object columnValue = field.get(entity); + + return getColumnValueFromObject(columnValue); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/src/main/java/persistence/sql/dml/SelectQueryBuilder.java b/src/main/java/persistence/sql/dml/SelectQueryBuilder.java new file mode 100644 index 000000000..36267af59 --- /dev/null +++ b/src/main/java/persistence/sql/dml/SelectQueryBuilder.java @@ -0,0 +1,74 @@ +package persistence.sql.dml; + +import static persistence.sql.ddl.common.StringConstants.COLUMN_DEFINITION_DELIMITER; +import static persistence.sql.ddl.common.StringConstants.PRIMARY_KEY_NOT_FOUND; + +import jakarta.persistence.Id; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.stream.Collectors; +import persistence.sql.AbstractQueryBuilder; +import persistence.sql.ddl.TableQueryBuilder; + +public class SelectQueryBuilder extends AbstractQueryBuilder { + private final TableQueryBuilder tableQueryBuilder; + + public SelectQueryBuilder( + TableQueryBuilder tableQueryBuilder + ) { + this.tableQueryBuilder = tableQueryBuilder; + } + + public String getSelectAllQuery(Class entityClass) { + return String.format( + "SELECT %s FROM %s", + getColumnNamesClause(entityClass), + tableQueryBuilder.getTableNameFrom(entityClass) + ); + } + + public String getSelectByIdQuery(Class entityClass, Object id) { + return String.format( + "SELECT %s FROM %s WHERE %s = %s", + getColumnNamesClause(entityClass), + tableQueryBuilder.getTableNameFrom(entityClass), + getPrimaryKeyColumnName(entityClass), + getPrimaryKeyValueQueryFromEntityClassAndId(entityClass, id) + ); + } + + public String getSelectCountQuery(Class entityClass) { + return String.format( + "SELECT COUNT(%s) FROM %s", + getPrimaryKeyColumnName(entityClass), + tableQueryBuilder.getTableNameFrom(entityClass) + ); + } + + private String getColumnNamesClause(Class entityClass) { + return getColumnFieldStream(entityClass) + .map(this::getColumnNameFrom) + .collect(Collectors.joining(COLUMN_DEFINITION_DELIMITER)); + } + + private String getPrimaryKeyColumnName(Class entityClass) { + return Arrays.stream(entityClass.getDeclaredFields()) + .filter(field -> field.isAnnotationPresent(Id.class)) + .findFirst() + .map(this::getColumnNameFrom) + .orElseThrow(() -> new IllegalStateException(PRIMARY_KEY_NOT_FOUND)); + } + + private String getPrimaryKeyValueQueryFromEntityClassAndId(Class entityClass, Object id) { + Field primaryKeyField = Arrays.stream(entityClass.getDeclaredFields()) + .filter(field -> field.isAnnotationPresent(Id.class)) + .findFirst() + .orElseThrow(() -> new IllegalStateException(PRIMARY_KEY_NOT_FOUND)); + + if (!primaryKeyField.getType().equals(id.getClass())) { + throw new IllegalStateException("Primary key type mismatch"); + } + + return getColumnValueFromObject(id); + } +} diff --git a/src/test/java/persistence/entity/impl/EntityManagerImplTest.java b/src/test/java/persistence/entity/impl/EntityManagerImplTest.java new file mode 100644 index 000000000..15d6d69a6 --- /dev/null +++ b/src/test/java/persistence/entity/impl/EntityManagerImplTest.java @@ -0,0 +1,155 @@ +package persistence.entity.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertAll; + +import database.DatabaseServer; +import database.H2; +import java.sql.SQLException; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdbc.JdbcTemplate; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import persistence.entity.EntityManager; +import persistence.sql.QueryBuilder; +import persistence.sql.ddl.entity.Person; + +class EntityManagerImplTest { + private static DatabaseServer server; + + private EntityManager entityManager; + + private final static Map idToPersonMap = Stream.of( + new Person(1L, "root", 20, "root@gmail.com"), + new Person(2L, "test", 30, "test@gmail.com"), + new Person(3L, "user1", 40, "user1@gmail.com"), + new Person(4L, "user2", 50, "user2@gmail.com") + ).collect(Collectors.toMap(Person::getId, Function.identity())); + + @BeforeAll + static void beforeAll() throws SQLException { + server = new H2(); + server.start(); + } + + @AfterAll + static void afterAll() { + server.stop(); + } + + @BeforeEach + void setUp() throws SQLException { + JdbcTemplate jdbcTemplate = getJdbcTemplate(); + + entityManager = new EntityManagerImpl(jdbcTemplate); + + QueryBuilder queryBuilder = new QueryBuilder(); + + jdbcTemplate.execute(queryBuilder.getCreateTableQuery(Person.class)); + } + + @AfterEach + void tearDown() throws SQLException { + dropTable(); + } + + private static JdbcTemplate getJdbcTemplate() throws SQLException { + return new JdbcTemplate(server.getConnection()); + } + + private static Stream providePerson() { + return idToPersonMap.values().stream() + .map(Arguments::of); + } + + @DisplayName("요구사항1 - find 메서드를 통해 id에 해당하는 Person 레코드를 조회할 수 있다.") + @ParameterizedTest(name = "id: {0}") + @ValueSource(longs = {1, 2, 3, 4}) + void find(Long id) throws SQLException { + // given + initializeTable(); + Person givenPerson = idToPersonMap.get(id); + + // when + Person entity = entityManager.find(Person.class, id); + + // then + assertAll( + () -> assertThat(entity).isNotNull(), + () -> assertThat(entity).isEqualTo(givenPerson) + ); + } + + @DisplayName("요구사항2 - persist (insert) 메서드를 통해 Entity를 저장할 수 있다.") + @Test + void persist() { + // given + Person person = new Person("test", 20, "test@gmail.com"); + + // when + Object savedEntity = entityManager.persist(person); + + // then + assertThat(savedEntity).isEqualTo(person); + } + + @DisplayName("요구사항3 - remove (delete) 메서드를 통해 특정 Entity를 삭제할 수 있다.") + @ParameterizedTest(name = "person: {0}") + @MethodSource("providePerson") + void remove(Person givenPerson) throws SQLException { + // given + initializeTable(); + Long id = givenPerson.getId(); + + // when + entityManager.remove(givenPerson); + + // then + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> + entityManager.find(Person.class, id) + ); + + Integer totalCountOfEntity = selectCountOfTable(); + assertThat(totalCountOfEntity).isEqualTo(idToPersonMap.size() - 1); + } + + private void initializeTable() throws SQLException { + JdbcTemplate jdbcTemplate = getJdbcTemplate(); + + QueryBuilder queryBuilder = new QueryBuilder(); + + for (Person person : idToPersonMap.values()) { + jdbcTemplate.execute(queryBuilder.getInsertQuery(person)); + } + } + + private void dropTable() throws SQLException { + JdbcTemplate jdbcTemplate = getJdbcTemplate(); + + QueryBuilder queryBuilder = new QueryBuilder(); + + jdbcTemplate.execute(queryBuilder.getDropTableQuery(Person.class)); + } + + private Integer selectCountOfTable() throws SQLException { + JdbcTemplate jdbcTemplate = getJdbcTemplate(); + + QueryBuilder queryBuilder = new QueryBuilder(); + + return jdbcTemplate.queryForObject(queryBuilder.getSelectCountQuery(Person.class), + resultSet -> resultSet.getInt(1)); + } +} diff --git a/src/test/java/persistence/sql/ddl/query/QueryTranslator1Test.java b/src/test/java/persistence/sql/ddl/query/QueryBuilder1Test.java similarity index 80% rename from src/test/java/persistence/sql/ddl/query/QueryTranslator1Test.java rename to src/test/java/persistence/sql/ddl/query/QueryBuilder1Test.java index 69b0525b8..bd65f83dc 100644 --- a/src/test/java/persistence/sql/ddl/query/QueryTranslator1Test.java +++ b/src/test/java/persistence/sql/ddl/query/QueryBuilder1Test.java @@ -8,21 +8,21 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.slf4j.Logger; -import persistence.sql.QueryTranslator; +import persistence.sql.QueryBuilder; import persistence.sql.ddl.entity.Person1; @DisplayName("1단계 요구사항 - @Entity, @Id 어노테이션을 바탕으로 create 쿼리 만들어보기") -class QueryTranslator1Test { - private static final Logger log = org.slf4j.LoggerFactory.getLogger(QueryTranslator2Test.class); +class QueryBuilder1Test { + private static final Logger log = org.slf4j.LoggerFactory.getLogger(QueryBuilder2Test.class); private final Class entityClass = Person1.class; - private final QueryTranslator queryTranslator = new QueryTranslator(); + private final QueryBuilder queryBuilder = new QueryBuilder(); @Test @DisplayName("@Entity, @Id 어노테이션을 바탕으로 create 쿼리 만들어보기") void createDDL() { - String ddl = queryTranslator.getCreateTableQuery(entityClass); + String ddl = queryBuilder.getCreateTableQuery(entityClass); log.debug("DDL: {}", ddl); @@ -33,7 +33,7 @@ void createDDL() { @Test @DisplayName("@Entity, @Id 어노테이션을 바탕으로 drop 쿼리 만들어보기") void buildDropQuery() { - String dropQuery = queryTranslator.getDropTableQuery(entityClass); + String dropQuery = queryBuilder.getDropTableQuery(entityClass); log.debug("Drop query: {}", dropQuery); @@ -43,7 +43,7 @@ void buildDropQuery() { @Test @DisplayName("클래스 정보를 바탕으로 테이블명 가져오기") void getTableNameByClassName() { - String tableName = queryTranslator.getTableNameFrom(entityClass); + String tableName = queryBuilder.getTableNameFrom(entityClass); log.debug("Table name: {}", tableName); @@ -54,7 +54,7 @@ void getTableNameByClassName() { @Test @DisplayName("클래스 정보를 바탕으로 컬럼 선언문 가져오기") void getColumnDefinitionStatement() { - String columnDefinitionStatement = queryTranslator.getTableColumnDefinitionFrom(entityClass); + String columnDefinitionStatement = queryBuilder.getColumnDefinitionsFrom(entityClass); log.debug("Column definition statement: {}", columnDefinitionStatement); @@ -73,7 +73,7 @@ void getColumnDefinitionStatementFromField( ) throws NoSuchFieldException { Field field = entityClass.getDeclaredField(fieldName); - String actualColumnDefinitionStatement = queryTranslator.getColumnDefinitionFrom(field); + String actualColumnDefinitionStatement = queryBuilder.getColumnDefinitionFrom(field); assertThat(actualColumnDefinitionStatement).isEqualTo(expectedColumnDefinitionStatement); } diff --git a/src/test/java/persistence/sql/ddl/query/QueryTranslator2Test.java b/src/test/java/persistence/sql/ddl/query/QueryBuilder2Test.java similarity index 81% rename from src/test/java/persistence/sql/ddl/query/QueryTranslator2Test.java rename to src/test/java/persistence/sql/ddl/query/QueryBuilder2Test.java index eb8d8c4b3..846b667e3 100644 --- a/src/test/java/persistence/sql/ddl/query/QueryTranslator2Test.java +++ b/src/test/java/persistence/sql/ddl/query/QueryBuilder2Test.java @@ -8,22 +8,22 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.slf4j.Logger; -import persistence.sql.QueryTranslator; +import persistence.sql.QueryBuilder; import persistence.sql.ddl.entity.Person2; @DisplayName("2단계 요구사항 - @Entity, @Id, @Column 어노테이션을 바탕으로 create 쿼리 만들어보기") -class QueryTranslator2Test { +class QueryBuilder2Test { - private static final Logger log = org.slf4j.LoggerFactory.getLogger(QueryTranslator2Test.class); + private static final Logger log = org.slf4j.LoggerFactory.getLogger(QueryBuilder2Test.class); private final Class entityClass = Person2.class; - private final QueryTranslator queryTranslator = new QueryTranslator(); + private final QueryBuilder queryBuilder = new QueryBuilder(); @Test @DisplayName("@Entity, @Id, @Column 어노테이션을 바탕으로 create 쿼리 만들어보기") void createDDL() { - String ddl = queryTranslator.getCreateTableQuery(entityClass); + String ddl = queryBuilder.getCreateTableQuery(entityClass); log.debug("DDL: {}", ddl); @@ -34,7 +34,7 @@ void createDDL() { @Test @DisplayName("@Entity, @Id, @Column 어노테이션을 바탕으로 drop 쿼리 만들어보기") void buildDropQuery() { - String dropQuery = queryTranslator.getDropTableQuery(entityClass); + String dropQuery = queryBuilder.getDropTableQuery(entityClass); log.debug("Drop query: {}", dropQuery); @@ -44,7 +44,7 @@ void buildDropQuery() { @Test @DisplayName("클래스 정보를 바탕으로 테이블명 가져오기") void getTableNameByClassName() { - String tableName = queryTranslator.getTableNameFrom(entityClass); + String tableName = queryBuilder.getTableNameFrom(entityClass); log.debug("Table name: {}", tableName); @@ -54,7 +54,7 @@ void getTableNameByClassName() { @Test @DisplayName("클래스 정보와 @Id, @Column 어노테이션을 바탕으로 컬럼 선언문 가져오기") void getColumnDefinitionStatement() { - String columnDefinitionStatement = queryTranslator.getTableColumnDefinitionFrom(entityClass); + String columnDefinitionStatement = queryBuilder.getColumnDefinitionsFrom(entityClass); log.debug("Column definition statement: {}", columnDefinitionStatement); @@ -74,7 +74,7 @@ void getColumnDefinitionStatementFromField( ) throws NoSuchFieldException { Field field = entityClass.getDeclaredField(fieldName); - String actualColumnDefinitionStatement = queryTranslator.getColumnDefinitionFrom(field); + String actualColumnDefinitionStatement = queryBuilder.getColumnDefinitionFrom(field); assertThat(actualColumnDefinitionStatement).isEqualTo(expectedColumnDefinitionStatement); } diff --git a/src/test/java/persistence/sql/ddl/query/QueryTranslator3Test.java b/src/test/java/persistence/sql/ddl/query/QueryBuilder3Test.java similarity index 81% rename from src/test/java/persistence/sql/ddl/query/QueryTranslator3Test.java rename to src/test/java/persistence/sql/ddl/query/QueryBuilder3Test.java index 2c58a341f..d5e06361d 100644 --- a/src/test/java/persistence/sql/ddl/query/QueryTranslator3Test.java +++ b/src/test/java/persistence/sql/ddl/query/QueryBuilder3Test.java @@ -8,22 +8,22 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.slf4j.Logger; -import persistence.sql.QueryTranslator; +import persistence.sql.QueryBuilder; import persistence.sql.ddl.entity.Person3; @DisplayName("3단계 요구사항 - @Entity, @Table, @Id, @Column, @Transient 어노테이션을 바탕으로 create 쿼리 만들어보기") -class QueryTranslator3Test { +class QueryBuilder3Test { - private static final Logger log = org.slf4j.LoggerFactory.getLogger(QueryTranslator3Test.class); + private static final Logger log = org.slf4j.LoggerFactory.getLogger(QueryBuilder3Test.class); private final Class entityClass = Person3.class; - private final QueryTranslator queryTranslator = new QueryTranslator(); + private final QueryBuilder queryBuilder = new QueryBuilder(); @Test @DisplayName("@Entity, @Table, @Id, @Column, @Transient 어노테이션을 바탕으로 create 쿼리 만들어보기") void createDDL() { - String ddl = queryTranslator.getCreateTableQuery(entityClass); + String ddl = queryBuilder.getCreateTableQuery(entityClass); log.debug("DDL: {}", ddl); @@ -34,7 +34,7 @@ void createDDL() { @Test @DisplayName("@Entity, @Table, @Id, @Column, @Transient 어노테이션을 바탕으로 drop 쿼리 만들어보기") void buildDropQuery() { - String dropQuery = queryTranslator.getDropTableQuery(entityClass); + String dropQuery = queryBuilder.getDropTableQuery(entityClass); log.debug("Drop query: {}", dropQuery); @@ -44,7 +44,7 @@ void buildDropQuery() { @Test @DisplayName("클래스 정보와 @Table 어노테이션을 바탕으로 테이블명 가져오기") void getTableNameByClassName() { - String tableName = queryTranslator.getTableNameFrom(entityClass); + String tableName = queryBuilder.getTableNameFrom(entityClass); log.debug("Table name: {}", tableName); @@ -54,7 +54,7 @@ void getTableNameByClassName() { @Test @DisplayName("클래스 정보와 @Id, @Column, @Transient 어노테이션을 바탕으로 컬럼 선언문 가져오기") void getColumnDefinitionStatement() { - String columnDefinitionStatement = queryTranslator.getTableColumnDefinitionFrom(entityClass); + String columnDefinitionStatement = queryBuilder.getColumnDefinitionsFrom(entityClass); log.debug("Column definition statement: {}", columnDefinitionStatement); @@ -74,7 +74,7 @@ void getColumnDefinitionStatementFromField( ) throws NoSuchFieldException { Field field = entityClass.getDeclaredField(fieldName); - String actualColumnDefinitionStatement = queryTranslator.getColumnDefinitionFrom(field); + String actualColumnDefinitionStatement = queryBuilder.getColumnDefinitionFrom(field); assertThat(actualColumnDefinitionStatement).isEqualTo(expectedColumnDefinitionStatement); } diff --git a/src/test/java/persistence/sql/ddl/query/QueryTranslator4Test.java b/src/test/java/persistence/sql/ddl/query/QueryBuilder4Test.java similarity index 75% rename from src/test/java/persistence/sql/ddl/query/QueryTranslator4Test.java rename to src/test/java/persistence/sql/ddl/query/QueryBuilder4Test.java index 7d6e3f6d2..896edc1cc 100644 --- a/src/test/java/persistence/sql/ddl/query/QueryTranslator4Test.java +++ b/src/test/java/persistence/sql/ddl/query/QueryBuilder4Test.java @@ -5,21 +5,21 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.slf4j.Logger; -import persistence.sql.QueryTranslator; +import persistence.sql.QueryBuilder; import persistence.sql.ddl.entity.Person4; @DisplayName("4단계 요구사항 - @Entity, @Table(schema), @Id, @Column, @Transient 어노테이션을 바탕으로 drop 쿼리 만들어보기") -class QueryTranslator4Test { - private static final Logger log = org.slf4j.LoggerFactory.getLogger(QueryTranslator4Test.class); +class QueryBuilder4Test { + private static final Logger log = org.slf4j.LoggerFactory.getLogger(QueryBuilder4Test.class); private final Class entityClass = Person4.class; - private final QueryTranslator queryTranslator = new QueryTranslator(); + private final QueryBuilder queryBuilder = new QueryBuilder(); @Test @DisplayName("@Entity, @Table(schema), @Id, @Column, @Transient 어노테이션을 바탕으로 drop 쿼리 만들어보기") void buildDropQuery() { - String dropQuery = queryTranslator.getDropTableQuery(entityClass); + String dropQuery = queryBuilder.getDropTableQuery(entityClass); log.debug("Drop query: {}", dropQuery); diff --git a/src/test/java/persistence/sql/ddl/query/QueryTranslatorTest.java b/src/test/java/persistence/sql/ddl/query/QueryBuilderTest.java similarity index 75% rename from src/test/java/persistence/sql/ddl/query/QueryTranslatorTest.java rename to src/test/java/persistence/sql/ddl/query/QueryBuilderTest.java index ce7307826..a113ac22f 100644 --- a/src/test/java/persistence/sql/ddl/query/QueryTranslatorTest.java +++ b/src/test/java/persistence/sql/ddl/query/QueryBuilderTest.java @@ -8,21 +8,21 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.slf4j.Logger; -import persistence.sql.QueryTranslator; +import persistence.sql.QueryBuilder; import persistence.sql.ddl.entity.Person; import persistence.sql.ddl.entity.Person4; -class QueryTranslatorTest { - private static final Logger log = org.slf4j.LoggerFactory.getLogger(QueryTranslatorTest.class); +class QueryBuilderTest { + private static final Logger log = org.slf4j.LoggerFactory.getLogger(QueryBuilderTest.class); private final Class entityClass = Person.class; - private final QueryTranslator queryTranslator = new QueryTranslator(); + private final QueryBuilder queryBuilder = new QueryBuilder(); @Test @DisplayName("@Entity, @Table, @Id, @Column, @Transient 어노테이션을 바탕으로 create 쿼리 만들어보기") void createDDL() { - String ddl = queryTranslator.getCreateTableQuery(entityClass); + String ddl = queryBuilder.getCreateTableQuery(entityClass); log.debug("DDL: {}", ddl); @@ -33,7 +33,7 @@ void createDDL() { @Test @DisplayName("@Entity, @Table(schema), @Id, @Column, @Transient 어노테이션을 바탕으로 create 쿼리 만들어보기") void createDDLWithSchema() { - String ddl = queryTranslator.getCreateTableQuery(Person4.class); + String ddl = queryBuilder.getCreateTableQuery(Person4.class); log.debug("DDL: {}", ddl); @@ -44,7 +44,7 @@ void createDDLWithSchema() { @Test @DisplayName("@Entity, @Table, @Id, @Column, @Transient 어노테이션을 바탕으로 drop 쿼리 만들어보기") void buildDropQuery() { - String dropQuery = queryTranslator.getDropTableQuery(entityClass); + String dropQuery = queryBuilder.getDropTableQuery(entityClass); log.debug("Drop query: {}", dropQuery); @@ -54,7 +54,7 @@ void buildDropQuery() { @Test @DisplayName("@Entity, @Table(schema), @Id, @Column, @Transient 어노테이션을 바탕으로 drop 쿼리 만들어보기") void buildDropQueryWithSchema() { - String dropQuery = queryTranslator.getDropTableQuery(Person4.class); + String dropQuery = queryBuilder.getDropTableQuery(Person4.class); log.debug("Drop query: {}", dropQuery); @@ -64,7 +64,7 @@ void buildDropQueryWithSchema() { @Test @DisplayName("클래스 정보와 @Table 어노테이션을 바탕으로 테이블명 가져오기") void getTableName() { - String tableName = queryTranslator.getTableNameFrom(entityClass); + String tableName = queryBuilder.getTableNameFrom(entityClass); log.debug("Table name: {}", tableName); @@ -74,7 +74,7 @@ void getTableName() { @Test @DisplayName("클래스 정보와 @Table(schema) 어노테이션을 바탕으로 테이블명 가져오기") void getTableNameWithSchema() { - String tableName = queryTranslator.getTableNameFrom(Person4.class); + String tableName = queryBuilder.getTableNameFrom(Person4.class); log.debug("Table name: {}", tableName); @@ -84,7 +84,7 @@ void getTableNameWithSchema() { @Test @DisplayName("클래스 정보와 @Id, @Column, @Transient 어노테이션을 바탕으로 컬럼 선언문 가져오기") void getColumnDefinitionStatement() { - String columnDefinitionStatement = queryTranslator.getTableColumnDefinitionFrom(entityClass); + String columnDefinitionStatement = queryBuilder.getColumnDefinitionsFrom(entityClass); log.debug("Column definition statement: {}", columnDefinitionStatement); @@ -104,7 +104,7 @@ void getColumnDefinitionStatementFromField( ) throws NoSuchFieldException { Field field = entityClass.getDeclaredField(fieldName); - String actualColumnDefinitionStatement = queryTranslator.getColumnDefinitionFrom(field); + String actualColumnDefinitionStatement = queryBuilder.getColumnDefinitionFrom(field); assertThat(actualColumnDefinitionStatement).isEqualTo(expectedColumnDefinitionStatement); } @@ -114,7 +114,7 @@ void getColumnDefinitionStatementFromField( void getInsertQuery() { Person person = new Person("홍길동", 20, "test@gamil.com"); - String insertQuery = queryTranslator.getInsertQuery(person); + String insertQuery = queryBuilder.getInsertQuery(person); assertThat(insertQuery).isEqualTo("INSERT INTO users (nick_name, old, email) VALUES ('홍길동', 20, 'test@gamil.com')"); } @@ -123,7 +123,7 @@ void getInsertQuery() { @DisplayName("요구사항 2 - 위의 정보를 바탕으로 모두 조회(findAll) 기능 구현해보기") void getSelectAllQuery() { // when - String selectAllQuery = queryTranslator.getSelectAllQuery(Person.class); + String selectAllQuery = queryBuilder.getSelectAllQuery(Person.class); // then assertThat(selectAllQuery).isEqualTo("SELECT id, nick_name, old, email FROM users"); @@ -133,12 +133,22 @@ void getSelectAllQuery() { @DisplayName("요구사항 3 - 위의 정보를 바탕으로 단건 조회(findById) 기능 구현해보기") void getSelectByIdQuery() { // when - String selectByIdQuery = queryTranslator.getSelectByIdQuery(Person.class, 1L); + String selectByIdQuery = queryBuilder.getSelectByIdQuery(Person.class, 1L); // then assertThat(selectByIdQuery).isEqualTo("SELECT id, nick_name, old, email FROM users WHERE id = 1"); } + @Test + @DisplayName("요구사항 3 - 위의 정보를 바탕으로 count 쿼리 만들어보기") + void getSelectCountQuery() { + // when + String selectCountQuery = queryBuilder.getSelectCountQuery(Person.class); + + // then + assertThat(selectCountQuery).isEqualTo("SELECT COUNT(id) FROM users"); + } + @Test @DisplayName("요구사항 4 - 위의 정보를 바탕으로 delete 쿼리 만들어보기") void getDeleteAllQuery() { @@ -147,17 +157,13 @@ void getDeleteAllQuery() { Person givenEntity = new Person(givenId, "홍길동", 20, "test@gmail.com"); // when - String deleteAllQuery = queryTranslator.getDeleteAllQuery(Person.class); - String deleteByIdQuery = queryTranslator.getDeleteByIdQuery(Person.class, givenId); - String deleteQueryFromEntity = queryTranslator.getDeleteQueryFromEntity( - Person.class, - givenEntity - ); + String deleteAllQuery = queryBuilder.getDeleteAllQuery(Person.class); + String deleteByIdQuery = queryBuilder.getDeleteByIdQuery(Person.class, givenId); + String deleteQueryFromEntity = queryBuilder.getDeleteQueryFromEntity(givenEntity); // then assertThat(deleteAllQuery).isEqualTo("DELETE FROM users"); assertThat(deleteByIdQuery).isEqualTo("DELETE FROM users WHERE id = 1"); assertThat(deleteQueryFromEntity).isEqualTo("DELETE FROM users WHERE id = 1"); - } } diff --git a/src/test/java/persistence/sql/dml/ColumnValueTranslatorTest.java b/src/test/java/persistence/sql/dml/ColumnValueTranslatorTest.java deleted file mode 100644 index ce362481f..000000000 --- a/src/test/java/persistence/sql/dml/ColumnValueTranslatorTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package persistence.sql.dml; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import persistence.sql.ddl.entity.Person; - -class ColumnValueTranslatorTest { - - private final ColumnValueTranslator columnValueTranslator = new ColumnValueTranslator(); - - @DisplayName("엔티티 객체를 통해서 정상적으로 Column Value Clause 쿼리를 가져올 수 있다.") - @Test - void getColumnValueClause() { - // given - Person person = new Person("nick", 20, "nick@gmail.com"); - - // when - String columnClause = columnValueTranslator.getColumnValueClause(person); - - // then - assertThat(columnClause).isEqualTo("'nick', 20, 'nick@gmail.com'"); - } -}