diff --git a/docker-compose.yml b/docker-compose.yml
index 3d08a0ee64f..ef809d4dc16 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -25,3 +25,14 @@ services:
- POSTGRES_DB=petclinic
profiles:
- postgres
+ oracle:
+ image: gvenzl/oracle-free:slim-faststart
+ ports:
+ - "1521:1521"
+ environment:
+ - APP_USER=petclinic
+ - APP_USER_PASSWORD=petclinic
+ - ORACLE_RANDOM_PASSWORD=yes
+ - ORACLE_DATABASE=petclinic
+ profiles:
+ - oracle
diff --git a/pom.xml b/pom.xml
index ce55092cf66..0e3929a4229 100644
--- a/pom.xml
+++ b/pom.xml
@@ -85,7 +85,11 @@
postgresql
runtime
-
+
+ com.oracle.database.jdbc
+ ojdbc11
+ runtime
+
javax.cache
@@ -133,7 +137,11 @@
mysql
test
-
+
+ org.testcontainers
+ oracle-free
+ test
+
jakarta.xml.bind
jakarta.xml.bind-api
@@ -218,7 +226,7 @@
spring-boot-maven-plugin
-
build-info
@@ -376,7 +384,7 @@
-
org.eclipse.m2e
@@ -434,4 +442,4 @@
-
\ No newline at end of file
+
diff --git a/src/main/resources/application-oracle.properties b/src/main/resources/application-oracle.properties
new file mode 100644
index 00000000000..6dc9ecdcac4
--- /dev/null
+++ b/src/main/resources/application-oracle.properties
@@ -0,0 +1,6 @@
+database=oracle
+spring.datasource.url=${ORACLE_URL:jdbc:oracle:thin:@//localhost:1521/petclinic}
+spring.datasource.username=${APP_USER:petclinic}
+spring.datasource.password=${APP_USER_PASSWORD:petclinic}
+# SQL is written to be idempotent so this is safe
+spring.sql.init.mode=always
diff --git a/src/main/resources/db/oracle/data.sql b/src/main/resources/db/oracle/data.sql
new file mode 100644
index 00000000000..311a3d92529
--- /dev/null
+++ b/src/main/resources/db/oracle/data.sql
@@ -0,0 +1,53 @@
+INSERT INTO vets VALUES (default, 'James', 'Carter');
+INSERT INTO vets VALUES (default, 'Helen', 'Leary');
+INSERT INTO vets VALUES (default, 'Linda', 'Douglas');
+INSERT INTO vets VALUES (default, 'Rafael', 'Ortega');
+INSERT INTO vets VALUES (default, 'Henry', 'Stevens');
+INSERT INTO vets VALUES (default, 'Sharon', 'Jenkins');
+
+INSERT INTO specialties VALUES (default, 'radiology');
+INSERT INTO specialties VALUES (default, 'surgery');
+INSERT INTO specialties VALUES (default, 'dentistry');
+
+INSERT INTO vet_specialties VALUES (2, 1);
+INSERT INTO vet_specialties VALUES (3, 2);
+INSERT INTO vet_specialties VALUES (3, 3);
+INSERT INTO vet_specialties VALUES (4, 2);
+INSERT INTO vet_specialties VALUES (5, 1);
+
+INSERT INTO types VALUES (default, 'cat');
+INSERT INTO types VALUES (default, 'dog');
+INSERT INTO types VALUES (default, 'lizard');
+INSERT INTO types VALUES (default, 'snake');
+INSERT INTO types VALUES (default, 'bird');
+INSERT INTO types VALUES (default, 'hamster');
+
+INSERT INTO owners VALUES (default, 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023');
+INSERT INTO owners VALUES (default, 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749');
+INSERT INTO owners VALUES (default, 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763');
+INSERT INTO owners VALUES (default, 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198');
+INSERT INTO owners VALUES (default, 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765');
+INSERT INTO owners VALUES (default, 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654');
+INSERT INTO owners VALUES (default, 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387');
+INSERT INTO owners VALUES (default, 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683');
+INSERT INTO owners VALUES (default, 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435');
+INSERT INTO owners VALUES (default, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487');
+
+INSERT INTO pets VALUES (default, 'Leo', TO_DATE('2010-09-07', 'YYYY-MM-DD'), 1, 1);
+INSERT INTO pets VALUES (default, 'Basil', TO_DATE('2012-08-06', 'YYYY-MM-DD'), 6, 2);
+INSERT INTO pets VALUES (default, 'Rosy', TO_DATE('2011-04-17', 'YYYY-MM-DD'), 2, 3);
+INSERT INTO pets VALUES (default, 'Jewel', TO_DATE('2010-03-07', 'YYYY-MM-DD'), 2, 3);
+INSERT INTO pets VALUES (default, 'Iggy', TO_DATE('2010-11-30', 'YYYY-MM-DD'), 3, 4);
+INSERT INTO pets VALUES (default, 'George', TO_DATE('2010-01-20', 'YYYY-MM-DD'), 4, 5);
+INSERT INTO pets VALUES (default, 'Samantha', TO_DATE('2012-09-04', 'YYYY-MM-DD'), 1, 6);
+INSERT INTO pets VALUES (default, 'Max', TO_DATE('2012-09-04', 'YYYY-MM-DD'), 1, 6);
+INSERT INTO pets VALUES (default, 'Lucky', TO_DATE('2011-08-06', 'YYYY-MM-DD'), 5, 7);
+INSERT INTO pets VALUES (default, 'Mulligan', TO_DATE('2007-02-24', 'YYYY-MM-DD'), 2, 8);
+INSERT INTO pets VALUES (default, 'Freddy', TO_DATE('2010-03-09', 'YYYY-MM-DD'), 5, 9);
+INSERT INTO pets VALUES (default, 'Lucky', TO_DATE('2010-06-24', 'YYYY-MM-DD'), 2, 10);
+INSERT INTO pets VALUES (default, 'Sly', TO_DATE('2012-06-08', 'YYYY-MM-DD'), 1, 10);
+
+INSERT INTO visits VALUES (default, 7, TO_DATE('2013-01-01', 'YYYY-MM-DD'), 'rabies shot');
+INSERT INTO visits VALUES (default, 8, TO_DATE('2013-01-02', 'YYYY-MM-DD'), 'rabies shot');
+INSERT INTO visits VALUES (default, 8, TO_DATE('2013-01-03', 'YYYY-MM-DD'), 'neutered');
+INSERT INTO visits VALUES (default, 7, TO_DATE('2013-01-04', 'YYYY-MM-DD'), 'spayed');
diff --git a/src/main/resources/db/oracle/petclinic_db_setup_oracle.txt b/src/main/resources/db/oracle/petclinic_db_setup_oracle.txt
new file mode 100644
index 00000000000..e5fa53ff4f8
--- /dev/null
+++ b/src/main/resources/db/oracle/petclinic_db_setup_oracle.txt
@@ -0,0 +1,19 @@
+===============================================================================
+=== Spring PetClinic sample application - OracleSQL Configuration ===
+===============================================================================
+
+1) Run the "docker-compose.yml" from the root of the project:
+
+ $ docker-compose up oracle
+ ...
+ spring-petclinit-oracle-1 | #########################
+ spring-petclinit-oracle-1 | DATABASE IS READY TO USE!
+ spring-petclinit-oracle-1 | #########################
+ ...
+
+2) Run the app with `spring.profiles.active=oracle` (e.g. as a System property via the command
+ line, but any way that sets that property in a Spring Boot app should work). For example use
+
+ mvn spring-boot:run -Dspring-boot.run.profiles=oracle
+
+ To activate the profile on the command line.
diff --git a/src/main/resources/db/oracle/schema.sql b/src/main/resources/db/oracle/schema.sql
new file mode 100644
index 00000000000..9a3577b1296
--- /dev/null
+++ b/src/main/resources/db/oracle/schema.sql
@@ -0,0 +1,56 @@
+CREATE TABLE IF NOT EXISTS vets (
+ id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ first_name VARCHAR(30),
+ last_name VARCHAR(30)
+);
+CREATE INDEX IF NOT EXISTS vets_last_name ON vets (last_name);
+
+CREATE TABLE IF NOT EXISTS specialties (
+ id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ name VARCHAR(80)
+);
+CREATE INDEX IF NOT EXISTS specialties_name ON specialties (name);
+
+CREATE TABLE IF NOT EXISTS vet_specialties (
+ vet_id INTEGER NOT NULL,
+ specialty_id INTEGER NOT NULL,
+ CONSTRAINT fk_vet_specialties_vets FOREIGN KEY (vet_id) REFERENCES vets (id),
+ CONSTRAINT fk_vet_specialties_specialties FOREIGN KEY (specialty_id) REFERENCES specialties (id)
+
+);
+
+CREATE TABLE IF NOT EXISTS types (
+ id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ name VARCHAR(80)
+);
+CREATE INDEX IF NOT EXISTS types_name ON types (name);
+
+CREATE TABLE IF NOT EXISTS owners (
+ id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ first_name VARCHAR(30),
+ last_name VARCHAR(30),
+ address VARCHAR(255),
+ city VARCHAR(80),
+ telephone VARCHAR(20)
+);
+CREATE INDEX IF NOT EXISTS owners_last_name ON owners (last_name);
+
+CREATE TABLE IF NOT EXISTS pets (
+ id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ name VARCHAR(30),
+ birth_date DATE,
+ type_id INTEGER NOT NULL,
+ owner_id INTEGER,
+ CONSTRAINT fk_pets_owners FOREIGN KEY (owner_id) REFERENCES owners (id),
+ CONSTRAINT fk_pets_types FOREIGN KEY (type_id) REFERENCES types (id)
+);
+CREATE INDEX IF NOT EXISTS pets_name ON pets (name);
+
+CREATE TABLE IF NOT EXISTS visits (
+ id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ pet_id INTEGER,
+ visit_date DATE,
+ description VARCHAR(255),
+ CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id)
+);
+CREATE INDEX IF NOT EXISTS visits_pet_id ON visits (pet_id);
diff --git a/src/test/java/org/springframework/samples/petclinic/OracleIntegrationTests.java b/src/test/java/org/springframework/samples/petclinic/OracleIntegrationTests.java
new file mode 100644
index 00000000000..50dac4f9d09
--- /dev/null
+++ b/src/test/java/org/springframework/samples/petclinic/OracleIntegrationTests.java
@@ -0,0 +1,54 @@
+package org.springframework.samples.petclinic;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledInNativeImage;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.samples.petclinic.vet.VetRepository;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.web.client.RestTemplate;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.oracle.OracleContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@ActiveProfiles("oracle")
+@Testcontainers(disabledWithoutDocker = true)
+@DisabledInNativeImage
+class OracleIntegrationTests {
+
+ @ServiceConnection
+ @Container
+ static OracleContainer container = new OracleContainer("gvenzl/oracle-free:slim-faststart");
+
+ @LocalServerPort
+ int port;
+
+ @Autowired
+ private VetRepository vets;
+
+ @Autowired
+ private RestTemplateBuilder builder;
+
+ @Test
+ void testFindAll() {
+ vets.findAll();
+ vets.findAll(); // served from cache
+ }
+
+ @Test
+ void testOwnerDetails() {
+ RestTemplate template = builder.rootUri("http://localhost:" + port).build();
+ ResponseEntity result = template.exchange(RequestEntity.get("/owners/1").build(), String.class);
+ assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
+ }
+
+}
diff --git a/src/test/java/org/springframework/samples/petclinic/OracleTestApplication.java b/src/test/java/org/springframework/samples/petclinic/OracleTestApplication.java
new file mode 100644
index 00000000000..d60760d11a1
--- /dev/null
+++ b/src/test/java/org/springframework/samples/petclinic/OracleTestApplication.java
@@ -0,0 +1,30 @@
+package org.springframework.samples.petclinic;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.testcontainers.oracle.OracleContainer;
+
+/**
+ * PetClinic Spring Boot Application
+ *
+ * @author Radu Ghiorma
+ *
+ */
+@Configuration
+public class OracleTestApplication {
+
+ @ServiceConnection
+ @Profile("oracle")
+ @Bean
+ static OracleContainer container() {
+ return new OracleContainer("gvenzl/oracle-free:slim-faststart");
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(PetClinicApplication.class, "--spring.profiles.active=oracle");
+ }
+
+}