diff --git a/src/main/java/example/entity/Order.java b/src/main/java/example/entity/Order.java new file mode 100644 index 00000000..883bbf8d --- /dev/null +++ b/src/main/java/example/entity/Order.java @@ -0,0 +1,40 @@ +package example.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import java.util.List; + +@Entity +@Table(name = "orders") +public class Order { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String orderNumber; + @OneToMany(fetch = FetchType.EAGER) + @JoinColumn(name = "order_id", referencedColumnName = "order_id") + private List orderItems; + + public String getOrderNumber() { + return orderNumber; + } + + public void setOrderNumber(String orderNumber) { + this.orderNumber = orderNumber; + } + +// public List getOrderItems() { +// return orderItems; +// } +// +// public void setOrderItems(List orderItems) { +// this.orderItems = orderItems; +// } +} diff --git a/src/main/java/example/entity/OrderItem.java b/src/main/java/example/entity/OrderItem.java new file mode 100644 index 00000000..c596f802 --- /dev/null +++ b/src/main/java/example/entity/OrderItem.java @@ -0,0 +1,44 @@ +package example.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "order_items") +public class OrderItem { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String product; + private Integer quantity; + @Column(name = "order_id") + private Long orderId; + + public String getProduct() { + return product; + } + + public void setProduct(String product) { + this.product = product; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + public Long getOrderId() { + return orderId; + } + + public void setOrderId(Long orderId) { + this.orderId = orderId; + } +} diff --git a/src/main/java/jdbc/EntityRowMapper.java b/src/main/java/jdbc/EntityRowMapper.java index 64bb0135..ddfb635a 100644 --- a/src/main/java/jdbc/EntityRowMapper.java +++ b/src/main/java/jdbc/EntityRowMapper.java @@ -1,5 +1,6 @@ package jdbc; +import jakarta.persistence.JoinColumn; import jakarta.persistence.Transient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,7 +24,8 @@ public T mapRow(ResultSet resultSet) { T entity = this.entityClass.getDeclaredConstructor().newInstance(); for (Field field : this.entityClass.getDeclaredFields()) { - if (field.isAnnotationPresent(Transient.class)) { + if (field.isAnnotationPresent(Transient.class) + || field.isAnnotationPresent(JoinColumn.class)) { continue; } field.setAccessible(true); diff --git a/src/main/java/jdbc/JdbcTemplate.java b/src/main/java/jdbc/JdbcTemplate.java index 198d1a02..6528112d 100644 --- a/src/main/java/jdbc/JdbcTemplate.java +++ b/src/main/java/jdbc/JdbcTemplate.java @@ -1,5 +1,8 @@ package jdbc; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.sql.Connection; import java.sql.ResultSet; import java.sql.Statement; @@ -7,6 +10,7 @@ import java.util.List; public class JdbcTemplate { + private static final Logger logger = LoggerFactory.getLogger(JdbcTemplate.class); private final Connection connection; public JdbcTemplate(final Connection connection) { @@ -19,6 +23,7 @@ public void execute(final String sql) { } catch (Exception e) { throw new RuntimeException(e); } + logger.debug("executed query : {}", sql); } public Long executeAndReturnGeneratedKey(final String sql) { @@ -32,6 +37,7 @@ public Long executeAndReturnGeneratedKey(final String sql) { } catch (Exception e) { throw new RuntimeException(e); } + logger.debug("executed query : {}", sql); return null; } @@ -40,10 +46,11 @@ public T queryForObject(final String sql, final RowMapper rowMapper) { if (results.size() != 1) { throw new RuntimeException("Expected 1 result, got " + results.size()); } + logger.debug("executed query : {}", sql); return results.get(0); } - public List query(final String sql, final RowMapper rowMapper) { + private List query(final String sql, final RowMapper rowMapper) { try (final ResultSet resultSet = connection.prepareStatement(sql).executeQuery()) { final List result = new ArrayList<>(); while (resultSet.next()) { diff --git a/src/main/java/persistence/EntityScanner.java b/src/main/java/persistence/EntityScanner.java index 23d683b6..ea701e62 100644 --- a/src/main/java/persistence/EntityScanner.java +++ b/src/main/java/persistence/EntityScanner.java @@ -1,9 +1,9 @@ package persistence; import jakarta.persistence.Entity; -import jakarta.persistence.Transient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import persistence.entity.EntityUtils; import persistence.sql.NameUtils; import persistence.sql.ddl.create.CreateQueryBuilder; import persistence.sql.ddl.create.component.column.ColumnComponentBuilder; @@ -77,12 +77,9 @@ public List getDdlDropQueries() { } private String generateDdlCreateQuery(Class entityClass) { - Field[] fields = entityClass.getDeclaredFields(); CreateQueryBuilder queryBuilder = CreateQueryBuilder.newInstance(); - for (Field field : fields) { - if (field.isAnnotationPresent(Transient.class)) { - continue; - } + + for (Field field : EntityUtils.getManagedFields(entityClass)) { queryBuilder.add(ColumnComponentBuilder.from(field)); queryBuilder.add(ConstraintComponentBuilder.from(field)); } diff --git a/src/main/java/persistence/entity/EntityLoader.java b/src/main/java/persistence/entity/EntityLoader.java index add421dd..f5ad2eba 100644 --- a/src/main/java/persistence/entity/EntityLoader.java +++ b/src/main/java/persistence/entity/EntityLoader.java @@ -2,8 +2,19 @@ import jdbc.EntityRowMapper; import jdbc.JdbcTemplate; +import persistence.sql.component.ColumnInfo; +import persistence.sql.component.ConditionBuilder; +import persistence.sql.component.JoinCondition; +import persistence.sql.component.JoinConditionBuilder; +import persistence.sql.component.JoinInfo; +import persistence.sql.component.JoinType; +import persistence.sql.component.TableInfo; import persistence.sql.dml.select.SelectQueryBuilder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + public class EntityLoader { private final JdbcTemplate jdbcTemplate; @@ -11,8 +22,45 @@ public EntityLoader(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public Object find(Class clazz, Long id) { - String findByIdQuery = SelectQueryBuilder.generateQuery(clazz, id); - return jdbcTemplate.queryForObject(findByIdQuery, new EntityRowMapper<>(clazz)); + public Object find(Class entityClass, Long id) { + TableInfo fromTable = TableInfo.from(entityClass); + ColumnInfo idColumn = fromTable.getIdColumn(); + + SelectQueryBuilder selectQueryBuilder = new SelectQueryBuilder() + .fromTableInfo(fromTable); + + List joinConditions = getJoinConditions(fromTable); + if (!joinConditions.isEmpty()) { + selectQueryBuilder + .joinConditions(joinConditions); + } + + selectQueryBuilder + .whereCondition( + new ConditionBuilder() + .columnInfo(idColumn) + .values(Collections.singletonList(id.toString())) + .build() + ); + + String query = selectQueryBuilder.build().toString(); + return jdbcTemplate.queryForObject(query, new EntityRowMapper<>(entityClass)); + } + + private List getJoinConditions(TableInfo tableInfo) { + List joinConditions = new ArrayList<>(); + + List joinInfos = tableInfo.getJoinInfos(); + for (JoinInfo joinInfo : joinInfos) { + JoinCondition joinCondition = new JoinConditionBuilder() + .joinType(JoinType.LEFT_JOIN) + .tableInfo(joinInfo.getTargetColumnInfo().getTableInfo()) + .sourceColumnInfo(joinInfo.getSourceColumnInfo()) + .targetColumnInfo(joinInfo.getTargetColumnInfo()) + .build(); + joinConditions.add(joinCondition); + } + + return joinConditions; } } diff --git a/src/main/java/persistence/entity/EntityManagerImpl.java b/src/main/java/persistence/entity/EntityManagerImpl.java index ac7705ba..86c7e010 100644 --- a/src/main/java/persistence/entity/EntityManagerImpl.java +++ b/src/main/java/persistence/entity/EntityManagerImpl.java @@ -44,19 +44,19 @@ private void executeEntityActions(List entryList) { } @Override - public Object find(Class clazz, Long id) { + public Object find(Class entityClass, Long id) { if (id == null) { throw new IllegalArgumentException("Unable to find without an ID!"); } /* 관리 중인 엔티티라면 영속성 컨텍스트에서 꺼내서 반환 */ - Object managedEntity = persistenceContext.getEntity(clazz, id); + Object managedEntity = persistenceContext.getEntity(entityClass, id); if (managedEntity != null) { return managedEntity; } /* 관리 중이지 않다면 DB 조회 */ - Object entity = entityLoader.find(clazz, id); + Object entity = entityLoader.find(entityClass, id); persistenceContext.putEntity(entity); return entity; } diff --git a/src/main/java/persistence/entity/EntityUtils.java b/src/main/java/persistence/entity/EntityUtils.java index 153fa0cb..1f3e3121 100644 --- a/src/main/java/persistence/entity/EntityUtils.java +++ b/src/main/java/persistence/entity/EntityUtils.java @@ -3,6 +3,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.Transient; import java.lang.reflect.Field; @@ -25,15 +26,25 @@ public static Field[] getManagedFields(Class clazz) { .toArray(Field[]::new); } + public static Field[] getManagedFieldsExceptId(Class clazz) { + return Arrays.stream(clazz.getDeclaredFields()) + .filter(EntityUtils::isManagedField) + .filter(field -> !field.isAnnotationPresent(Id.class)) + .toArray(Field[]::new); + } + private static boolean isManagedField(Field field) { if (field.isAnnotationPresent(Transient.class)) { return false; } - if (field.isAnnotationPresent(Id.class) - && field.isAnnotationPresent(GeneratedValue.class) - && GenerationType.IDENTITY.equals(field.getAnnotation(GeneratedValue.class).strategy())) { + if (field.isAnnotationPresent(JoinColumn.class)) { return false; } +// if (field.isAnnotationPresent(Id.class) +// && field.isAnnotationPresent(GeneratedValue.class) +// && GenerationType.IDENTITY.equals(field.getAnnotation(GeneratedValue.class).strategy())) { +// return false; +// } return true; } diff --git a/src/main/java/persistence/sql/NameUtils.java b/src/main/java/persistence/sql/NameUtils.java index 95f12c0d..6cf3699f 100644 --- a/src/main/java/persistence/sql/NameUtils.java +++ b/src/main/java/persistence/sql/NameUtils.java @@ -1,6 +1,7 @@ package persistence.sql; import jakarta.persistence.Column; +import jakarta.persistence.JoinColumn; import jakarta.persistence.Table; import java.lang.reflect.Field; diff --git a/src/main/java/persistence/sql/component/ColumnInfo.java b/src/main/java/persistence/sql/component/ColumnInfo.java new file mode 100644 index 00000000..88f163e0 --- /dev/null +++ b/src/main/java/persistence/sql/component/ColumnInfo.java @@ -0,0 +1,31 @@ +package persistence.sql.component; + +import persistence.sql.NameUtils; + +import java.lang.reflect.Field; + +public class ColumnInfo { + private TableInfo tableInfo; + private Field columnField; + + private ColumnInfo(TableInfo tableInfo, Field field) { + this.tableInfo = tableInfo; + this.columnField = field; + } + + public static ColumnInfo of(TableInfo tableInfo, Field columnField) { + return new ColumnInfo(tableInfo, columnField); + } + + public TableInfo getTableInfo() { + return tableInfo; + } + + public Field getColumnField() { + return columnField; + } + + public String getFullName() { + return tableInfo.getTableName() + "." + NameUtils.getColumnName(columnField); + } +} diff --git a/src/main/java/persistence/sql/component/Condition.java b/src/main/java/persistence/sql/component/Condition.java new file mode 100644 index 00000000..cbdccf66 --- /dev/null +++ b/src/main/java/persistence/sql/component/Condition.java @@ -0,0 +1,39 @@ +package persistence.sql.component; + +import java.util.List; + +public class Condition { + private ColumnInfo columnInfo; + private List values; + private Condition andCondition; + private Condition orCondition; + + public ColumnInfo getColumnInfo() { + return columnInfo; + } + + public List getValues() { + return values; + } + + public Condition getAndCondition() { + return andCondition; + } + + public Condition getOrCondition() { + return orCondition; + } + + public Condition(ColumnInfo columnInfo, List values) { + this.columnInfo = columnInfo; + this.values = values; + } + + public void setAndCondition(Condition andCondition) { + this.andCondition = andCondition; + } + + public void setOrCondition(Condition orCondition) { + this.orCondition = orCondition; + } +} \ No newline at end of file diff --git a/src/main/java/persistence/sql/component/ConditionBuilder.java b/src/main/java/persistence/sql/component/ConditionBuilder.java new file mode 100644 index 00000000..f58ee513 --- /dev/null +++ b/src/main/java/persistence/sql/component/ConditionBuilder.java @@ -0,0 +1,41 @@ +package persistence.sql.component; + +import java.util.List; + +public class ConditionBuilder { + private ColumnInfo columnInfo; + private List values; + private Condition andCondition; + private Condition orCondition; + + public ConditionBuilder columnInfo(ColumnInfo columnInfo) { + this.columnInfo = columnInfo; + return this; + } + + public ConditionBuilder values(List values) { + this.values = values; + return this; + } + + public ConditionBuilder andCondition(Condition andCondition) { + this.andCondition = andCondition; + return this; + } + + public ConditionBuilder orCondition(Condition orCondition) { + this.orCondition = orCondition; + return this; + } + + public Condition build() { + Condition condition = new Condition(columnInfo, values); + if (andCondition != null) { + condition.setAndCondition(andCondition); + } + if (orCondition != null) { + condition.setOrCondition(orCondition); + } + return condition; + } +} diff --git a/src/main/java/persistence/sql/component/JoinCondition.java b/src/main/java/persistence/sql/component/JoinCondition.java new file mode 100644 index 00000000..9d562c1a --- /dev/null +++ b/src/main/java/persistence/sql/component/JoinCondition.java @@ -0,0 +1,31 @@ +package persistence.sql.component; + +public class JoinCondition { + private JoinType joinType; + private TableInfo tableInfo; + private ColumnInfo sourceColumnInfo; + private ColumnInfo targetColumnInfo; + + public JoinType getJoinType() { + return joinType; + } + + public TableInfo getTableInfo() { + return tableInfo; + } + + public ColumnInfo getSourceColumnInfo() { + return sourceColumnInfo; + } + + public ColumnInfo getTargetColumnInfo() { + return targetColumnInfo; + } + + public JoinCondition(JoinType joinType, TableInfo tableInfo, ColumnInfo onConditionColumn1, ColumnInfo onConditionColumn2) { + this.joinType = joinType; + this.tableInfo = tableInfo; + this.sourceColumnInfo = onConditionColumn1; + this.targetColumnInfo = onConditionColumn2; + } +} diff --git a/src/main/java/persistence/sql/component/JoinConditionBuilder.java b/src/main/java/persistence/sql/component/JoinConditionBuilder.java new file mode 100644 index 00000000..4be8d7ab --- /dev/null +++ b/src/main/java/persistence/sql/component/JoinConditionBuilder.java @@ -0,0 +1,32 @@ +package persistence.sql.component; + +public class JoinConditionBuilder { + private JoinType joinType; + private TableInfo tableInfo; + private ColumnInfo sourceColumnInfo; + private ColumnInfo targetColumnInfo; + + public JoinConditionBuilder joinType(JoinType joinType) { + this.joinType = joinType; + return this; + } + + public JoinConditionBuilder tableInfo(TableInfo tableInfo) { + this.tableInfo = tableInfo; + return this; + } + + public JoinConditionBuilder sourceColumnInfo(ColumnInfo sourceColumnInfo) { + this.sourceColumnInfo = sourceColumnInfo; + return this; + } + + public JoinConditionBuilder targetColumnInfo(ColumnInfo targetColumnInfo) { + this.targetColumnInfo = targetColumnInfo; + return this; + } + + public JoinCondition build() { + return new JoinCondition(joinType, tableInfo, sourceColumnInfo, targetColumnInfo); + } +} diff --git a/src/main/java/persistence/sql/component/JoinInfo.java b/src/main/java/persistence/sql/component/JoinInfo.java new file mode 100644 index 00000000..333a55eb --- /dev/null +++ b/src/main/java/persistence/sql/component/JoinInfo.java @@ -0,0 +1,31 @@ +package persistence.sql.component; + +import java.lang.reflect.Field; + +public class JoinInfo { + private Field sourceTableJoinField; + private ColumnInfo sourceColumnInfo; + private ColumnInfo targetColumnInfo; + + private JoinInfo(Field sourceTableJoinField, ColumnInfo sourceColumnInfo, ColumnInfo targetColumnInfo) { + this.sourceTableJoinField = sourceTableJoinField; + this.sourceColumnInfo = sourceColumnInfo; + this.targetColumnInfo = targetColumnInfo; + } + + public static JoinInfo of(Field sourceTableJoinField, ColumnInfo sourceColumnInfo, ColumnInfo targetColumnInfo) { + return new JoinInfo(sourceTableJoinField, sourceColumnInfo, targetColumnInfo); + } + + public Field getSourceTableJoinField() { + return sourceTableJoinField; + } + + public ColumnInfo getSourceColumnInfo() { + return sourceColumnInfo; + } + + public ColumnInfo getTargetColumnInfo() { + return targetColumnInfo; + } +} diff --git a/src/main/java/persistence/sql/component/JoinType.java b/src/main/java/persistence/sql/component/JoinType.java new file mode 100644 index 00000000..9c936c59 --- /dev/null +++ b/src/main/java/persistence/sql/component/JoinType.java @@ -0,0 +1,16 @@ +package persistence.sql.component; + +public enum JoinType { + INNER_JOIN("inner join"), + LEFT_JOIN("left join"), + /* TODO */; + private String value; + + JoinType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/persistence/sql/component/TableInfo.java b/src/main/java/persistence/sql/component/TableInfo.java new file mode 100644 index 00000000..0fb04fc7 --- /dev/null +++ b/src/main/java/persistence/sql/component/TableInfo.java @@ -0,0 +1,73 @@ +package persistence.sql.component; + +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import persistence.sql.NameUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; + +public class TableInfo { + private Class entityClass; + private String tableName; + + private TableInfo(Class entityClass) { + this.entityClass = entityClass; + this.tableName = NameUtils.getTableName(entityClass); + } + + public static TableInfo from(Class entityClass) { + return new TableInfo(entityClass); + } + + public String getTableName() { + return tableName; + } + + public ColumnInfo getIdColumn() { + Field idColumnField = Arrays.stream(entityClass.getDeclaredFields()) + .filter(field -> field.isAnnotationPresent(Id.class)) + .findAny() + .orElseThrow(); + return ColumnInfo.of(this, idColumnField); + } + + public ColumnInfo getColumn(String columnName) { + Field columnField = Arrays.stream(entityClass.getDeclaredFields()) + .filter(field -> columnName.equals(NameUtils.getColumnName(field))) + .findAny() + .orElseThrow(); + return ColumnInfo.of(this, columnField); + } + + public List getJoinInfos() { + return Arrays.stream(entityClass.getDeclaredFields()) + .filter(field -> field.isAnnotationPresent(JoinColumn.class)) + .map(this::getJoinInfo) + .toList(); + } + + private JoinInfo getJoinInfo(Field sourceTableJoinField) { + return JoinInfo.of(sourceTableJoinField, this.getIdColumn(), getJoinTargetColumnInfo(sourceTableJoinField)); + } + + private ColumnInfo getJoinTargetColumnInfo(Field field) { + String joinTargetColumnName = field.getAnnotation(JoinColumn.class).referencedColumnName(); + + Type type = field.getGenericType(); + + if (type instanceof ParameterizedType parameterizedType) { + type = parameterizedType.getActualTypeArguments()[0]; + } + + if (type instanceof Class clazz) { + TableInfo joinTargetTableInfo = TableInfo.from((Class) clazz); + return joinTargetTableInfo.getColumn(joinTargetColumnName); + } + + throw new RuntimeException("Unable to cast to appropriate class type!"); + } +} diff --git a/src/main/java/persistence/sql/dml/insert/InsertQueryBuilder.java b/src/main/java/persistence/sql/dml/insert/InsertQueryBuilder.java index edb5a200..8b73d2a9 100644 --- a/src/main/java/persistence/sql/dml/insert/InsertQueryBuilder.java +++ b/src/main/java/persistence/sql/dml/insert/InsertQueryBuilder.java @@ -28,7 +28,7 @@ public static String generateQuery(Object entity) { private static String columnClause(Class clazz) { StringBuilder stringBuilder = new StringBuilder(" ("); - Field[] managedFields = EntityUtils.getManagedFields(clazz); + Field[] managedFields = EntityUtils.getManagedFieldsExceptId(clazz); for (Field field : managedFields) { stringBuilder .append(NameUtils.getColumnName(field)) @@ -45,10 +45,14 @@ private static String valueClause(Object entity) { StringBuilder stringBuilder = new StringBuilder("("); Class clazz = entity.getClass(); - Field[] fields = EntityUtils.getManagedFields(clazz); + Field[] fields = EntityUtils.getManagedFieldsExceptId(clazz); for (Field field : fields) { field.setAccessible(true); Object fieldValue = EntityUtils.getFieldValue(field, entity); + if (fieldValue == null) { + stringBuilder.append("null, "); + continue; + } stringBuilder .append("'") .append(fieldValue) diff --git a/src/main/java/persistence/sql/dml/select/SelectQuery.java b/src/main/java/persistence/sql/dml/select/SelectQuery.java new file mode 100644 index 00000000..aa2d1a63 --- /dev/null +++ b/src/main/java/persistence/sql/dml/select/SelectQuery.java @@ -0,0 +1,124 @@ +package persistence.sql.dml.select; + +import persistence.sql.component.ColumnInfo; +import persistence.sql.component.Condition; +import persistence.sql.component.JoinCondition; +import persistence.sql.component.TableInfo; + +import java.util.List; + +public class SelectQuery { + private List selectColumnInfos; + private TableInfo fromTableInfo; + private Condition whereCondition; + private List joinConditions; + + public SelectQuery(TableInfo fromTableInfo, Condition whereCondition) { + this.fromTableInfo = fromTableInfo; + this.whereCondition = whereCondition; + } + + public void setSelectColumnInfos(List selectColumnInfos) { + this.selectColumnInfos = selectColumnInfos; + } + + public void setJoinConditions(List joinConditions) { + this.joinConditions = joinConditions; + } + + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder + .append(getSelectClause()) + .append(getFromClause()); + if (joinConditions != null) { + stringBuilder.append(getJoinClauses()); + } + if (whereCondition != null) { + stringBuilder.append(getWhereClause()); + } + + stringBuilder.setLength(stringBuilder.length() - 1); + stringBuilder.append(";"); + return stringBuilder.toString(); + } + + private String getSelectClause() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("select "); + + if (selectColumnInfos == null) { + stringBuilder.append("* "); + return stringBuilder.toString(); + } + + selectColumnInfos.forEach( + columnInfo -> stringBuilder + .append(columnInfo.getFullName()) + .append(", ") + ); + stringBuilder.setLength(stringBuilder.length() - 2); + stringBuilder.append(" "); + return stringBuilder.toString(); + } + + private String getFromClause() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder + .append("from ") + .append(fromTableInfo.getTableName()) + .append(" "); + return stringBuilder.toString(); + } + + private String getWhereClause() { + StringBuilder stringBuilder = new StringBuilder(); + ColumnInfo columnInfo = whereCondition.getColumnInfo(); + + stringBuilder + .append("where ") + .append(columnInfo.getFullName()) + .append(" "); + + List values = whereCondition.getValues(); + if (values.isEmpty()) { + stringBuilder.append("= null"); + } else if (values.size() == 1) { + stringBuilder.append("= ").append(values.get(0)); + } else { + stringBuilder.append("in ("); + values.forEach(value -> stringBuilder.append(value).append(", ")); + stringBuilder.setLength(stringBuilder.length() - 2); + stringBuilder.append(")"); + } + /* todo : andCondition and orCondition */ + stringBuilder.append(" "); + return stringBuilder.toString(); + } + + private String getJoinClauses() { + StringBuilder stringBuilder = new StringBuilder(); + joinConditions.forEach( + joinCondition -> stringBuilder.append(getSingleJoinClause(joinCondition)) + ); + return stringBuilder.toString(); + } + + private String getSingleJoinClause(JoinCondition joinCondition) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder + .append(joinCondition.getJoinType().getValue()) + .append(" ") + .append(joinCondition.getTableInfo().getTableName()) + .append(" on "); + ColumnInfo sourceColumnInfo = joinCondition.getSourceColumnInfo(); + stringBuilder + .append(sourceColumnInfo.getFullName()) + .append(" = "); + ColumnInfo targetColumnInfo = joinCondition.getTargetColumnInfo(); + stringBuilder + .append(targetColumnInfo.getFullName()) + .append(" "); + return stringBuilder.toString(); + } +} diff --git a/src/main/java/persistence/sql/dml/select/SelectQueryBuilder.java b/src/main/java/persistence/sql/dml/select/SelectQueryBuilder.java index 59627024..6276c3b8 100644 --- a/src/main/java/persistence/sql/dml/select/SelectQueryBuilder.java +++ b/src/main/java/persistence/sql/dml/select/SelectQueryBuilder.java @@ -1,47 +1,46 @@ package persistence.sql.dml.select; -import jakarta.persistence.Id; -import persistence.sql.NameUtils; +import persistence.sql.component.ColumnInfo; +import persistence.sql.component.Condition; +import persistence.sql.component.JoinCondition; +import persistence.sql.component.TableInfo; -import java.lang.reflect.Field; +import java.util.List; public class SelectQueryBuilder { - private SelectQueryBuilder() { + private List selectColumnInfos; + private TableInfo fromTableInfo; + private Condition whereCondition; + private List joinConditions; + + public SelectQueryBuilder selectColumnInfos(List selectColumnInfos) { + this.selectColumnInfos = selectColumnInfos; + return this; } - public static String generateQuery(Class entityClass) { - String tableName = NameUtils.getTableName(entityClass); + public SelectQueryBuilder fromTableInfo(TableInfo fromTableInfo) { + this.fromTableInfo = fromTableInfo; + return this; + } - StringBuilder stringBuilder = new StringBuilder(); - stringBuilder - .append("select * from ") - .append(tableName) - .append(";"); - return stringBuilder.toString(); + public SelectQueryBuilder whereCondition(Condition whereCondition) { + this.whereCondition = whereCondition; + return this; } - public static String generateQuery(Class entityClass, Long id) { - String tableName = NameUtils.getTableName(entityClass); - String idColumnName = NameUtils.getColumnName(getIdColumn(entityClass)); - - StringBuilder stringBuilder = new StringBuilder(); - stringBuilder - .append("select * from ") - .append(tableName) - .append(" where ") - .append(idColumnName) - .append(" = ") - .append(id.toString()) - .append(";"); - return stringBuilder.toString(); + public SelectQueryBuilder joinConditions(List joinConditions) { + this.joinConditions = joinConditions; + return this; } - private static Field getIdColumn(Class entityClass) { - for (Field field : entityClass.getDeclaredFields()) { - if (field.isAnnotationPresent(Id.class)) { - return field; - } + public SelectQuery build() { + SelectQuery selectQuery = new SelectQuery(fromTableInfo, whereCondition); + if (joinConditions != null) { + selectQuery.setJoinConditions(joinConditions); + } + if (selectColumnInfos != null) { + selectQuery.setSelectColumnInfos(selectColumnInfos); } - throw new IllegalArgumentException("Inappropriate entity class!"); + return selectQuery; } } diff --git a/src/test/java/persistence/entity/EntityManagerImplTest.java b/src/test/java/persistence/entity/EntityManagerImplTest.java index 7b9cc032..0b856411 100644 --- a/src/test/java/persistence/entity/EntityManagerImplTest.java +++ b/src/test/java/persistence/entity/EntityManagerImplTest.java @@ -2,6 +2,8 @@ import database.DatabaseServer; import database.H2; +import example.entity.Order; +import example.entity.OrderItem; import example.entity.Person; import jdbc.JdbcTemplate; import org.junit.jupiter.api.AfterEach; @@ -14,6 +16,7 @@ import persistence.EntityScanner; import java.sql.SQLException; +import java.util.UUID; public class EntityManagerImplTest { private static final Logger logger = LoggerFactory.getLogger(EntityManagerImplTest.class); @@ -168,4 +171,32 @@ void removeInTransactionTest() { EntityLoader entityLoader = new EntityLoader(jdbcTemplate); Assertions.assertThrows(RuntimeException.class, () -> entityLoader.find(Person.class, 1L)); } + + @Test + @DisplayName("test") + void test() { + EntityManagerImpl entityManagerImpl = new EntityManagerImpl(jdbcTemplate); + entityManagerImpl.beginTransaction(); + + Order insertingOrder = new Order(); + insertingOrder.setOrderNumber(UUID.randomUUID().toString()); + + OrderItem insertingOrderItem1 = new OrderItem(); + insertingOrderItem1.setProduct("P-001"); + insertingOrderItem1.setQuantity(10); + insertingOrderItem1.setOrderId(1L); + + OrderItem insertingOrderItem2 = new OrderItem(); + insertingOrderItem2.setProduct("P-002"); + insertingOrderItem2.setQuantity(15); + insertingOrderItem2.setOrderId(1L); + + entityManagerImpl.persist(insertingOrder); + entityManagerImpl.persist(insertingOrderItem1); + entityManagerImpl.persist(insertingOrderItem2); + + Object o = entityManagerImpl.find(Order.class, 1L); + + logger.debug("{}", o); + } } diff --git a/src/test/java/persistence/sql/dml/select/SelectQueryBuilderTest.java b/src/test/java/persistence/sql/dml/select/SelectQueryBuilderTest.java index ea4b00e3..8ec97695 100644 --- a/src/test/java/persistence/sql/dml/select/SelectQueryBuilderTest.java +++ b/src/test/java/persistence/sql/dml/select/SelectQueryBuilderTest.java @@ -1,10 +1,25 @@ package persistence.sql.dml.select; +import example.entity.Order; +import example.entity.OrderItem; import example.entity.Person; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import persistence.sql.component.ColumnInfo; +import persistence.sql.component.ConditionBuilder; +import persistence.sql.component.JoinCondition; +import persistence.sql.component.JoinConditionBuilder; +import persistence.sql.component.JoinInfo; +import persistence.sql.component.JoinType; +import persistence.sql.component.TableInfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; public class SelectQueryBuilderTest { private static final Logger logger = LoggerFactory.getLogger(SelectQueryBuilderTest.class); @@ -13,18 +28,71 @@ public class SelectQueryBuilderTest { @DisplayName("FindAll query 테스트") void findAllQueryTest() { Class personClass = Person.class; - String selectQuery = SelectQueryBuilder.generateQuery(personClass); - - logger.debug("query : {}", selectQuery); + SelectQuery selectQuery = new SelectQueryBuilder() + .fromTableInfo(TableInfo.from(personClass)) + .build(); + String query = selectQuery.toString(); + logger.debug(query); + assertThat(query).isEqualTo("select * from users;"); } @Test @DisplayName("FindById query 테스트") void findByIdTest() { Class personClass = Person.class; + TableInfo personTable = TableInfo.from(personClass); + ColumnInfo idColumn = personTable.getIdColumn(); + + SelectQuery selectQuery = new SelectQueryBuilder() + .fromTableInfo(personTable) + .whereCondition( + new ConditionBuilder() + .columnInfo(idColumn) + .values(Collections.singletonList("1")) + .build() + ) + .build(); + String query = selectQuery.toString(); + logger.debug(query); + assertThat(query).isEqualTo("select * from users where users.id = 1;"); + } + + @Test + @DisplayName("Find with join test") + void findWithJoinTest() { + Class orderClass = Order.class; + Class orderItemClass = OrderItem.class; + + TableInfo orderTableInfo = TableInfo.from(orderClass); + + List joinConditions = new ArrayList<>(); + + List joinInfos = orderTableInfo.getJoinInfos(); + for (JoinInfo joinInfo : joinInfos) { + JoinCondition joinCondition = new JoinConditionBuilder() + .joinType(JoinType.LEFT_JOIN) + .tableInfo(joinInfo.getTargetColumnInfo().getTableInfo()) + .sourceColumnInfo(joinInfo.getSourceColumnInfo()) + .targetColumnInfo(joinInfo.getTargetColumnInfo()) + .build(); + joinConditions.add(joinCondition); + } + + ColumnInfo orderId = orderTableInfo.getIdColumn(); - String selectQuery = SelectQueryBuilder.generateQuery(personClass, 1L); + SelectQuery selectQuery = new SelectQueryBuilder() + .fromTableInfo(orderTableInfo) + .joinConditions(joinConditions) + .whereCondition( + new ConditionBuilder() + .columnInfo(orderId) + .values(Collections.singletonList("1")) + .build() + ) + .build(); - logger.debug("query : {}", selectQuery); + String query = selectQuery.toString(); + logger.debug(query); + assertThat(query).isEqualTo("select * from orders left join order_items on orders.id = order_items.order_id where orders.id = 1;"); } }