# Running Oracle Database Server as a Docker container A series of 3 article(s), in reading order. # Running Oracle Database Server as a Docker container > Learn how to run Oracle Database Server inside a Docker container. This step-by-step guide covers image setup, license acceptance, token generation, and database access. --- ![Running Oracle Database Server as a Docker container](/img/v2/oracle.webp) **TL;DR** This comprehensive guide provides a step-by-step walkthrough for running an Oracle Enterprise Database server in a Docker container. It covers the entire process, from the initial setup of creating an Oracle account and handling authentication to pull the official image, to deploying the container with persistent storage and custom startup scripts. You'll learn the key concepts of Oracle's multi-tenant architecture (CDB vs. PDB) and how to connect to and manage your database using tools like `sqlplus`, Oracle SQL Developer, and the web-based Oracle Enterprise Manager. At work, I had to create an Oracle Enterprise database locally so that I could code scripts in different languages to show [how to connect to Oracle using, for example .Net, NodeJS, Python or PHP](/blog/oracle-dotnet-nodejs-php-python). So the idea was immediately to check whether there was an official image for this. And there is one but, damned, it's not as simple as for PostgreSQL, MySQL or [MS SQL Server](/blog/docker-mssql-server). In this article, which is a clean-up of my notes, we're going to install Oracle Enterprise locally in a container, create a database with example data and access the database in several ways. It's a step-by-step guide to make life easier for anyone who needs to do the same thing - me, for example, in a few months' time when I've forgotten everything. It takes a while (Oracle is slow to initialize), but here's where we're headed: a running Oracle container, ready to query. ![Database is ready to use](./images/container_db_is_ready.webp) ![Oracle getting the list of employees](./images/oracle_getting_employees.webp) Let's build it, starting with the image itself. ## Download a Docker image for OracleDB So, yes, there is a Docker image called `container-registry.oracle.com/database/enterprise:latest` but before being able to download it, we'll need to do a few things: 1. Create a free account on oracle.com 2. Generate a token 3. Accept License terms 4. Configure your local Docker environment to use that token If you miss one step and try to download the image immediately, you'll get this error: ```bash $ docker pull container-registry.oracle.com/database/enterprise:latest Error response from daemon: Head "https://container-registry.oracle.com/v2/database/enterprise/manifests/21.3.0.0": unauthorized: Auth failed. ``` ### Creation of a free account on oracle.com Please surf on and create a user by clicking on the `Sign In` button / top right link. > Screenshot — https\://container-registry.oracle.com/ords/ocr/ba/database/enterprise > > Make sure to be logged in > Screenshot — https\://container-registry.oracle.com/ords/ocr/ba/database/enterprise > > Oracle sign in page ### Generate a token Once your user has been validated (after reception of an email); go back to the website; log in and click on your profile then choose `Auth Token`. > Screenshot — https\://container-registry.oracle.com/ords/ocr/ba/database/enterprise > > Creation of your Auth Token Click on the `Generate Secret Key` button and copy your **SSO Username** and your **Secret Key** somewhere (in your password vault application f.i.). ### Accept License terms Please go to and search for `enterprise` in the `Repository` column. > Screenshot — https\://container-registry.oracle.com/ords/ocr/ba/database > > Click on enterprise Being logged in, please click on the `Continue` button that appears on the right. > Screenshot — https\://container-registry.oracle.com/ords/ocr/ba/database > > Accepting the license terms A popup window will be displayed and, at the bottom, you'll need to click on `Accept`. Optional, by going back to you'll see that Oracle has recorded the fact you've accepted their terms. > Screenshot — https\://container-registry.oracle.com/ords/ocr/ba/database > > Accepting License Agreement ### Configure your local Docker environment Last part, teach Docker you can download from the Oracle registry. Go to the command line and type `docker login container-registry.oracle.com`. Fill in your **SSO Username** and your **Secret Key** (as given by the Auth Token page on oracle.com). Docker will display *Login Succeeded*. > **Optional, here is the disconnect command:** > > If needed, run `docker logout container-registry.oracle.com` to remove the authentication to oracle.com on your computer. > **Optional, the authentication is stored in the config.json file:** > > By running `cat ~/.docker/config.json`, you'll see in `auths` the presence of the Oracle registry > > **\~/.docker/config.json** > > ```json > { > "auths": { > "container-registry.oracle.com": {}, > "https://index.docker.io/v1/": {} > } > } > ``` ### Finally, you can pull the image If all previous steps have been correctly done, now, you should be able to run `docker pull container-registry.oracle.com/database/enterprise:latest` without any error and download the 3.5GB Docker image. > **The latest image is huge but the slim one is too old:** > > There is a slim image around 1.5GB (`docker pull container-registry.oracle.com/database/enterprise:12.2.0.1-slim`) but it's a very old one (somewhere in 2018). ## Important Concepts to Consider When Working With Oracle v12 and After (skip this if you just want to run it) ### Container database (CDB) versus Pluggable database (PDB) In Oracle's multi-tenant architecture (introduced since Oracle 12c), the concepts of *Container Database* (`CDB`) and **Pluggable Database** (`PDB`) are fundamental. In comparison with other database engines, a `PDB` is a database while the `CDB` is the *underlying container infrastructure that manages one or more PDBs, sharing common background processes and memory*. > **For this article, we don't work at the CDB level at all:** > > In this article, we're only interested in creating tables in a database (PDB) — **we always need to make sure to connect to a PDB when creating tables.** We want our tables to be in a single database; not "shared" at CDB root level. > > So, every time we'll need to work on tables; make sure to connect to the `PDB`. ## Create an Oracle database container Once the image has been pulled, we can create our container i.e. our Oracle database service. The list of supported command line flags is here: If we wish, we can run a `docker run` command right now but, before, let's make some configuration. Why? Because the container generation is terribly slow and can take up to ten minutes. In order to be as efficient as possible, we'll first create a set of files on our disk so while creating the database, Docker will also automatically create some tables and put records in them. ### Creation of some configuration files Please create a new folder on your disk, jump in it, then create the folder where we'll put our startup files: ```bash $ mkdir -p /tmp/oracle && cd $_ $ mkdir -p scripts/startup ``` #### Let's create a Human Resources sample database Oracle provides some sample databases on the [Oracle Database Sample Schemas](https://github.com/oracle-samples/db-sample-schemas/releases) page. You can download from there the `db-sample-schemas-23.3.zip` archive, open it and go to the `human_resources` folder. You'll found there a file called `hr_create.sql` which is a sample of an HR database. There is a second file called `hr_populate.sql` to feed the tables with some data. > **Information:** > > For easiness, you can retrieve these files by clicking on these two links: [hr\_create.sql](./files/hr_create.sql) and [hr\_populate.sql](./files/hr_populate.sql). Please download and copy these two files in the `scripts/startup/sql/` folder you've previously created. The second thing to do is to create the file `scripts/startup/populate_db.sh` with the content below so we'll automate the creation of our tables and put some records in our database. As said in a previous chapter, it's important to make sure to connect to the `PDB` so tables are created in a database; not in the container. **scripts/startup/populate\_db.sh** ```bash #!/usr/bin/env bash sqlplus -S sys/admin@localhost:1521/ORCLCDB AS SYSDBA < **Tables will be owned by the system user:** > > Pay attention to the `CONNECT system/admin@` orclpdb1 `statement: we'll first connect as the system user before creating the tables. So, tables will be accessible by that user i.e. the`system\` user. If you need the data model, here it is: ![The Oracle Human Resources sample relations](./images/hr_schema.webp) ### Create the OracleDBData volume Because running the container is a very slow process, we'll create a Docker volume to keep the database persistent on the disk. The volume will keep the database on our disk, in a Docker self-managed volume. Like this, we can stop the Oracle container, start it again and we'll not lose our database. *The DB will be persistent into a self-manager Docker volume.* Please run this command in the console to create a Docker self-managed volume: ```bash $ docker volume create OracleDBData ``` ### Create the oracle network And lastly, we'll create a specific network for our Oracle container: ```bash $ docker network create oracle ``` ### Ready to create the container We're ready to create our container by running this command: ```bash $ docker run -d \ --name oracle-db \ --network oracle \ -p 1521:1521 \ -p 5500:5500 \ -e ORACLE_SID=ORCLCDB \ -e ORACLE_PDB=ORCLPDB1 \ -e ORACLE_PWD=admin \ -v OracleDBData:/opt/oracle/oradata \ -v ./scripts/startup/:/docker-entrypoint-initdb.d/startup \ container-registry.oracle.com/database/enterprise:latest ``` > **Note:** > > This is terribly slow\... Oracle will need something like 10 minutes before the container can be used. The `docker run` was terribly complex; here is a breakdown: * `--name oracle-db`: Our running container will be named oracle-db (so we can access to it later on by running `docker exec -it oracle-db [...]`), * `--network oracle`: We will create our container on the `oracle` network, * `-p 1521:1521`: Oracle use a port number 1521 (Oracle Listener) and we'll expose that port on our machine, * `-p 5500:5500`: Optional. Oracle use a second port number 5500 (OEM Express) and we'll expose that port too so we'll be able to access to `http://localhost:`5500, * `-e ORACLE_SID=ORCLCDB`: The Oracle Database SID (SID stands for *System identifier*) is a very important setting. We'll keep the default value which is ORCLCDB (remember; `CDB` stands for `Container database`) * `-e ORACLE_PDB=ORCLPDB1`: The Oracle Database PDB name. Here too, we'll keep the default value which is ORCLPDB1. (remember; `PDB` stands for `pluggable database`). * `-e ORACLE_PWD=admin`: The Oracle database SYS, SYSTEM and PDBADMIN password. There is no default value since the password is auto generated and should be retrieved from the logs (`docker log`). This parameter modifies the password for the SYS, SYSTEM and PDBADMIN users. * `-v OracleDBData:/opt/oracle/oradata`: We'll persist the database on our disk but we don't care *where*, we'll let Docker manage it. We've previously created a Docker volume named `OracleDBData`. * `-v ./scripts/startup/:/docker-entrypoint-initdb.d/startup`: This will allow us to create files locally in the `./scripts/startup` folder (like our SQL files) and run them inside the container once the main Oracle database is ready. And, indeed, we've previously created the script `populate_db.sh`, `sql/hr_create.sql` and `sql/hr_populate.sql` files in our `scripts/startup` folder. ## Important constants to remember From now on, our Oracle database server is running as a Docker container. We'll use some constants a lot. Here are the constants to remember: * 1521 is the port number to use when connecting to the Oracle DB service, * oracle-db is the name of our container, * ORCLCDB is the name of our CDB, * ORCLPDB1 is the name of our pluggable database, * `admin` is the password to use and * `system` is the user used when we've created the database If you look back at the `populate_db.sh` script, we had: **scripts/startup/populate\_db.sh** ```bash # highlight-next-line sqlplus -S sys/admin@localhost:1521/ORCLCDB AS SYSDBA < **Am I connected on the container database or in a pluggable database?:** > > Just run, in the SQL\*Plus console, the `SHOW CON_NAME;` command. > > ![Connected on the PDB](./images/connected_on_pdb.webp) > > If you're connected to the CDB, you'll get the next answer and it's wrong. Type `exit` and connect to the PDB. > > ![Connected on the CDB](./images/connected_on_cdb.webp) ### A word about schema Previously, we've created our tables using the `scripts/startup/populate_db.sh` script. Here was the script used: **scripts/startup/populate\_db.sh** ```bash #!/usr/bin/env bash sqlplus -S sys/admin@localhost:1521/ORCLCDB AS SYSDBA < **Information:** > > `SET WRAP OFF` will allow us to use the screen's full width instead of a ridiculous width of 80 characters and `SET PAGESIZE 1000` is to avoid a paginated list every 10 records. ### Accessing the Oracle Enterprise Manager Database Express Earlier, when running our `docker run` command, we've mapped the port 5500 from the container on our host. That port is the one used by Oracle Enterprise Manager Database Express (aka `OEM Express`). This is a web application we can reach by surfing to `https://localhost:` 5500`/em/`. Connect to it using `system` / `admin` and orclpdb1 as container name. > Screenshot — https\://localhost:5500/em/ > > Oracle Enterprise Manager Database Express ### Accessing our database using Oracle tools #### Oracle SQL Developer You can download [Oracle SQL Developer](https://www.oracle.com/be/database/sqldeveloper). It's a ZIP file and once unzipped; you can run the interface without the need to install the software, it's ready-to-use. Once installed, run it and create a new connection to: | Label | Value | | ------------ | ----------- | | Username | `SYS` | | Role | `SYSDBA` | | Password | `admin` | | Hostname | `127.0.0.1` | | Port | `1521` | | Service name | `orclpdb1` | ![Creating a connection in Oracle SQL Developer as sys](./images/oracle_sql_dev_as_sys.webp) > **Remember; you should connect to our PDB; not to the CDB:** > > Tables are located in the *pluggable database* (`PDB`); not in the *container database* (`CDB`) ![Getting data using Oracle SQL Developer](./images/oracle_sql_get_data.webp) Start to type `SELECT * FROM system.` and press `CTRL`+`SPACE` to get the list of objects in that schema. By scrolling a few, you'll find employees. ![Oracle get employees](./images/oracle_get_employees.webp) Validate or simply type the full SQL `SELECT * FROM system.employees`. In the example below, I'll ask the list of regions and the list of employees. I will run the query by simply pressing `F5` — the same result already shown at the top of this article. ##### Connect to Oracle SQL Developer using the correct user In the previous chapter, we've established a login using `sys` for the username and for that reason, if we click in the `Tables (Filtered)` menu in the left tree view, we'll get a bunch of tables but not the one of the `system` user like our employees table. ![Creating a connection in Oracle SQL Developer as system](./images/oracle_sql_dev_as_system.webp) Now, by unfolding the list of tables, we can see ours: ![Seeing our Human Resources tables](./images/oracle_sql_dev_tables.webp) #### Access our Oracle DB container using Oracle SQLPlus Another way would be to use the official sql\*plus Docker image like this: ```bash $ docker run --rm -it --network oracle oracletools/sqlplus:v19.18_lin SYS/admin@oracle-db:1521/orclpdb1 as sysdba ``` Once connected, for instance, we can get the list of countries like this: ![Oracle sqlplus](./images/oracle_sqlplus_countries.webp) > **Intern versus extern:** > > For this article, it was not really needed to use that image. Until now, we've more than once used SQL\*Plus like when we've fired `docker exec -it ` oracle-db ` sqlplus sys/admin@` ORCLPDB1 ` as sysdba` to connect to the database. > > The difference is: `docker exec -it ` oracle-db ` [...]` jumps into our database container and runs sqlplus *internally* (from within the container where the database is stored) while `docker run [...] oracle oracletools/sqlplus:v19.18_lin [...]` runs sqlplus *outside* as a separate container. ## Going further Now that the oracle-db container is running, two follow-up articles reuse it directly: [Transform an Oracle DB as OpenData using Oracle REST Data Services](/blog/docker-oracle-ords) and [Accessing an Oracle database using .Net, NodeJS, PHP and Python](/blog/oracle-dotnet-nodejs-php-python). --- # Transform an Oracle DB as OpenData using Oracle REST Data Services > Set up Oracle REST Data Services (ORDS) in Docker to transform your Oracle DB into OpenData with a powerful REST API. Includes filtering and pagination. --- ![Transform an Oracle DB as OpenData using Oracle REST Data Services](/img/v2/oracle.webp) **TL;DR** This tutorial demonstrates how to transform your Oracle Database into a powerful RESTful API using Oracle REST Data Services (ORDS) in Docker. Building on a previously established Oracle DB container, this guide walks you through deploying the ORDS container, linking it to your database, and configuring schemas for REST access. You'll learn how to REST-enable specific database tables or views using Oracle SQL Developer and then access your data via auto-generated OpenAPI endpoints. The article also covers how to use URL parameters for advanced querying, including filtering, sorting, and pagination. In a previous article ([Running Oracle Database Server as a Docker container](/blog/docker-oracle-database-server)), we've created a Docker container called `oracle-db` where a Human Resources database is running. In this article we'll partially expose the database on the web using OpenAPI so, in short, we'll allow authorized people to access, f.i., `http://oursite/api/employees` to get the list of all employees. *That generated OpenAPI schema can be linted, by the way; see [Validate your OpenAPI schema against the Belgif REST standards](/blog/belgif-api-linter).* Exactly the same way we've done with PostgREST (see my [Don't query your PostgreSQL db anymore, prefer PostgREST](/blog/docker-postgrest)) but, this time, with an Oracle database. And the magic will happen thanks **[Oracle REST Data Services](https://www.oracle.com/database/technologies/appdev/rest.html)** aka **ords**. Here's where we're headed: the same Oracle table, now reachable as plain JSON over HTTP. ![Getting the list of employees as JSON using curl](./images/getting_employees_as_json_curl.webp) In the previous article ([Running Oracle Database Server as a Docker container](/blog/docker-oracle-database-server)), we've created a Docker container called `oracle-db` where a Human Resources database is running. Please read that article and follow steps so, before continuing here, you've a running Docker container as described below. ## Some prerequisites **Before you begin make sure...** 1. You've a running Docker container called `oracle-db` 2. The container is thus running on your `localhost` 3. The port number to access your database is `1521` 4. The credentials to use is `SYS` (or `SYSTEM`) and `admin` for the password 5. The container contains a database called `ORCLPDB1` (also called service name) which is the Human Resources sample of Oracle (refer to the mentioned article) and 6. Our database `oracle-db` is running on a network called `oracle` ## Download a Docker image for Oracle REST Data Services Unlike the Docker Oracle Database Server image, we don't need any credentials for downloading the official [Oracle ORDS](https://container-registry.oracle.com/ords/ocr/ba/database/ords-developer) image. Simply run `docker pull container-registry.oracle.com/database/ords-developer:latest` to get it. It's a 2.2 GB file size image. ```bash $ docker pull container-registry.oracle.com/database/ords-developer:latest ``` ## Create an Oracle ORDS container We'll need to proceed step-by-step. ### Create a volume for the ORDS configuration ORDS requires two folders, one for what it calls **its secrets** and one for **its configuration files**. Let's use a Docker self managed volume for the configuration files so please run `docker volume create ords_config` in your console. ### Configure the connection string In order to be able to start ORDS, the official doc states we need to create a file called `conn_string.txt` with our connection string. In our use case, here is the file we need to create: ```bash $ mkdir -p ords_secrets $ chmod 777 ords_secrets $ echo 'CONN_STRING=SYS/admin@oracle-db:1521/ORCLPDB1' > ords_secrets/conn_string.txt ``` > **Information:** > > If you've a doubt about which value has to be used as `service_name`; start a sqlplus console (`docker exec -it ` oracle-db ` sqlplus sys/admin@` ORCLPDB1 ` as sysdba`) and run `SELECT global_name FROM global_name;` in SQL\*Plus. > > ![Getting service name](./images/getting_service_name.webp) The official ORDS Docker image comes with APEX (which is the abbreviation for *Oracle Application Express*). Based on the documentation, *Oracle Application Express (APEX) is a low-code development platform that enables you to build scalable, secure enterprise apps, with world-class features, that can be deployed anywhere. See for more information.* For our needs, we just want ORDS; we don't need APEX. For this purpose, we'll initialise the `IGNORE_APEX` OS variable to `TRUE`. ### Create the container To create our ORDS (without APEX) container; please run: ```bash $ docker run -d --rm \ --name oracle-ords \ --network oracle \ -p 8181:8181 \ -e IGNORE_APEX=TRUE \ -v ./ords_secrets/:/opt/oracle/variables \ -v ords_config:/etc/ords/config/ \ container-registry.oracle.com/database/ords-developer:latest ``` > **The `docker run` explained:** > > * `-d`: the ORDS will run as a daemon service, > * `--rm`: once the service is terminated, the Docker container will be removed, > * `--network`: we should run ORDS on the same network than our database; i.e. `oracle` > * `-p > 8181 > :8181`: we've to map the intern port `8181` used by ORDS on our host and, for simplicity, we'll expose the port 8181 also, > * `-e IGNORE_APEX=TRUE`: as mentioned, we want ORDS; not APEX so tells Docker to not install APEX, > * `-v ./ords_secrets/:/opt/oracle/variables`: as stated in the documentation, we need to provide a file called `conn_string.txt` and to map that file in the `/opt/oracle/variables` inside the container and, finally, > * `-v ords_config:/etc/ords/config/`: we'll use a self-managed Docker volume to keep the configuration files of ORDS. That command will add the ORDS layer in your database (based on the connection string; which is ORCLPDB1 for us). ![Installation of ORDS](./images/ords_installation.webp) > **On subsequent runs, we shouldn't provide connection string anymore.:** > > Once ORDS has been installed as done here above, if you need to rerun the ORDS container, we should no more provide the secret so we have to remove the `-v ./ords_secrets/:/opt/oracle/variables` flag. > > If you need to run the container once more; here is the command line: > > ```bash > $ docker run -d --rm \ > --name oracle-ords \ > --network oracle \ > -p 8181:8181 \ > -v ords_config:/etc/ords/config/ \ > container-registry.oracle.com/database/ords-developer:latest > ``` ### We need to create our user in our database Before being able to use ORDS and access objects using an HTTP request, we should create a new user in our database and give it some rights. Run `docker exec -it ` oracle-db ` sqlplus sys/admin@` ORCLPDB1 ` as sysdba` to start a SQL\*Plus console and connect to the PDB database. We need to create a user for ORDS; let's say user `hr`. Please run this statement: `CREATE USER hr IDENTIFIED BY admin;` Then grant permissions: `GRANT CONNECT, RESOURCE, UNLIMITED TABLESPACE TO hr;` ![Create the ORDS user](./images/ords_create_user.webp) Once created, we need to exit the console (started using the `sys` user) and reconnect with our user. Type thus `exit` to quit the sqlplus console and run `docker exec -it ` oracle-db ` sqlplus hr/admin@` ORCLPDB1. Once back in the sqlplus console (logged in as user `hr`), please run : `EXECUTE ORDS.ENABLE_SCHEMA;`. ![Enabling the ORDS Schema](./images/ords_enable_schema.webp) *(read [ORDS 101: Enabling Oracle Schemas for HTTPS/REST](https://www.thatjeffsmith.com/archive/2023/09/ords-101-enabling-oracle-schemas-for-https-rest/) if you want deeper info)* > **Caution:** > > If you get an error at this level, it means ORDS wasn't installed in your database. Please read again the **Create the container** chapter. #### Start ORDS web interface At this stage, we've installed ORDS, configured our database to use it and we've created a user called `hr`. We can surf to `http://localhost:` 8181`/ords` and connect to the ORDS dashboard: > Screenshot — http\://localhost:8181/ords/\_/landing > > ORDS Welcome page > **Note:** > > APEX wasn't installed and thus disabled Use `hr` and `admin`, our custom user, for the login page: > Screenshot — http\://localhost:8181/ords/\_/landing > > ORDS login page Right now, we can ask ORDS to see the list of objects already accessible; simply surf to `http://localhost:` 8181`/ords/hr/open-api-catalog/` to get ... an empty list. This is normal since we should specify which object (a table, a view, a stored procedure) can be accessible or not. But, yes, by accessing `http://localhost:` 8181`/ords/hr/open-api-catalog/` and getting a JSON answer; we can confirm ORDS is running fine. The `http://localhost:` 8181`/ords/hr/open-api-catalog/` page is called the **Schema Metadata** ([documentation](https://docs.oracle.com/en/database/oracle/oracle-rest-data-services/21.4/aelig/developing-REST-applications.html#GUID-55736274-502E-4511-B232-829924334FA2)). #### REST enable database objects So, we need to enable each object one by one. The easier way is to use [Oracle SQL Developer](https://www.oracle.com/be/database/sqldeveloper). If you don't have it, just download it for free. ![Running Oracle SQL Developer](./images/starting_sql_dev.webp) Create a new connection with these settings: | Label | Value | | ------------ | ------------------------ | | Name | `Human Resources - ORDS` | | Username | `hr` | | Role | `default` | | Password | `admin` | | Hostname | `127.0.0.1` | | Port | `1521` | | Service name | `orclpdb1` | ![SQL Developer - Database connection](./images/sql_dev_login_page.webp) Click on the `Save` button then on the `Connect` one. Just after the login, by clicking on the list of tables or views, there is nothing. This is normal, we've just created our `hr` user and the associated `hr` schema and we don't have objects in that schema. ![Right now, there is nothing to REST enable](./images/sql_dev_hr_no_objects.webp) ##### Adding an employee view in our hr schema Let's connect to our database and display the list of employees: please run `docker exec -it ` oracle-db ` sqlplus sys/admin@` ORCLPDB1 ` as sysdba`; then `SELECT EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL FROM SYSTEM.EMPLOYEES;`. ![Getting the list of employees](./images/system_employees.webp) Now, we're sure we've access to the table, let's create a view in our `hr` schema to display the table content: * run `GRANT SELECT ON SYSTEM.EMPLOYEES TO hr;` and * create the view by running `CREATE VIEW HR.EMPLOYEES AS SELECT EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID FROM SYSTEM.EMPLOYEES;`. If we wish to check our view: * Type `exit` in the sqlplus console since you're connected as `sys`, * In the Linux console, run `docker exec -it ` oracle-db ` sqlplus hr/admin@` ORCLPDB1 to connect as `hr` then * run `SELECT * FROM HR.EMPLOYEES;`. As you can see, you'll obtain the list of employees. So, at this stage, we've added a view called `employees` in our `hr` schema and it works. > **Don't use SELECT \* FROM:** > > It's a very bad practice to use `SELECT * FROM ...`, always make sure to select the needed fields. Go back to the Oracle SQL Developer interface, right-click on the `Views` node and choose `Refresh`. You'll see the added view: ![The employees view](./images/sql_dev_employees_view.webp) Right-click on the `EMPLOYEES` view and click on `Enable REST Service...`. ![Enabling REST](./images/sql_dev_enable_rest.webp) Make sure to check the `Enable object` checkbox, give a name to your object and, for this small tutorial, uncheck the `Authorization required` checkbox. ![REST enable the employees view](./images/rest_enable_employees_1.webp) Click on the `Next` button and just pay attention on the `SQL` tab: ![REST enable the employees view - DDL part](./images/rest_enable_employees_2.webp) You'll get the DDL to run in a console if you want to REST enable the table by code; not by using the GUI. The last part is to jump now in the ORDS website (`http://localhost:` 8181`/ords/hr/_sdw/?nav=rest-workshop`). Please refresh the page and you'll now see you've one object in the `AUTOREST` area: > Screenshot — http\://localhost:8181/ords/hr/\_sdw/?nav=rest-workshop > > There is one object in the AUTOREST area Click on that area and you'll see the object (which is a view) we'll REST enabled previously: > Screenshot — http\://localhost:8181/ords/hr/\_sdw/?nav=rest-workshop > > Employees is AUTOREST enabled Look at the *Open in a new tab* icon on the image above. Click on that button and you'll get your records as a JSON response: > Screenshot — http\://localhost:8181/ords/hr/employees/ > > Getting the list of employees as JSON REST answer The `http://localhost:` 8181`/ords/hr/employees/` is called the **Object Data** ([documentation](https://docs.oracle.com/en/database/oracle/oracle-rest-data-services/21.4/aelig/developing-REST-applications.html#GUID-0B17836D-E5B5-4B45-A9DA-0ABF62426EDF)) Since it's nothing more than a URL, you can use it with any tools you want, f.i. using `curl`. The command to run is `curl http://localhost : 8181 /ords/hr/employees/ | jq` — the same output already teased at the top of this article. ###### And what about the OpenAPI catalog Please go back to your browser and access `http://localhost:` 8181`/ords/hr/open-api-catalog/` again. This time, you'll see that you've one item called `EMPLOYEES`. If you want to get the description of that *resource*, the page `http://localhost:` 8181`/ords/hr/open-api-catalog/employees/` can be visited to get f.i. the list of fields exposed. That page is called the **Object Metadata** ([documentation](https://docs.oracle.com/en/database/oracle/oracle-rest-data-services/21.4/aelig/developing-REST-applications.html#GUID-B870CF7E-4A19-4646-ACB4-84CC9FD5967E)) Let's create a new view for the fun: * Run `docker exec -it ` oracle-db ` sqlplus sys/admin@` ORCLPDB1 ` as sysdba` then * In the SQL console, please run `GRANT SELECT ON SYSTEM.DEPARTMENTS TO HR;` followed by * `CREATE VIEW HR.DEPARTMENTS AS SELECT DEPARTMENT_ID, DEPARTMENT_NAME, MANAGER_ID, LOCATION_ID FROM SYSTEM.DEPARTMENTS;` and * Jump in the Oracle SQL Developer interface, refresh the list of view, right-click on the new `departments` view Now, by refreshing `http://localhost:` 8181`/ords/hr/open-api-catalog/` again, you'll see you've now a second item called `DEPARTMENTS`. > Screenshot — http\://localhost:8181/ords/hr/open-api-catalog/ > > Accessing the metadata-catalog ##### Working with REST Go to `http://localhost:` 8181`/ords/hr/_sdw/?nav=rest-workshop` or, from the hamburger menu, click on the `REST` item. > Screenshot — http\://localhost:8181/ords/hr/\_sdw/?nav=rest-workshop > > REST dashboard ### Using Paging, Filtering and Ordering Options on the Querystring (optional — skip if the default endpoint is enough) When accessing to an endpoint (like `http://localhost:` 8181`/ords/hr/employees/`), we can manipulate the URL to add parameters like paging, filtering or ordering options. These are called **JSON QBE** for JSON query-by-example. See the official documentation: #### Pagination > [Doc](https://docs.oracle.com/en/database/oracle/oracle-rest-data-services/21.4/aelig/developing-REST-applications.html#GUID-6B5B51E6-0F35-4FB6-B271-FCC31E835347) Unless specifically configured, ORDS will use pagination, i.e. it will limit the list of records to 25 rows. So, if you've more than 25 employees, the endpoint `http://localhost:` 8181`/ords/hr/employees/` will only return the first 25 of them. By accessing a "full" page like `http://localhost:` 8181`/ords/hr/employees/`, we'll in fact just receive a specific number of records (as configured in ORDS). At the end of the JSON answer, there will be navigation links: > Screenshot — http\://localhost:8181/ords/hr/employees/ > > Pagination As you can see on the image; the JSON answer will provide a list of records (in an `items` so-called array) then a few properties like `hasMore` (true/false), `limit` i.e. the number of rows in each call, `offset` is the "page" (when 0 the first 25 rows are displayed, when 1 then rows 26 till 50, ...), `count` is the number of rows in the answer. So, while `hasMore` is `true`, there are still records to obtain with a subsequent request. There are two parameters to control paging of result data: `offset` and `limit`. So we can run multiple requests like `http://localhost:` 8181`/ords/hr/employees/?offset=25`, `http://localhost:` 8181`/ords/hr/employees/?offset=50` and so on. We can define the number of records by page using the `limit` querystring parameter: `http://localhost:` 8181`/ords/hr/employees/?limit=100`. > **Note:** > > The official documentation strongly discourages removing the limit (and thus asking for all records at once). For this reason there is no way to remove the limit. If you really wish to get the full list, try with a very high number like `limit=1000000` (one million). #### Filtering The general syntax is using `/?q={"column":{"$eq":"value"}}`. ##### Greater than For instance, who earn more than 20,000€ as salary? `http://localhost:` 8181`/ords/hr/employees/?q={"salary":{"$gt":20000}}` will apply a filter on `salary > 20000`. > Screenshot — http\://localhost:8181/ords/hr/employees/?q={"salary":{"$gt":20000}} > > Who earns more than 20k ##### Equal To find all employees called `Steven`: `http://localhost:` 8181`/ords/hr/employees/?q={"first_name":{"$eq":"Steven"}}` > Screenshot — http\://localhost:8181/ords/hr/employees/?q={"first\_name":{"$eq":"Steven"}} > > Steven ##### Instring / contains / like Employees with the pattern `alex` in their first name: `http://localhost:` 8181`/ords/hr/employees/?q={"first_name":{"$instr":"alex"}}` (will match f.i. `Alexander` or `Alexis`). > Screenshot — http\://localhost:8181/ords/hr/employees/?q={"first\_name":{"$instr":"alex"}} > > Contains Alex Who is working as ICT? Here the like operator will be the one to use: `http://localhost:` 8181`/ords/hr/employees/?q={"job_id":{"$like":"IT_%"}}` (`IT_%` means *starts with*) > Screenshot — http\://localhost:8181/ords/hr/employees/?q={"job\_id":{"$like":"IT\_%"}} > > Who works for IT ##### Complex filtering We can also use AND like in this example: `http://localhost:` 8181`/ords/hr/employees/?q={"first_name":{"$instr":"alex"},"salary":{"$gt":5000}}` i.e. retrieve all people having `alex` in their first name and earning more than 5,000€ > Screenshot — http\://localhost:8181/ords/hr/employees/?q={"first\_name":{"$instr":"alex"},"salary":{"$gt":5000}} > > Contains Alex and earn more than 5,000 > **Using complex filtering:** > > The following URL `http://localhost:` 8181`/ords/hr/employees/?q={"job_id":{"$like":"%CLERK"},"salary":{"$gt":2899},"hire_date":{"$gt":{"$date":"2016-12-31T12:59:59Z"}}}` will return every employee who: > > * working as clerk (`job_id` ending by the `CLERK` word), > * having a salary greater than 2,899€ and > * hired as from 1st January 2017. > > > Screenshot — http\://localhost:8181/ords/hr/employees/?q={"job\_id":{"$like":"%CLERK"},"salary":{"$gt":2899},"hire\_date":{"$gt":{"$date":"2016-12-31T12:59:59Z"}}} > > > > Using a combination of filters #### Sorting The general syntax is using `/?q={"$orderby":{"fieldname1":"asc","fieldname2":"desc"}}`. To sort on the first name desc and, then based on the salary (the higher first): `http://localhost:` 8181`/ords/hr/employees/?q={"$orderby":{"first_name":"desc","salary":"desc"}}`. > Screenshot — http\://localhost:8181/ords/hr/employees/?q={"$orderby":{"first\_name":"desc","salary":"desc"}} > > Ordering on the firstname and salary DESC ### Swagger ORDS can generate an `openapi.json` file to use with Swagger. Read more [Manage & Monitor Oracle Database with REST APIs](https://www.thatjeffsmith.com/archive/2022/08/manage-monitor-your-oracle-database-with-rest-apis/) ## Further reading * [ORDS best practices topics (www.oracle.com)](https://www.oracle.com/database/technologies/appdev/rest/best-practices/) * [Installation, Configuration, and Development Guide (docs.oracle.com)](https://docs.oracle.com/en/database/oracle/oracle-rest-data-services/21.4/aelig/toc.htm#GUID-A1CD111F-724B-4E91-8202-FA899EE521F1) * [Jeff Smith blog - Articles about ORDS](https://www.thatjeffsmith.com/archive/tag/ords/) * [Adding HTTP(S) Access Logs for ORDS Standalone](https://www.thatjeffsmith.com/archive/2022/09/adding-https-access-logs-to-ords-standalone/) --- # Accessing an Oracle database using .Net, NodeJS, PHP and Python > Step-by-step guide to accessing an Oracle database container with .Net, Node.js, PHP, and Python. Includes Dockerfile and full code samples for each language. (154 characters) --- ![Accessing an Oracle database using .Net, NodeJS, PHP and Python](/img/v2/oracle.webp) **TL;DR** This step-by-step guide demonstrates how to connect to an Oracle database from .Net, Node.js, PHP, and Python. For each language, you'll find a complete, ready-to-use example that includes the `Dockerfile` for building a container with all necessary dependencies, and a sample script to query the database. This article provides a quickstart template for anyone needing to integrate an Oracle database into their application, regardless of the programming language. In a previous article ([Running Oracle Database Server as a Docker container](/blog/docker-oracle-database-server)), we've created a Docker container called `oracle-db` where a Human Resources database is running. This time, let's play with .Net, NodeJS, PHP and Python to access to it and display records. For each language, we'll build a Docker image with all the required stuff like Oracle Instant Client then we'll create a small script to connect to the `employees` table and display records. The idea is then to provide a very quick skeleton for starting a project in those languages and if you need to directly connect to an Oracle database. *If you'd rather not write a connector at all, [Transform an Oracle DB as OpenData using Oracle REST Data Services](/blog/docker-oracle-ords) exposes the same tables as a REST API that any language can consume with a plain HTTP call.* Here's the result, whichever language you pick below: a small Docker container that connects and prints the employees table. ![Using .Net to access the list of employees](./images/using_dotnet.webp) In the [Running Oracle Database Server as a Docker container](/blog/docker-oracle-database-server) article, we've created a Docker container called `oracle-db` where a Human Resources database is running. Please read that article and follow steps so, before continuing here, you've a running Docker container as described below. ## Some prerequisites **Before you begin make sure...** 1. You've a running Docker container called `oracle-db`, 2. The container is thus running on your `localhost`, 3. The port number to access your database is `1521`, 4. The credentials to use is `SYS` (or `SYSTEM`) and `admin` for the password, 5. The container contains a database called `ORCLPDB1` (also called service name) which is the Human Resources sample of Oracle (refer to the mentioned article) and 6. Our database `oracle-db` is running on a network called `oracle`. ## The code samples were largely created using AI Because it's fast to do and also because I don't know .Net and very little NodeJS, I've used Google Gemini for building my samples below. Sometimes, like for .Net, it was almost ready-to-use. Sometimes for PHP, it was more difficult and it took a lot of fiddling on my part to find the right configuration. Nevertheless the objective of this article was to provide the minimum set of files in order to connect to an Oracle DB, run a SELECT query and display results; not to have a best-in-class codebase. ## Access our Oracle DB container using .Net First language by alphabetical order; let's play with a .Net container: ```bash $ mkdir -p /tmp/oracle/dotnet && cd $_ ``` Also create this directory: ```bash $ mkdir -p /tmp/oracle/dotnet/OracleConnector ``` Let's create a Dockerfile: **Dockerfile** ```docker # cspell:ignore libaio1,instantclient,libclntsh,libocci,ldconfig FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /app COPY OracleConnector/*.csproj ./OracleConnector/ RUN dotnet restore ./OracleConnector/OracleConnector.csproj COPY OracleConnector/. ./OracleConnector/ RUN dotnet publish ./OracleConnector/OracleConnector.csproj -c Release -o out FROM mcr.microsoft.com/dotnet/runtime:8.0 AS final WORKDIR /app COPY --from=build /app/out . # Install necessary Oracle Instant Client libraries ARG ORACLE_INSTANT_CLIENT_VERSION=21.12 ARG ORACLE_DOWNLOAD_URL=https://download.oracle.com/otn_software/linux/instantclient/instantclient-basic-linuxx64.zip RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ unzip \ libaio1 \ && rm -rf /var/lib/apt/lists/* RUN curl -sSL "${ORACLE_DOWNLOAD_URL}" -o instantclient.zip \ && mkdir -p /usr/lib/oracle/instantclient \ && unzip -qq instantclient.zip -d /usr/lib/oracle/instantclient \ && rm instantclient.zip \ && ln -s /usr/lib/oracle/instantclient/libclntsh.so.${ORACLE_INSTANT_CLIENT_VERSION}.1 /usr/lib/oracle/instantclient/libclntsh.so \ && ln -s /usr/lib/oracle/instantclient/libocci.so.${ORACLE_INSTANT_CLIENT_VERSION}.1 /usr/lib/oracle/instantclient/libocci.so \ && echo "/usr/lib/oracle/instantclient" > /etc/ld.so.conf.d/oracle-instantclient.conf \ && ldconfig ENTRYPOINT ["dotnet", "OracleConnector.dll"] ``` Now, the DotNet part. We need two files: `OracleConnector/OracleConnector.csproj` and `OracleConnector/main.cs`. **OracleConnector/OracleConnector.csproj** ```csproj Exe net8.0 enable enable ``` **OracleConnector/main.cs** ```cs // cspell:ignore orclpdb1 using Oracle.ManagedDataAccess.Client; using System; namespace OracleConnector { class Program { static void Main(string[] args) { string dbHost = Environment.GetEnvironmentVariable("ORACLE_HOST") ?? "oracle-db"; string dbPort = Environment.GetEnvironmentVariable("ORACLE_PORT") ?? "1521"; string dbService = Environment.GetEnvironmentVariable("ORACLE_SERVICE") ?? "orclpdb1"; string dbUser = Environment.GetEnvironmentVariable("ORACLE_USER") ?? "system"; string dbPassword = Environment.GetEnvironmentVariable("ORACLE_PASSWORD") ?? "admin"; string connectionString = $"Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST={dbHost})(PORT={dbPort})))(CONNECT_DATA=(SERVICE_NAME={dbService})));User ID={dbUser};Password={dbPassword};"; try { using (OracleConnection connection = new OracleConnection(connectionString)) { connection.Open(); string sql = "SELECT employee_id, first_name, last_name, email FROM employees WHERE ROWNUM <= 25"; using (OracleCommand command = new OracleCommand(sql, connection)) using (OracleDataReader reader = command.ExecuteReader()) { Console.WriteLine("\nEmployee ID | First Name | Last Name | Email"); Console.WriteLine("------------|----------------------|----------------------|-------------------------"); while (reader.Read()) { int employeeId = reader.GetInt32(0); string firstName = reader.GetString(1); string lastName = reader.GetString(2); string email = reader.GetString(3); Console.WriteLine($"{employeeId,-12}| {firstName,-21}| {lastName,-21}| {email}"); } } } } catch (OracleException ex) { Console.WriteLine($"Error connecting to or querying Oracle: {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"An unexpected error occurred: {ex.Message}"); } } } } ``` This is what your project looks like in VSCode: ![.Net project in VSCode](./images/vscode_dotnet.webp) Still in the console, located in folder `/tmp/oracle/dotnet`, create the Docker image and run the container (and thus the script): `clear ; docker build -t oracle-dotnet . && docker run --rm -it --network oracle oracle-dotnet`. Job done — the same result already shown at the top of this article, our .Net code has accessed the list. ## Access our Oracle DB container using NodeJS Second in the alphabetical order, let's play with a NodeJS container: ```bash $ mkdir -p /tmp/oracle/nodejs && cd $_ ``` Let's create a Dockerfile: **Dockerfile** ```docker # cspell:ignore libaio FROM node:20-alpine WORKDIR /app COPY package.json . RUN npm install -g npm@latest RUN npm install RUN apk add --no-cache --update curl unzip libaio COPY main.js . CMD ["npm", "start"] ``` We also need these files: **package.json** ```json { "name": "oracle-connector-nodejs", "version": "1.0.0", "description": "Node.js script to connect to Oracle", "main": "app.js", "scripts": { "start": "node main.js" }, "dependencies": { "oracledb": "^6.3.0" } } ``` **main.js** ```javascript // cspell:ignore orclpdb1, oracledb const oracledb = require("oracledb"); async function run() { let connection; try { const dbHost = process.env.ORACLE_HOST || "oracle-db"; const dbPort = process.env.ORACLE_PORT || "1521"; const dbService = process.env.ORACLE_SERVICE || "orclpdb1"; const dbUser = process.env.ORACLE_USER || "SYSTEM"; const dbPassword = process.env.ORACLE_PASSWORD || "admin"; const connectConfig = { user: dbUser, password: dbPassword, connectString: `${dbHost}:${dbPort}/${dbService}`, }; connection = await oracledb.getConnection(connectConfig); const result = await connection.execute( `SELECT employee_id, first_name, last_name, email FROM employees WHERE ROWNUM <= 25`, // Assuming your table is named 'EMPLOYEES' ); console.log( "\nEmployee ID | First Name | Last Name | Email", ); console.log( "------------|----------------------|-----------------------|-------------------------", ); for (const row of result.rows) { const employeeId = row[0]; const firstName = row[1]; const lastName = row[2]; const email = row[3]; console.log( `${employeeId.toString().padEnd(12)}| ${firstName.padEnd(21)}| ${lastName.padEnd(21)}| ${email}`, ); } } catch (err) { console.error("Error connecting to or querying Oracle:", err); } finally { if (connection) { try { await connection.close(); } catch (err) { console.error("Error closing connection:", err); } } } } run(); ``` Still in the console, located in folder `/tmp/oracle/nodejs`, create the Docker image and run the container (and thus the script): `clear ; docker build -t oracle-nodejs . && docker run --rm -it --network oracle oracle-nodejs`. ![Using NodeJS to access the list of employees](./images/using_nodejs.webp) Nice! As you can see, we can quite easily access our Oracle database using NodeJS. ## Access our Oracle DB container using PHP And, last in the alphabetical order; let's finish with a PHP container: ```bash $ mkdir -p /tmp/oracle/php && cd $_ ``` And there, let's create a Dockerfile: **Dockerfile** ```docker # cspell:ignore instantclient,libaio1,libncurses5,libreadline8,ldconfig,pecl FROM php:fpm RUN apt-get update && apt-get install -y --no-install-recommends \ libaio1 \ libncurses5 \ libreadline8 \ locales \ unzip \ && rm -rf /var/lib/apt/lists/* RUN locale-gen en_US.UTF-8 ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV LC_ALL=en_US.UTF-8 ARG ORACLE_INSTANT_CLIENT_VERSION=21.12 # Downloading Oracle Instant Client RUN curl -o /tmp/instantclient-basic.zip -L "https://download.oracle.com/otn_software/linux/instantclient/instantclient-basic-linuxx64.zip" \ && curl -o /tmp/instantclient-sdk.zip -L "https://download.oracle.com/otn_software/linux/instantclient/instantclient-sdk-linuxx64.zip" \ && mkdir -p /usr/lib/oracle \ && unzip -o /tmp/instantclient-basic.zip -d /usr/lib/oracle \ && unzip -o /tmp/instantclient-sdk.zip -d /usr/lib/oracle \ && rm /tmp/*.zip \ && ln -s /usr/lib/oracle/instantclient_* /usr/lib/oracle/instantclient \ && sh -c "echo /usr/lib/oracle/instantclient > /etc/ld.so.conf.d/oracle-instantclient.conf" \ && ldconfig # Install OCI8 Extension RUN echo 'instantclient,/usr/lib/oracle/instantclient' | pecl install oci8 \ && docker-php-ext-enable oci8 WORKDIR /app COPY main.php . CMD [ "php", "main.php" ] ``` Then let's create a PHP script (*generated using AI*) **main.php** ```php getMessage() . "\n"; } finally { if (is_resource($statement)) { oci_free_statement($statement); } if (is_resource($connection)) { oci_close($connection); } } ?> ``` Still in the console, located in folder `/tmp/oracle/php`, create the Docker image and run the container (and thus the script): `clear ; docker build -t oracle-php . && docker run --rm -it --network oracle oracle-php`. ![Using PHP to access the list of employees](./images/using_php.webp) ## Access our Oracle DB container using Python Let's create a Python script now: ```bash $ mkdir -p /tmp/oracle/python && cd $_ ``` And its Dockerfile; the only thing we need (except Python) is to install the `oracledb` dependency. Quite straightforward. **Dockerfile** ```docker # cspell:ignore oracledb FROM python:slim WORKDIR /app RUN pip install --no-cache-dir oracledb COPY main.py main.py CMD ["python", "main.py"] ``` Then let's create a Python script (*generated using AI*) **main.py** ```python from typing import Optional, Tuple, List import oracledb # cspell:ignore oracledb,orclpdb1,sysdba,rownum DB_USER: str = "SYS" DB_PASSWORD: str = "admin" DB_HOST: str = "oracle-db" DB_PORT: int = 1521 DB_SERVICE_NAME: str = "orclpdb1" connection: Optional[oracledb.Connection] = None cursor: Optional[oracledb.Cursor] = None try: dsn: str = f"{DB_HOST}:{DB_PORT}/{DB_SERVICE_NAME}" connection = oracledb.connect( user=DB_USER, password=DB_PASSWORD, dsn=dsn, mode=oracledb.AuthMode.SYSDBA, ) cursor = connection.cursor() sql: str = ( "SELECT employee_id, first_name, last_name, email " "FROM system.employees " "WHERE ROWNUM <= :max_rows" ) num_rows: int = 25 cursor.execute(sql, max_rows=num_rows) records: List[Tuple[int, str, str, str]] = cursor.fetchall() print("Employee ID | First Name | Last Name | Email") print("-" * 80) employee_id: int first_name: str last_name: str email: str for record in records: employee_id, first_name, last_name, email = record print(f"{employee_id:<12}| {first_name:<20}| {last_name:<20}| {email:<26}") except oracledb.Error as exception: print(f"Error connecting to Oracle: {exception}") finally: if cursor: try: cursor.close() except oracledb.Error as exception: print(f"Error closing cursor: {exception}") if connection: try: connection.close() except oracledb.Error as exception: print(f"Error closing connection: {exception}") ``` Still in the console, located in folder `/tmp/oracle/python`, create the Docker image and run the container (and thus the script): `clear ; docker build -t oracle-python . && docker run --rm -it --network oracle oracle-python`. ![Using Python to access the list of employees](./images/using_python.webp) Nice! As you can see, we can quite easily access our Oracle database using Python. ## Conclusion You know what? I've more than 15 years of experience with PHP and just 6 months using Python and, if we look at the Dockerfile of both languages, there is no doubt at all; the one for Python is so much easier to read and to configure. I had a hard time getting the PHP one to work, and none at all for Python since it's just one dependency to install and that's all. And if we look at PHP versus Python code, they are quite similar but, I should admit, the one for Python has my preference because we don't need to check for errors in multiple places, we can more easily define the data types of variables, the echo statement is more readable, ... ---