diff --git a/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/README.MD b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/README.MD new file mode 100644 index 00000000..452fff89 --- /dev/null +++ b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/README.MD @@ -0,0 +1,25 @@ +# Photo comment DAO exercise :muscle: +Improve your *Hibernate ORM* and *JPA One-To-Many* relationship mapping skills +### Task +Each `Photo` has a list of `PhotoComments`. Comments cannot exits without photo associated. You job is to provide a mapping +for this **bidirectional *one to many* relationship** and **implement required DAO methods.** Please follow the instructions +in the todo sections. + +To verify your mapping, run `PhotoCommentMappingTest.java` +To verify your DAO implementation, run `PhotoDaoTest.java` + + +### Pre-conditions :heavy_exclamation_mark: +You're supposed to be familiar with *JPA* mapping strategies and *Hibernate ORM* + +### How to start :question: +* Just clone the repository and start implementing the **todo** section, verify your changes by running tests +* If you don't have enough knowledge about this domain, check out the [links below](#related-materials-information_source) +* Don't worry if you got stuck, checkout the **exercise/completed** branch and see the final implementation + +### Related materials :information_source: + * [JPA and Hibernate basics tutorial](https://github.com/boy4uck/jpa-hibernate-tutorial/tree/master/jpa-hibernate-basics) + * [@OneToMany association](http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#associations-one-to-many) + * [The best way to map a @OneToMany relationship with JPA and Hibernate](https://vladmihalcea.com/the-best-way-to-map-a-onetomany-association-with-jpa-and-hibernate/) + + diff --git a/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/pom.xml b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/pom.xml new file mode 100644 index 00000000..a98e516a --- /dev/null +++ b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/pom.xml @@ -0,0 +1,22 @@ + + + + 3-0-jpa-and-hibernate + com.bobocode + 1.0-SNAPSHOT + + 4.0.0 + + 3-2-2-photo-comment-dao + + + + com.bobocode + jpa-hibernate-util + 1.0-SNAPSHOT + + + + \ No newline at end of file diff --git a/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/java/com/bobocode/dao/PhotoDao.java b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/java/com/bobocode/dao/PhotoDao.java new file mode 100644 index 00000000..43e4c01b --- /dev/null +++ b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/java/com/bobocode/dao/PhotoDao.java @@ -0,0 +1,49 @@ +package com.bobocode.dao; + +import com.bobocode.model.Photo; + +import java.util.List; + +/** + * {@link PhotoDao} defines and API of Data-Access Object for entity {@link Photo} + */ +public interface PhotoDao { + + /** + * Saves photo into db and sets an id + * + * @param photo new photo + */ + void save(Photo photo); + + /** + * Retrieves a photo from the database by its id + * + * @param id photo id + * @return photo instance + */ + Photo findById(long id); + + /** + * Returns a list of all stored photos + * + * @return list of stored photos + */ + List findAll(); + + /** + * Removes a photo from the database + * + * @param photo an instance of stored photo + */ + void remove(Photo photo); + + /** + * Adds a new comment to an existing photo. This method does not require additional SQL select methods to load + * {@link Photo}. + * + * @param photoId + * @param comment + */ + void addComment(long photoId, String comment); +} diff --git a/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/java/com/bobocode/dao/PhotoDaoImpl.java b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/java/com/bobocode/dao/PhotoDaoImpl.java new file mode 100644 index 00000000..c2f06977 --- /dev/null +++ b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/java/com/bobocode/dao/PhotoDaoImpl.java @@ -0,0 +1,43 @@ +package com.bobocode.dao; + +import com.bobocode.model.Photo; +import com.bobocode.util.ExerciseNotCompletedException; + +import javax.persistence.EntityManagerFactory; +import java.util.List; + +/** + * Please note that you should not use auto-commit mode for your implementation. + */ +public class PhotoDaoImpl implements PhotoDao { + private EntityManagerFactory entityManagerFactory; + + public PhotoDaoImpl(EntityManagerFactory entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + } + + @Override + public void save(Photo photo) { + throw new ExerciseNotCompletedException(); // todo + } + + @Override + public Photo findById(long id) { + throw new ExerciseNotCompletedException(); // todo + } + + @Override + public List findAll() { + throw new ExerciseNotCompletedException(); // todo + } + + @Override + public void remove(Photo photo) { + throw new ExerciseNotCompletedException(); // todo + } + + @Override + public void addComment(long photoId, String comment) { + throw new ExerciseNotCompletedException(); // todo + } +} diff --git a/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/java/com/bobocode/model/Photo.java b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/java/com/bobocode/model/Photo.java new file mode 100644 index 00000000..cc88e862 --- /dev/null +++ b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/java/com/bobocode/model/Photo.java @@ -0,0 +1,40 @@ +package com.bobocode.model; + +import com.bobocode.util.ExerciseNotCompletedException; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * todo: + * - make a setter for field {@link Photo#comments} {@code private} + * - implement equals() and hashCode() based on identifier field + * + * - configure JPA entity + * - specify table name: "photo" + * - configure auto generated identifier + * - configure not nullable and unique column: url + * + * - initialize field comments + * - map relation between Photo and PhotoComment on the child side + * - implement helper methods {@link Photo#addComment(PhotoComment)} and {@link Photo#removeComment(PhotoComment)} + * - enable cascade type {@link javax.persistence.CascadeType#ALL} for field {@link Photo#comments} + * - enable orphan removal + */ +@Getter +@Setter +public class Photo { + private Long id; + private String url; + private String description; + private List comments; + + public void addComment(PhotoComment comment) { + throw new ExerciseNotCompletedException(); + } + + public void removeComment(PhotoComment comment) { + throw new ExerciseNotCompletedException(); + } +} diff --git a/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/java/com/bobocode/model/PhotoComment.java b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/java/com/bobocode/model/PhotoComment.java new file mode 100644 index 00000000..83edf2f7 --- /dev/null +++ b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/java/com/bobocode/model/PhotoComment.java @@ -0,0 +1,27 @@ +package com.bobocode.model; + +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +/** + * todo: + * - implement equals and hashCode based on identifier field + * + * - configure JPA entity + * - specify table name: "photo_comment" + * - configure auto generated identifier + * - configure not nullable column: text + * + * - map relation between Photo and PhotoComment using foreign_key column: "photo_id" + * - configure relation as mandatory (not optional) + */ +@Getter +@Setter +public class PhotoComment { + private Long id; + private String text; + private LocalDateTime createdOn; + private Photo photo; +} diff --git a/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/resources/META-INF/persistence.xml b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/resources/META-INF/persistence.xml new file mode 100644 index 00000000..35e736a7 --- /dev/null +++ b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,20 @@ + + + + + com.bobocode.model.Photo + com.bobocode.model.PhotoComment + + + + + + + + + + + + + + diff --git a/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/test/java/com/bobocode/PhotoCommentMappingTest.java b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/test/java/com/bobocode/PhotoCommentMappingTest.java new file mode 100644 index 00000000..a6f91d5a --- /dev/null +++ b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/test/java/com/bobocode/PhotoCommentMappingTest.java @@ -0,0 +1,265 @@ +package com.bobocode; + +import com.bobocode.model.Photo; +import com.bobocode.model.PhotoComment; +import com.bobocode.util.EntityManagerUtil; +import org.junit.jupiter.api.*; + +import javax.persistence.*; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; + +import static com.bobocode.util.PhotoTestDataGenerator.*; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class PhotoCommentMappingTest { + private static EntityManagerUtil emUtil; + private static EntityManagerFactory entityManagerFactory; + + @BeforeAll + public static void setup() { + entityManagerFactory = Persistence.createEntityManagerFactory("PhotoComments"); + emUtil = new EntityManagerUtil(entityManagerFactory); + } + + @AfterAll + static void destroy() { + entityManagerFactory.close(); + } + + @Test + @Order(1) + @DisplayName("Comments list is initialized") + public void commentsListIsInitialized() { + Photo photo = new Photo(); + List comments = photo.getComments(); + + assertThat(comments).isNotNull(); + } + + @Test + @Order(2) + @DisplayName("Setter for field \"comments\" is private in Photo entity") + public void commentsSetterIsPrivate() throws NoSuchMethodException { + Method setComments = Photo.class.getDeclaredMethod("setComments", List.class); + + assertThat(setComments.getModifiers()).isEqualTo(Modifier.PRIVATE); + } + + @Test + @Order(3) + @DisplayName("Photo table name is specified") + public void photoTableNameIsSpecified() { + Table table = Photo.class.getAnnotation(Table.class); + String tableName = table.name(); + + assertThat(tableName).isEqualTo("photo"); + } + + @Test + @Order(4) + @DisplayName("Photo comment table name is specified") + public void photoCommentTableNameIsSpecified() { + Table table = PhotoComment.class.getAnnotation(Table.class); + + assertThat(table.name()).isEqualTo("photo_comment"); + } + + @Test + @Order(5) + @DisplayName("Photo URL is not null and unique") + public void photoUrlIsNotNullAndUnique() throws NoSuchFieldException { + Field url = Photo.class.getDeclaredField("url"); + Column column = url.getAnnotation(Column.class); + + assertThat(column.nullable()).isFalse(); + assertThat(column.unique()).isTrue(); + } + + @Test + @Order(6) + @DisplayName("Photo comment text is mandatory") + public void photoCommentTextIsMandatory() throws NoSuchFieldException { + Field text = PhotoComment.class.getDeclaredField("text"); + Column column = text.getAnnotation(Column.class); + + assertThat(column.nullable()).isFalse(); + } + + @Test + @Order(7) + @DisplayName("Cascade type ALL is enabled for comments") + public void cascadeTypeAllIsEnabledForComments() throws NoSuchFieldException { + Field comments = Photo.class.getDeclaredField("comments"); + OneToMany oneToMany = comments.getAnnotation(OneToMany.class); + CascadeType[] expectedCascade = {CascadeType.ALL}; + + assertThat(oneToMany.cascade()).isEqualTo(expectedCascade); + } + + @Test + @Order(8) + @DisplayName("Orphan removal is enabled for comments") + public void orphanRemovalIsEnabledForComments() throws NoSuchFieldException { + Field comments = Photo.class.getDeclaredField("comments"); + OneToMany oneToMany = comments.getAnnotation(OneToMany.class); + + assertThat(oneToMany.orphanRemoval()).isTrue(); + } + + @Test + @Order(9) + @DisplayName("Foreign key column is specified") + public void foreignKeyColumnIsSpecified() throws NoSuchFieldException { + Field photo = PhotoComment.class.getDeclaredField("photo"); + JoinColumn joinColumn = photo.getAnnotation(JoinColumn.class); + + assertThat(joinColumn.name()).isEqualTo("photo_id"); + } + + @Test + @Order(10) + @DisplayName("Save a photo only") + public void savePhotoOnly() { + Photo photo = createRandomPhoto(); + emUtil.performWithinTx(entityManager -> entityManager.persist(photo)); + + assertThat(photo.getId()).isNotNull(); + } + + @Test + @Order(11) + @DisplayName("Save a photo comment only") + public void savePhotoCommentOnly() { + PhotoComment photoComment = createRandomPhotoComment(); + + assertThatExceptionOfType(PersistenceException.class).isThrownBy(() -> + emUtil.performWithinTx(entityManager -> entityManager.persist(photoComment))); + } + + @Test + @Order(12) + @DisplayName("A comment cannot exist without a photo") + public void commentCannotExistsWithoutPhoto() throws NoSuchFieldException { + Field photo = PhotoComment.class.getDeclaredField("photo"); + ManyToOne manyToOne = photo.getAnnotation(ManyToOne.class); + + assertThat(manyToOne.optional()).isFalse(); + } + + @Test + @Order(13) + @DisplayName("Save a new comment") + public void saveNewComment() { + Photo photo = createRandomPhoto(); + emUtil.performWithinTx(entityManager -> entityManager.persist(photo)); + + PhotoComment photoComment = createRandomPhotoComment(); + photoComment.setPhoto(photo); + emUtil.performWithinTx(entityManager -> entityManager.persist(photoComment)); + + assertThat(photoComment.getId()).isNotNull(); + emUtil.performWithinTx(entityManager -> { + PhotoComment managedPhotoComment = entityManager.find(PhotoComment.class, photoComment.getId()); + assertThat(managedPhotoComment.getPhoto()).isEqualTo(photo); + }); + emUtil.performWithinTx(entityManager -> { + Photo managedPhoto = entityManager.find(Photo.class, photo.getId()); + assertThat(managedPhoto.getComments()).contains(photoComment); + }); + } + + @Test + @Order(14) + @DisplayName("Add a new comment") + public void addNewComment() { + Photo photo = createRandomPhoto(); + emUtil.performWithinTx(entityManager -> entityManager.persist(photo)); + + PhotoComment photoComment = createRandomPhotoComment(); + emUtil.performWithinTx(entityManager -> { + Photo managedPhoto = entityManager.find(Photo.class, photo.getId()); + managedPhoto.addComment(photoComment); + }); + + assertThat(photoComment.getId()).isNotNull(); + emUtil.performWithinTx(entityManager -> { + PhotoComment managedPhotoComment = entityManager.find(PhotoComment.class, photoComment.getId()); + assertThat(managedPhotoComment.getPhoto()).isEqualTo(photo); + }); + emUtil.performWithinTx(entityManager -> { + Photo managedPhoto = entityManager.find(Photo.class, photo.getId()); + assertThat(managedPhoto.getComments()).contains(photoComment); + }); + } + + @Test + @Order(15) + @DisplayName("Save new comments") + public void saveNewComments() { + Photo photo = createRandomPhoto(); + emUtil.performWithinTx(entityManager -> entityManager.persist(photo)); + + List listOfComments = createListOfRandomComments(5); + listOfComments.forEach(comment -> comment.setPhoto(photo)); + + emUtil.performWithinTx(entityManager -> listOfComments.forEach(entityManager::persist)); + + emUtil.performWithinTx(entityManager -> { + Photo managedPhoto = entityManager.find(Photo.class, photo.getId()); + assertThat(managedPhoto.getComments()).containsExactlyInAnyOrderElementsOf(listOfComments); + }); + } + + @Test + @Order(16) + @DisplayName("Add a new comment") + public void addNewComments() { + Photo photo = createRandomPhoto(); + emUtil.performWithinTx(entityManager -> entityManager.persist(photo)); + List listOfComments = createListOfRandomComments(5); + + emUtil.performWithinTx(entityManager -> { + Photo managedPhoto = entityManager.find(Photo.class, photo.getId()); + listOfComments.forEach(managedPhoto::addComment); + }); + + emUtil.performWithinTx(entityManager -> { + Photo managedPhoto = entityManager.find(Photo.class, photo.getId()); + assertThat(managedPhoto.getComments()).containsExactlyInAnyOrderElementsOf(listOfComments); + }); + } + + @Test + @Order(17) + @DisplayName("Remove a comment") + public void removeComment() { + Photo photo = createRandomPhoto(); + PhotoComment photoComment = createRandomPhotoComment(); + List commentList = createListOfRandomComments(5); + photo.addComment(photoComment); + commentList.forEach(photo::addComment); + emUtil.performWithinTx(entityManager -> entityManager.persist(photo)); + + emUtil.performWithinTx(entityManager -> { + Photo managedPhoto = entityManager.find(Photo.class, photo.getId()); + PhotoComment managedComment = entityManager.find(PhotoComment.class, photoComment.getId()); + managedPhoto.removeComment(managedComment); + }); + + + emUtil.performWithinTx(entityManager -> { + Photo managedPhoto = entityManager.find(Photo.class, photo.getId()); + PhotoComment managedPhotoComment = entityManager.find(PhotoComment.class, photoComment.getId()); + + assertThat(managedPhoto.getComments()).doesNotContain(photoComment); + assertThat(managedPhoto.getComments()).containsExactlyInAnyOrderElementsOf(commentList); + assertThat(managedPhotoComment).isNull(); + }); + } +} diff --git a/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/test/java/com/bobocode/PhotoDaoTest.java b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/test/java/com/bobocode/PhotoDaoTest.java new file mode 100644 index 00000000..42223914 --- /dev/null +++ b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/test/java/com/bobocode/PhotoDaoTest.java @@ -0,0 +1,99 @@ +package com.bobocode; + +import com.bobocode.dao.PhotoDao; +import com.bobocode.dao.PhotoDaoImpl; +import com.bobocode.model.Photo; +import com.bobocode.util.EntityManagerUtil; +import org.junit.jupiter.api.*; + +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import java.util.List; + +import static com.bobocode.util.PhotoTestDataGenerator.createListOfRandomPhotos; +import static com.bobocode.util.PhotoTestDataGenerator.createRandomPhoto; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class PhotoDaoTest { + private EntityManagerUtil emUtil; + private PhotoDao photoDao; + private EntityManagerFactory entityManagerFactory; + + @BeforeEach + public void setup() { + entityManagerFactory = Persistence.createEntityManagerFactory("PhotoComments"); + emUtil = new EntityManagerUtil(entityManagerFactory); + photoDao = new PhotoDaoImpl(entityManagerFactory); + } + + @AfterEach + public void destroy() { + entityManagerFactory.close(); + } + + @Test + @Order(1) + @DisplayName("Save a photo") + public void savePhoto() { + Photo photo = createRandomPhoto(); + + photoDao.save(photo); + + Photo fountPhoto = emUtil.performReturningWithinTx(entityManager -> entityManager.find(Photo.class, photo.getId())); + assertThat(fountPhoto).isEqualTo(photo); + } + + @Test + @Order(3) + @DisplayName("Find a photo by Id") + public void findPhotoById() { + Photo photo = createRandomPhoto(); + emUtil.performWithinTx(entityManager -> entityManager.persist(photo)); + + Photo foundPhoto = photoDao.findById(photo.getId()); + + assertThat(foundPhoto).isEqualTo(photo); + } + + @Test + @Order(3) + @DisplayName("Find all photos") + public void findAllPhotos() { + List listOfRandomPhotos = createListOfRandomPhotos(5); + emUtil.performWithinTx(entityManager -> listOfRandomPhotos.forEach(entityManager::persist)); + + List foundPhotos = photoDao.findAll(); + + assertThat(foundPhotos).containsExactlyInAnyOrderElementsOf(listOfRandomPhotos); + } + + @Test + @Order(4) + @DisplayName("Remove a photo") + public void removePhoto() { + Photo photo = createRandomPhoto(); + emUtil.performWithinTx(entityManager -> entityManager.persist(photo)); + + photoDao.remove(photo); + + Photo removedPhoto = emUtil.performReturningWithinTx(entityManager -> entityManager.find(Photo.class, photo.getId())); + assertThat(removedPhoto).isNull(); + } + + @Test + @Order(5) + @DisplayName("Add a photo comment") + public void addPhotoComment() { + Photo photo = createRandomPhoto(); + emUtil.performWithinTx(entityManager -> entityManager.persist(photo)); + + photoDao.addComment(photo.getId(), "Nice picture!"); + + emUtil.performWithinTx(entityManager -> { + Photo managedPhoto = entityManager.find(Photo.class, photo.getId()); + assertThat(managedPhoto.getComments()).extracting("text").contains("Nice picture!"); + }); + } +} diff --git a/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/test/java/com/bobocode/util/PhotoTestDataGenerator.java b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/test/java/com/bobocode/util/PhotoTestDataGenerator.java new file mode 100644 index 00000000..6149e23c --- /dev/null +++ b/3-0-jpa-and-hibernate/3-2-2-photo-comment-dao/src/test/java/com/bobocode/util/PhotoTestDataGenerator.java @@ -0,0 +1,35 @@ +package com.bobocode.util; + +import com.bobocode.model.Photo; +import com.bobocode.model.PhotoComment; +import org.apache.commons.lang3.RandomStringUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toList; + +public class PhotoTestDataGenerator { + public static Photo createRandomPhoto(){ + Photo photo = new Photo(); + photo.setUrl(RandomStringUtils.randomAlphabetic(30)); + photo.setDescription(RandomStringUtils.randomAlphabetic(50)); + return photo; + } + + public static PhotoComment createRandomPhotoComment() { + PhotoComment photoComment = new PhotoComment(); + photoComment.setCreatedOn(LocalDateTime.now()); + photoComment.setText(RandomStringUtils.randomAlphabetic(50)); + return photoComment; + } + + public static List createListOfRandomPhotos(int size) { + return Stream.generate(PhotoTestDataGenerator::createRandomPhoto).limit(size).collect(toList()); + } + public static List createListOfRandomComments(int size) { + return Stream.generate(PhotoTestDataGenerator::createRandomPhotoComment).limit(size).collect(toList()); + } +} + diff --git a/3-0-jpa-and-hibernate/pom.xml b/3-0-jpa-and-hibernate/pom.xml index 584ef4ed..add27038 100644 --- a/3-0-jpa-and-hibernate/pom.xml +++ b/3-0-jpa-and-hibernate/pom.xml @@ -20,6 +20,7 @@ 3-1-1-employee-profile 3-1-2-company-products 3-1-3-author-book + 3-2-2-photo-comment-dao