diff --git a/spring-geode-tests/smoke-tests/multi-store/lombok.config b/spring-geode-tests/smoke-tests/multi-store/lombok.config new file mode 100644 index 00000000..6aa51d71 --- /dev/null +++ b/spring-geode-tests/smoke-tests/multi-store/lombok.config @@ -0,0 +1,2 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true diff --git a/spring-geode-tests/smoke-tests/multi-store/spring-geode-smoke-tests-multi-store.gradle b/spring-geode-tests/smoke-tests/multi-store/spring-geode-smoke-tests-multi-store.gradle new file mode 100644 index 00000000..b97d81a2 --- /dev/null +++ b/spring-geode-tests/smoke-tests/multi-store/spring-geode-smoke-tests-multi-store.gradle @@ -0,0 +1,32 @@ +plugins { + id "io.freefair.lombok" version "3.2.1" +} + +apply plugin: 'io.spring.convention.spring-test' + +description = "Smoke Tests to assert that a multi-store Spring Data project using JPA for database access and Apache Geode for caching works as expected." + +repositories { + mavenCentral() + maven { url 'https://repo.spring.io/milestone' } + maven { url 'https://repo.spring.io/snapshot' } +} + +dependencies { + + implementation "org.assertj:assertj-core" + implementation "org.projectlombok:lombok" + implementation "org.springframework.boot:spring-boot-starter-data-jpa" + implementation "org.springframework.geode:spring-geode-starter:$version" + + runtime "org.hsqldb:hsqldb" + + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + +} + +test { + useJUnitPlatform() +} diff --git a/spring-geode-tests/smoke-tests/multi-store/src/main/java/example/app/DatabaseWithLookAsideCachingApplication.java b/spring-geode-tests/smoke-tests/multi-store/src/main/java/example/app/DatabaseWithLookAsideCachingApplication.java new file mode 100644 index 00000000..63c3ad04 --- /dev/null +++ b/spring-geode-tests/smoke-tests/multi-store/src/main/java/example/app/DatabaseWithLookAsideCachingApplication.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package example.app; + +import org.apache.geode.cache.client.ClientRegionShortcut; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.gemfire.config.annotation.EnableCachingDefinedRegions; + +import example.app.model.Contact; + +/** + * The {@link DatabaseWithLookAsideCachingApplication} class is a Spring Boot application testing Spring Data's + * multi-store support along with using Apache Geode as a caching provider in Spring's Cache Abstraction. + * + * @author John Blum + * @see org.springframework.boot.SpringApplication + * @see org.springframework.boot.autoconfigure.SpringBootApplication + * @see org.springframework.boot.autoconfigure.domain.EntityScan + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions + * @see example.app.model.Contact + * @since 1.2.0 + */ +@SpringBootApplication +@SuppressWarnings("unused") +public class DatabaseWithLookAsideCachingApplication { + + public static void main(String[] args) { + SpringApplication.run(DatabaseWithLookAsideCachingApplication.class, args); + } + + @Configuration + @EntityScan(basePackageClasses = Contact.class) + @EnableCachingDefinedRegions(clientRegionShortcut = ClientRegionShortcut.LOCAL) + static class ApplicationConfiguration { } + +} diff --git a/spring-geode-tests/smoke-tests/multi-store/src/main/java/example/app/model/Contact.java b/spring-geode-tests/smoke-tests/multi-store/src/main/java/example/app/model/Contact.java new file mode 100644 index 00000000..36a656db --- /dev/null +++ b/spring-geode-tests/smoke-tests/multi-store/src/main/java/example/app/model/Contact.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package example.app.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +import org.springframework.data.annotation.Id; +import org.springframework.lang.NonNull; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * Abstract Data Type (ADT) modeling contact information for a person. + * + * @author John Blum + * @see javax.persistence.Entity + * @see org.springframework.data.annotation.Id + * @since 1.2.0 + */ +@Data +@Entity +@Table(name = "Contacts") +@ToString(of = "name") +@EqualsAndHashCode(of = "name") +@RequiredArgsConstructor(staticName = "newContact") +@SuppressWarnings("unused") +public class Contact { + + @javax.persistence.Id @Id @NonNull + private String name; + + @Column(name = "email_address") + private String emailAddress; + + @Column(name = "phone_number") + private String phoneNumber; + + protected Contact() { } + + public Contact withEmailAddress(String emailAddress) { + setEmailAddress(emailAddress); + return this; + } + + public Contact withPhoneNumber(String phoneNumber) { + setPhoneNumber(phoneNumber); + return this; + } +} diff --git a/spring-geode-tests/smoke-tests/multi-store/src/main/java/example/app/repo/ContactRepository.java b/spring-geode-tests/smoke-tests/multi-store/src/main/java/example/app/repo/ContactRepository.java new file mode 100644 index 00000000..532093ef --- /dev/null +++ b/spring-geode-tests/smoke-tests/multi-store/src/main/java/example/app/repo/ContactRepository.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package example.app.repo; + +import org.springframework.data.repository.CrudRepository; + +import example.app.model.Contact; + +/** + * Spring Data {@link CrudRepository} and Data Access Object (DAO) for performing basic CRUD and simple Query + * data access operations on a {@link Contact}. + * + * @author John Blum + * @see org.springframework.data.repository.CrudRepository + * @see example.app.model.Contact + * @since 1.2.0 + */ +public interface ContactRepository extends CrudRepository { + +} diff --git a/spring-geode-tests/smoke-tests/multi-store/src/main/java/example/app/service/ContactsService.java b/spring-geode-tests/smoke-tests/multi-store/src/main/java/example/app/service/ContactsService.java new file mode 100644 index 00000000..3d7c972c --- /dev/null +++ b/spring-geode-tests/smoke-tests/multi-store/src/main/java/example/app/service/ContactsService.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package example.app.service; + +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import example.app.model.Contact; +import example.app.repo.ContactRepository; + +/** + * Spring {@link Service} class for servicing {@link Contact Contacts}. + * + * @author John Blum + * @see org.springframework.cache.annotation.Cacheable + * @see org.springframework.cache.annotation.CachePut + * @see org.springframework.stereotype.Service + * @see example.app.model.Contact + * @see example.app.repo.ContactRepository + * @since 1.2.0 + */ +@Service +public class ContactsService { + + private final AtomicBoolean cacheMiss = new AtomicBoolean(false); + + private final ContactRepository contactRepository; + + public ContactsService(ContactRepository contactRepository) { + + Assert.notNull(contactRepository, "ContactRepository is required"); + + this.contactRepository = contactRepository; + } + + public boolean isCacheMiss() { + return this.cacheMiss.getAndSet(false); + } + + @Cacheable(cacheNames = "ContactsByName") + public Contact findByName(String name) { + + this.cacheMiss.set(true); + + return this.contactRepository.findById(name).orElseThrow(() -> + newIllegalStateException("No Contact with name [%s] was found", name)); + } + + @CachePut(cacheNames = "ContactsByName", key="#contact.name") + public Contact save(Contact contact) { + return this.contactRepository.save(contact); + } +} diff --git a/spring-geode-tests/smoke-tests/multi-store/src/test/java/example/app/DatabaseWithLookAsideCachingApplicationSmokeTests.java b/spring-geode-tests/smoke-tests/multi-store/src/test/java/example/app/DatabaseWithLookAsideCachingApplicationSmokeTests.java new file mode 100644 index 00000000..6159b0a6 --- /dev/null +++ b/spring-geode-tests/smoke-tests/multi-store/src/test/java/example/app/DatabaseWithLookAsideCachingApplicationSmokeTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package example.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import example.app.model.Contact; +import example.app.repo.ContactRepository; +import example.app.service.ContactsService; + +/** + * Smoke Tests for {@link DatabaseWithLookAsideCachingApplication}. + * + * @author John Blum + * @see org.junit.Test + * @see org.springframework.boot.test.context.SpringBootTest + * @see org.springframework.test.context.junit4.SpringRunner + * @see example.app.model.Contact + * @see example.app.repo.ContactRepository + * @see example.app.service.ContactsService + * @since 1.2.0 + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@SuppressWarnings("unused") +public class DatabaseWithLookAsideCachingApplicationSmokeTests { + + @Autowired + private ContactRepository contactRepository; + + @Autowired + private ContactsService contactsService; + + @Before + public void setup() { + + assertThat(this.contactRepository).isNotNull(); + assertThat(this.contactRepository.findById("Fro Doe").isPresent()).isTrue(); + assertThat(this.contactsService).isNotNull(); + assertThat(this.contactsService.isCacheMiss()).isInstanceOf(Boolean.class); + } + + private void assertContact(Contact actual, Contact expected) { + assertContact(actual, expected.getName(), expected.getEmailAddress(), expected.getPhoneNumber()); + } + + private void assertContact(Contact contact, String name, String emailAddress, String phoneNumber) { + + assertThat(contact).isNotNull(); + assertThat(contact.getName()).isEqualTo(name); + assertThat(contact.getEmailAddress()).isEqualTo(emailAddress); + assertThat(contact.getPhoneNumber()).isEqualTo(phoneNumber); + } + + @Test + public void loadsExistingContactFromDatabaseThenGetsFromCache() { + + assertThat(this.contactsService.isCacheMiss()).isFalse(); + + Contact froDoe = this.contactsService.findByName("Fro Doe"); + + assertContact(froDoe, "Fro Doe", "frodoe@home.com", "503-555-1234"); + assertThat(this.contactsService.isCacheMiss()).isTrue(); + + Contact froDoeReloaded = this.contactsService.findByName(froDoe.getName()); + + assertContact(froDoeReloaded, froDoe); + assertThat((this.contactsService.isCacheMiss())).isFalse(); + } + + @Test + public void savesNewContactToDatabaseAndCachesContact() { + + assertThat(this.contactsService.isCacheMiss()).isFalse(); + + Contact pieDoe = Contact.newContact("Pie Doe") + .withEmailAddress("pieDoe@work.com") + .withPhoneNumber("503-555-4321"); + + pieDoe = this.contactsService.save(pieDoe); + + assertThat(this.contactsService.isCacheMiss()).isFalse(); + + Contact pieDoeLoaded = this.contactsService.findByName(pieDoe.getName()); + + assertContact(pieDoeLoaded, pieDoe); + assertThat(this.contactsService.isCacheMiss()).isFalse(); + } + + @Test + public void savesNewContactToDatabaseUsingRepositoryThenLoadsContactFromDatabaseAndCachesContact() { + + assertThat(this.contactsService.isCacheMiss()).isFalse(); + + Contact giDoe = Contact.newContact("Gi Doe") + .withEmailAddress("giDoe@thefarm.org") + .withPhoneNumber("971-555-8899"); + + giDoe = this.contactRepository.save(giDoe); + + Contact giDoeLoaded = this.contactsService.findByName(giDoe.getName()); + + assertContact(giDoeLoaded, giDoe); + assertThat(this.contactsService.isCacheMiss()).isTrue(); + + Contact giDoeReloaded = this.contactsService.findByName(giDoe.getName()); + + assertContact(giDoeReloaded, giDoe); + assertThat(this.contactsService.isCacheMiss()).isFalse(); + } +} diff --git a/spring-geode-tests/smoke-tests/multi-store/src/test/resources/application.properties b/spring-geode-tests/smoke-tests/multi-store/src/test/resources/application.properties new file mode 100644 index 00000000..f004202b --- /dev/null +++ b/spring-geode-tests/smoke-tests/multi-store/src/test/resources/application.properties @@ -0,0 +1,4 @@ +# Test Spring Boot application.properties for Multi-Store Spring Data Repositories + +spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=none diff --git a/spring-geode-tests/smoke-tests/multi-store/src/test/resources/data.sql b/spring-geode-tests/smoke-tests/multi-store/src/test/resources/data.sql new file mode 100644 index 00000000..587c32cf --- /dev/null +++ b/spring-geode-tests/smoke-tests/multi-store/src/test/resources/data.sql @@ -0,0 +1 @@ +INSERT INTO contacts (name, email_address, phone_number) VALUES ('Fro Doe', 'frodoe@home.com', '503-555-1234'); diff --git a/spring-geode-tests/smoke-tests/multi-store/src/test/resources/logback.xml b/spring-geode-tests/smoke-tests/multi-store/src/test/resources/logback.xml new file mode 100644 index 00000000..de99c52b --- /dev/null +++ b/spring-geode-tests/smoke-tests/multi-store/src/test/resources/logback.xml @@ -0,0 +1,22 @@ + + + + + + + + %d %5p %40.40c:%4L - %m%n + + + + + + + + + + + + + + diff --git a/spring-geode-tests/smoke-tests/multi-store/src/test/resources/schema.sql b/spring-geode-tests/smoke-tests/multi-store/src/test/resources/schema.sql new file mode 100644 index 00000000..2e920f8f --- /dev/null +++ b/spring-geode-tests/smoke-tests/multi-store/src/test/resources/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS contacts ( + name VARCHAR(256) PRIMARY KEY, + email_address VARCHAR(256), + phone_number VARCHAR(256) +);