diff --git a/README.md b/README.md index 5e6c2d36d..32f77f5a5 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,8 @@ ## 3단계 ### 요구 사항1 - 만들었던 PersistenceContext 에서 효율적인 메모리 관리를 위한 기능 구현 (1차 캐싱)[O] -### 요구 사항2 - 더티체킹 구현[] +### 요구 사항2 - 더티체킹 구현[O] + +## 4단계 +### 요구 사항 1 - Entity 의 라이프 사이클 관리 작업 수행 시 엔터티의 상태를 추가[O] + diff --git a/src/main/java/jdbc/JdbcTemplate.java b/src/main/java/jdbc/JdbcTemplate.java index 45217fb14..f86919fac 100644 --- a/src/main/java/jdbc/JdbcTemplate.java +++ b/src/main/java/jdbc/JdbcTemplate.java @@ -25,6 +25,21 @@ public void execute(final String sql) { } } + public Long executeAndReturnGeneratedKey(final String sql) { + try (final Statement statement = connection.createStatement()) { + statement.execute(sql, Statement.RETURN_GENERATED_KEYS); + try (ResultSet generatedKeys = statement.getGeneratedKeys()) { + if (generatedKeys.next()) { + return generatedKeys.getLong(1); + } else { + throw new RuntimeException("No ID generated for the insert."); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + public T queryForObject(final String sql, final RowMapper rowMapper) { final List results = query(sql, rowMapper); if (results.size() != 1) { diff --git a/src/main/java/persistence/entity/DatabaseSnapshots.java b/src/main/java/persistence/entity/DatabaseSnapshots.java deleted file mode 100644 index f1e05421b..000000000 --- a/src/main/java/persistence/entity/DatabaseSnapshots.java +++ /dev/null @@ -1,34 +0,0 @@ -package persistence.entity; - -import static persistence.util.ReflectionCopy.copy; - -import java.util.HashMap; -import java.util.Map; - -public class DatabaseSnapshots { - - private final Map databaseSnapshots; - - public DatabaseSnapshots() { - this.databaseSnapshots = new HashMap<>(); - } - - public void addDatabaseSnapshot(Object entity) { - long id = new LongTypeId(entity).getId(); - EntityKey entityKey = new EntityKey(id, entity.getClass().getName()); - Object copyEntity = copy(entity); - - databaseSnapshots.put(entityKey, copyEntity); - } - - public Object getDatabaseSnapshot(Object entity) { - long id = new LongTypeId(entity).getId(); - EntityKey entityKey = new EntityKey(id, entity.getClass().getName()); - return databaseSnapshots.get(entityKey); - } - - public void clear() { - databaseSnapshots.clear(); - } - -} diff --git a/src/main/java/persistence/entity/DirtyCheck.java b/src/main/java/persistence/entity/DirtyCheck.java deleted file mode 100644 index 5ca4af1a4..000000000 --- a/src/main/java/persistence/entity/DirtyCheck.java +++ /dev/null @@ -1,19 +0,0 @@ -package persistence.entity; - -import java.util.List; -import persistence.sql.dml.ColumnValues; - -public class DirtyCheck { - - private final PersistenceContext persistenceContext; - - public DirtyCheck(PersistenceContext persistenceContext) { - this.persistenceContext = persistenceContext; - } - - public List findDirtyColumns(Object entity) throws IllegalAccessException { - Object snapshot = persistenceContext.getDatabaseSnapshot(entity); - return new ColumnValues<>(snapshot).findDifferentColumns(new ColumnValues<>(entity)); - } - -} diff --git a/src/main/java/persistence/entity/EntityEntry.java b/src/main/java/persistence/entity/EntityEntry.java new file mode 100644 index 000000000..b8e2f58b7 --- /dev/null +++ b/src/main/java/persistence/entity/EntityEntry.java @@ -0,0 +1,36 @@ +package persistence.entity; + +import static persistence.util.ReflectionCopy.copy; + +import java.util.List; +import lombok.Getter; +import persistence.sql.dml.ColumnValues; + +public class EntityEntry { + @Getter + private final Object entity; + @Getter + private Status status; + private Object snapshot; + + public EntityEntry(Object entity, Status status) { + this.entity = entity; + this.status = status; + } + + public void updateStatus(Status newStatus) { + this.status = newStatus; + } + + public void updateSnapshot(Object entity) { + this.snapshot = copy(entity); + } + + public List findDirtyColumns(Object entity) throws IllegalAccessException { + if (status != Status.MANAGED) { + throw new IllegalAccessException("Entity is not managed"); + } + return new ColumnValues<>(snapshot).findDifferentColumns(new ColumnValues<>(entity)); + } + +} diff --git a/src/main/java/persistence/entity/EntityPersister.java b/src/main/java/persistence/entity/EntityPersister.java index f3d163eb1..03ec6ca8e 100644 --- a/src/main/java/persistence/entity/EntityPersister.java +++ b/src/main/java/persistence/entity/EntityPersister.java @@ -1,5 +1,6 @@ package persistence.entity; +import java.lang.reflect.Field; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -25,9 +26,15 @@ public void update(Object entity, List changedColumns) throws IllegalAcc .update(entity, changedColumns); } - public void insert(Object entity) throws IllegalAccessException { - getEntityQueryHandler(entity.getClass()) + public Object insert(Object entity) throws IllegalAccessException { + long id = getEntityQueryHandler(entity.getClass()) .insert(entity); + return setId(entity, id); + } + + public void delete(Object entity) { + getEntityQueryHandler(entity.getClass()) + .delete(entity); } private EntityQueryHandler getEntityQueryHandler(Class entityClass) { @@ -37,4 +44,15 @@ private EntityQueryHandler getEntityQueryHandler(Class entityClass) { return entityQueryHandlerMap.get(entityClass); } + private Object setId(Object entity, long id) throws IllegalAccessException { + try { + Field idField = entity.getClass().getDeclaredField("id"); + idField.setAccessible(true); + idField.set(entity, id); + return entity; + } catch (NoSuchFieldException e) { + throw new IllegalStateException("Entity does not have an 'id' field", e); + } + } + } diff --git a/src/main/java/persistence/entity/EntityQueryHandler.java b/src/main/java/persistence/entity/EntityQueryHandler.java index 0dbb6b9e4..1b60465db 100644 --- a/src/main/java/persistence/entity/EntityQueryHandler.java +++ b/src/main/java/persistence/entity/EntityQueryHandler.java @@ -35,16 +35,16 @@ public T findById(Object primaryKey) { return jdbcTemplate.queryForObject(sql, entityLoader); } - public void update(Object entity, List changedColumns) throws IllegalAccessException { + public void update(Object entity, List updateColumns) throws IllegalAccessException { UpdateQuery updateQuery = sqlQueries.getSqlQuery(UPDATE); - String sql = updateQuery.generateQuery(entity, changedColumns); + String sql = updateQuery.generateQuery(entity, updateColumns); jdbcTemplate.execute(sql); } - public void insert(Object entity) throws IllegalAccessException { + public long insert(Object entity) throws IllegalAccessException { InsertQuery insertQuery = sqlQueries.getSqlQuery(INSERT); String sql = insertQuery.generateQuery(entity); - jdbcTemplate.execute(sql); + return jdbcTemplate.executeAndReturnGeneratedKey(sql); } public void delete(Object entity) { diff --git a/src/main/java/persistence/entity/PendingEntities.java b/src/main/java/persistence/entity/PendingEntities.java index fa7581554..014f232e7 100644 --- a/src/main/java/persistence/entity/PendingEntities.java +++ b/src/main/java/persistence/entity/PendingEntities.java @@ -1,26 +1,27 @@ package persistence.entity; -import java.util.HashSet; -import java.util.Set; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class PendingEntities { - private final Set pendingEntities = new HashSet<>(); + private final Map entityEntries = new HashMap<>(); public void persistEntity(Object entity) { - pendingEntities.add(entity); + entityEntries.put(getKey(entity), new EntityEntry(entity, Status.SAVING)); } - public Set getEntities() { - return pendingEntities; + public List getEntities() { + return entityEntries.values().stream().map(EntityEntry::getEntity).toList(); } - public void removeEntity(Object entity) { - pendingEntities.remove(entity); + public void evict(Object entity) { + entityEntries.remove(getKey(entity)); } - public void clear() { - pendingEntities.clear(); + private int getKey(Object entity) { + return System.identityHashCode(entity); } } diff --git a/src/main/java/persistence/entity/PersistedEntities.java b/src/main/java/persistence/entity/PersistedEntities.java index 3227218be..c025b2065 100644 --- a/src/main/java/persistence/entity/PersistedEntities.java +++ b/src/main/java/persistence/entity/PersistedEntities.java @@ -1,31 +1,63 @@ package persistence.entity; -import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; public class PersistedEntities { - private final Map persistedEntities = new HashMap<>(); + private final Map entityEntries = new HashMap<>(); public Object findEntity(EntityKey entityKey) { - return persistedEntities.getOrDefault(entityKey, null); + if (entityEntries.containsKey(entityKey)) { + return entityEntries.get(entityKey).getEntity(); + } + return null; } public void persistEntity(EntityKey entityKey, Object entity) { - persistedEntities.put(entityKey, entity); + var entityEntry = new EntityEntry(entity, Status.MANAGED); + entityEntry.updateSnapshot(entity); + entityEntries.put(entityKey, entityEntry); } - public void removeEntity(EntityKey entityKey) { - persistedEntities.remove(entityKey); + public void evict(Object entity) { + var entityKey = getKey(entity); + entityEntries.remove(entityKey); } - public Collection getEntities() { - return persistedEntities.values(); + public void changeToDeleteState(EntityKey entityKey) { + var entityEntry = entityEntries.get(entityKey); + if (entityEntry == null) { + return; + } + entityEntry.updateStatus(Status.DELETED); } - public void clear() { - persistedEntities.clear(); + public List getDeletedEntities() { + return entityEntries.values().stream().filter(e -> e.getStatus() == Status.DELETED).map(EntityEntry::getEntity) + .toList(); + } + + public List getManagedEntities() { + return entityEntries.values().stream().filter(e -> e.getStatus() == Status.MANAGED).map(EntityEntry::getEntity) + .toList(); + } + + public List findDirtyColumns(Object entity) throws IllegalAccessException { + var entityKey = getKey(entity); + var entityEntry = entityEntries.get(entityKey); + return entityEntry.findDirtyColumns(entity); + } + + public void updateDatabaseSnapshot(Object entity) { + var entityKey = getKey(entity); + var entityEntry = entityEntries.get(entityKey); + entityEntry.updateSnapshot(entity); + } + + private EntityKey getKey(Object entity) { + return new EntityKey(new LongTypeId(entity).getId(), entity.getClass().getName()); } } diff --git a/src/main/java/persistence/entity/PersistenceContext.java b/src/main/java/persistence/entity/PersistenceContext.java index cf9c9f431..3fdb7f55b 100644 --- a/src/main/java/persistence/entity/PersistenceContext.java +++ b/src/main/java/persistence/entity/PersistenceContext.java @@ -1,24 +1,27 @@ package persistence.entity; -import java.util.Collection; -import java.util.Set; +import java.util.List; public interface PersistenceContext { T getEntity(Class entityClass, Object primaryKey); - void attachEntity(Object entity); + void addEntity(Object entity); + + void removeEntity(Object entity); void detachEntity(Object entity); - Set getPendingEntities(); + List getSavingEntities(); + + List getDeletedEntities(); - Collection getPersistedEntities(); + List getManagedEntities(); - void captureDatabaseSnapshot(Object entity); + List findDirtyColumns(Object entity) throws IllegalAccessException; - Object getDatabaseSnapshot(Object entity); + void updateDatabaseSnapshot(Object entity); - void reset(); + void mangeEntity(Object entity) throws IllegalAccessException; } diff --git a/src/main/java/persistence/entity/Status.java b/src/main/java/persistence/entity/Status.java new file mode 100644 index 000000000..fc17474df --- /dev/null +++ b/src/main/java/persistence/entity/Status.java @@ -0,0 +1,10 @@ +package persistence.entity; + +public enum Status { + MANAGED, + READ_ONLY, + DELETED, + GONE, + LOADING, + SAVING +} diff --git a/src/main/java/persistence/entity/impl/EntityManagerImpl.java b/src/main/java/persistence/entity/impl/EntityManagerImpl.java index 069e12f18..fb9055b80 100644 --- a/src/main/java/persistence/entity/impl/EntityManagerImpl.java +++ b/src/main/java/persistence/entity/impl/EntityManagerImpl.java @@ -3,7 +3,6 @@ import java.sql.Connection; import java.util.List; import jdbc.TransactionalJdbcTemplate; -import persistence.entity.DirtyCheck; import persistence.entity.EntityManager; import persistence.entity.EntityPersister; import persistence.entity.PersistenceContext; @@ -13,13 +12,11 @@ public class EntityManagerImpl implements EntityManager { private final PersistenceContext persistenceContext; private final TransactionalJdbcTemplate transactionalJdbcTemplate; private final EntityPersister entityPersister; - private final DirtyCheck dirtyCheck; public EntityManagerImpl(Connection connection) { this.transactionalJdbcTemplate = new TransactionalJdbcTemplate(connection); this.persistenceContext = new PersistenceContextImpl(); this.entityPersister = new EntityPersister(transactionalJdbcTemplate); - this.dirtyCheck = new DirtyCheck(persistenceContext); } @Override @@ -29,8 +26,7 @@ public T find(Class entityClass, Object primaryKey) { entity = entityClass.cast( entityPersister.findById(entityClass, primaryKey) ); - persistenceContext.captureDatabaseSnapshot(entity); - persistenceContext.attachEntity(entity); + persistenceContext.addEntity(entity); } return entity; @@ -38,26 +34,33 @@ public T find(Class entityClass, Object primaryKey) { @Override public void persist(Object entity) { - persistenceContext.attachEntity(entity); + persistenceContext.addEntity(entity); } @Override public void remove(Object entity) { - persistenceContext.detachEntity(entity); + persistenceContext.removeEntity(entity); } @Override public void flush() throws IllegalAccessException { - for (Object entity : persistenceContext.getPendingEntities()) { - entityPersister.insert(entity); + for (Object entity : persistenceContext.getSavingEntities()) { + Object entityWithId = entityPersister.insert(entity); + persistenceContext.mangeEntity(entityWithId); } - for (Object entity : persistenceContext.getPersistedEntities()) { - List changedColumns = dirtyCheck.findDirtyColumns(entity); + + for (Object entity : persistenceContext.getDeletedEntities()) { + entityPersister.delete(entity); + persistenceContext.detachEntity(entity); + } + + for (Object entity : persistenceContext.getManagedEntities()) { + List changedColumns = persistenceContext.findDirtyColumns(entity); if (!changedColumns.isEmpty()) { entityPersister.update(entity, changedColumns); + persistenceContext.updateDatabaseSnapshot(entity); } } - persistenceContext.reset(); } @Override diff --git a/src/main/java/persistence/entity/impl/PersistenceContextImpl.java b/src/main/java/persistence/entity/impl/PersistenceContextImpl.java index e058ef6e9..e6e333b18 100644 --- a/src/main/java/persistence/entity/impl/PersistenceContextImpl.java +++ b/src/main/java/persistence/entity/impl/PersistenceContextImpl.java @@ -1,8 +1,6 @@ package persistence.entity.impl; -import java.util.Collection; -import java.util.Set; -import persistence.entity.DatabaseSnapshots; +import java.util.List; import persistence.entity.EntityKey; import persistence.entity.LongTypeId; import persistence.entity.PendingEntities; @@ -13,12 +11,10 @@ public class PersistenceContextImpl implements PersistenceContext { private final PersistedEntities persistedEntities; private final PendingEntities pendingEntities; - private final DatabaseSnapshots databaseSnapshots; public PersistenceContextImpl() { persistedEntities = new PersistedEntities(); pendingEntities = new PendingEntities(); - databaseSnapshots = new DatabaseSnapshots(); } @Override @@ -29,7 +25,7 @@ public T getEntity(Class entityClass, Object primaryKey) { @Override - public void attachEntity(Object entity) { + public void addEntity(Object entity) { if (new LongTypeId(entity).isEntityIdNull()) { pendingEntities.persistEntity(entity); return; @@ -37,37 +33,48 @@ public void attachEntity(Object entity) { persistedEntities.persistEntity(getEntityKey(entity), entity); } + @Override + public void removeEntity(Object entity) { + pendingEntities.evict(entity); + persistedEntities.changeToDeleteState(getEntityKey(entity)); + } + @Override public void detachEntity(Object entity) { - pendingEntities.removeEntity(entity); - persistedEntities.removeEntity(getEntityKey(entity)); + persistedEntities.evict(entity); } @Override - public Set getPendingEntities() { + public List getSavingEntities() { return pendingEntities.getEntities(); } @Override - public Collection getPersistedEntities() { - return persistedEntities.getEntities(); + public List getDeletedEntities() { + return persistedEntities.getDeletedEntities(); + } + + @Override + public List getManagedEntities() { + return persistedEntities.getManagedEntities(); } @Override - public void captureDatabaseSnapshot(Object entity) { - databaseSnapshots.addDatabaseSnapshot(entity); + public List findDirtyColumns(Object entity) throws IllegalAccessException { + return persistedEntities.findDirtyColumns(entity); } @Override - public Object getDatabaseSnapshot(Object entity) { - return databaseSnapshots.getDatabaseSnapshot(entity); + public void updateDatabaseSnapshot(Object entity) { + persistedEntities.updateDatabaseSnapshot(entity); } + @Override - public void reset() { - pendingEntities.clear(); - persistedEntities.clear(); - databaseSnapshots.clear(); + public void mangeEntity(Object entity) { + pendingEntities.evict(entity); + long id = new LongTypeId(entity).getId(); + persistedEntities.persistEntity(new EntityKey(id, entity.getClass().getName()), entity); } private EntityKey getEntityKey(Object entity) { @@ -77,4 +84,5 @@ private EntityKey getEntityKey(Object entity) { ); } + } diff --git a/src/main/java/persistence/sql/dml/UpdateQuery.java b/src/main/java/persistence/sql/dml/UpdateQuery.java index 80d274977..ce4ea02f1 100644 --- a/src/main/java/persistence/sql/dml/UpdateQuery.java +++ b/src/main/java/persistence/sql/dml/UpdateQuery.java @@ -6,23 +6,22 @@ public class UpdateQuery implements SqlQuery { - public String generateQuery(Object entity, List changedColumns) throws IllegalAccessException { - List changedValues = new ColumnValues<>(entity).getValuesByColumns(changedColumns); + public String generateQuery(Object entity, List updateColumns) throws IllegalAccessException { String tableName = new EntityTableMetadata(entity.getClass()).getTableName(); - + List values = new ColumnValues<>(entity).getValuesByColumns(updateColumns); Long id = new EntityId(entity).getId(); - return generateUpdateQuery(tableName, changedValues, id); + return generateUpdateQuery(tableName, values, id); } private String generateUpdateQuery( String tableName, - List changedValues, + List values, Long id ) { return new QueryBuilder() .update(tableName) - .setValues(changedValues) + .setValues(values) .where("id = " + id) .build(); } diff --git a/src/test/java/persistence/entity/CustomJpaRepositoryTest.java b/src/test/java/persistence/entity/CustomJpaRepositoryTest.java index 40a56b82f..bf5f0b6b4 100644 --- a/src/test/java/persistence/entity/CustomJpaRepositoryTest.java +++ b/src/test/java/persistence/entity/CustomJpaRepositoryTest.java @@ -59,9 +59,7 @@ void saveWithDirty() throws SQLException, IllegalAccessException { em.persist(person); em.flush(); - Person personOne = em.find(Person.class, 1L); - personOne.setName("Jane"); - + person.setName("Jane"); em.getTransaction().commit(); Person updatedPerson = em.find(Person.class, 1L); @@ -84,9 +82,7 @@ void saveWithDirtyWithNullColumn() throws SQLException, IllegalAccessException { em.persist(person); em.flush(); - Person personOne = em.find(Person.class, 1L); - personOne.setAge(null); - + person.setAge(null); em.getTransaction().commit(); Person updatedPerson = em.find(Person.class, 1L); diff --git a/src/test/java/persistence/entity/impl/EntityManagerImplTest.java b/src/test/java/persistence/entity/impl/EntityManagerImplTest.java index 11f6f858e..8dcdff6a4 100644 --- a/src/test/java/persistence/entity/impl/EntityManagerImplTest.java +++ b/src/test/java/persistence/entity/impl/EntityManagerImplTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import database.DatabaseServer; import database.H2; @@ -47,7 +48,7 @@ void createTable() { @Test @DisplayName("find 구현해보기") - void findTest() throws SQLException, IllegalAccessException { + void findTest() throws SQLException { EntityManagerFactory entityManagerFactory = new EntityManagerFactoryImpl(server); EntityManager em = entityManagerFactory.createEntityManager(); @@ -58,9 +59,7 @@ void findTest() throws SQLException, IllegalAccessException { .age(20) .email("john@naver.com") .build(); - em.persist(person); - em.flush(); em.getTransaction().commit(); @@ -74,7 +73,7 @@ void findTest() throws SQLException, IllegalAccessException { @Test @DisplayName("remove 구현해보기") - void removeTest() throws SQLException, IllegalAccessException { + void removeTest() throws SQLException { EntityManagerFactory entityManagerFactory = new EntityManagerFactoryImpl(server); EntityManager em = entityManagerFactory.createEntityManager(); @@ -85,18 +84,62 @@ void removeTest() throws SQLException, IllegalAccessException { .age(20) .email("john@naver.com") .build(); - em.persist(person); + em.remove(person); - em.flush(); + em.getTransaction().commit(); + + assertThatThrownBy(() -> em.find(Person.class, 1L)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Expected 1 result, got 0"); + } + + @Test + @DisplayName("managed entity 삭제 테스트") + void removeManagedEntityTest() throws SQLException { + EntityManagerFactory entityManagerFactory = new EntityManagerFactoryImpl(server); + EntityManager em = entityManagerFactory.createEntityManager(); + + em.getTransaction().beginTransaction(); + + Person person = Person.builder() + .name("John") + .age(20) + .email("john@naver.com") + .build(); + em.persist(person); em.getTransaction().commit(); + em.getTransaction().beginTransaction(); + em.remove(person); + em.getTransaction().commit(); + assertThatThrownBy(() -> em.find(Person.class, 1L)) .isInstanceOf(RuntimeException.class) .hasMessageContaining("Expected 1 result, got 0"); } + @Test + @DisplayName("flush 이후 entity id 조회 가능 여부 테스트") + void flushAndFindId() throws SQLException, IllegalAccessException { + EntityManagerFactory entityManagerFactory = new EntityManagerFactoryImpl(server); + EntityManager em = entityManagerFactory.createEntityManager(); + + Person person = Person.builder() + .name("John") + .age(20) + .email("john@naver.com") + .build(); + + assertNull(person.getId()); + + em.persist(person); + em.flush(); + + assertEquals(1L, person.getId()); + } + @Test @DisplayName("update 구현해보기") void updateTest() throws SQLException, IllegalAccessException { @@ -113,14 +156,13 @@ void updateTest() throws SQLException, IllegalAccessException { em.persist(person); em.flush(); - Person personOne = em.find(Person.class, 1L); - personOne.setName("Jane"); - em.flush(); - + person.setName("Jane"); + person.setAge(30); em.getTransaction().commit(); Person updatedPerson = em.find(Person.class, 1L); assertEquals("Jane", updatedPerson.getName()); + assertEquals(30, updatedPerson.getAge()); } @AfterEach