1. Introduction
In this article, we'll cover the basics of database schemas, why we need them, and how they are useful. After that, we'll focus on practical examples of setting schema in JDBC with PostgreSQL as a database.
2. What is a Database Schema
In general, a database schema is a set of rules that regulate a database. It is an additional layer of abstraction around a database. There are two kinds of schemas:
- Logical database schema defines rules that apply to the data stored in a database.
- Physical database schema defines rules on how data is physically stored on a storage system.
In PostgreSQL, schema refers to the first kind. Schema is a logical namespace that contains database objects such as tables, views, indexes, etc. Each schema belongs to one database, and each database has at least one schema. If not specified otherwise, the default schema in PostgreSQL is public. Every database object we create, without specifying the schema, belongs to the public schema.
A schema in PostgreSQL allows us to organize tables and views into groups and make them more manageable. This way, we can set up privileges on our database objects on a more granular level. Also, schemas allow us to have multiple users using the same databases simultaneously without interfering.
3. How to Use Schema With PostgreSQL
To access an object of a database schema, we must specify the schema's name before the name of a given database object that we want to use. For example, to query table product within schema store, we need to use the qualified name of the table:
SELECT * FROM store.product;
The recommendation is to avoid hardcoding schema names to prevent coupling concrete schema to our application. This means that we use database object names directly and let the database system determine which schema to use. PostgreSQL determines where to search for a given table by following a search path.
3.1. PostgreSQL search_path
The search path is an ordered list of schemas that define the database system's search for a given database object. If the object is present in any (or multiple) schemas we get the first found occurrence. Otherwise, we get an error. The first schema in the search path is also called the current schema. To preview which schemas are on the search path, we can use the query:
SHOW search_path;
Default PostgreSQL configuration will return $user and public schemas. The public schema we already mentioned, the $user schema, is a schema named after the current user, and it might not exist. In that case, the database ignores that schema.
To add store schema to the search path, we can execute the query:
SET search_path TO store,public;
After this, we can query the product table without specifying the schema. Also, we could remove the public schema from the search path.
Setting the search path as we described above is a configuration on the ROLE level. We can change the search path on the whole database by changing the postgresql.conf file and reloading database instance.
3.2. JDBC URL
We can use JDBC URL to specify all kinds of parameters during connection setup. The usual parameters are database type, address, port, database name, etc. Since Postgres version 9.4. there is added support for specifying the current schema using URL.
Before we bring this concept to practice, let's set up a testing environment. For this, we'll use the testcontainers library and create the following test setup:
@ClassRule
public static PostgresqlTestContainer container = PostgresqlTestContainer.getInstance();
@BeforeClass
public static void setup() throws Exception {
Properties properties = new Properties();
properties.setProperty("user", container.getUsername());
properties.setProperty("password", container.getPassword());
Connection connection = DriverManager.getConnection(container.getJdbcUrl(), properties);
connection.createStatement().execute("CREATE SCHEMA store");
connection.createStatement().execute("CREATE TABLE store.product(id SERIAL PRIMARY KEY, name VARCHAR(20))");
connection.createStatement().execute("INSERT INTO store.product VALUES(1, 'test product')");
}
With @ClassRule, we create an instance of PostgreSQL database container. Next, in the setup method, create a connection to that database and create the required objects.
Now when the database is set, let's connect to the store schema using JDBC URL:
@Test
public void settingUpSchemaUsingJdbcURL() throws Exception {
Properties properties = new Properties();
properties.setProperty("user", container.getUsername());
properties.setProperty("password", container.getPassword());
Connection connection = DriverManager.getConnection(container.getJdbcUrl().concat("¤tSchema=store"), properties);
ResultSet resultSet = connection.createStatement().executeQuery("SELECT * FROM product");
resultSet.next();
assertThat(resultSet.getInt(1), equalTo(1));
assertThat(resultSet.getString(2), equalTo("test product"));
}
To change the default schema, we need to specify the currentSchema parameter. If we enter a non-existing schema, PSQLException is thrown during a select query, saying the database object is missing.
3.3. PGSimpleDataSource
To connect to a database, we can use javax.sql.DataSource implementation from PostgreSQL driver library named PGSimpleDataSource. This concrete implementation has support for setting up a schema:
@Test
public void settingUpSchemaUsingPGSimpleDataSource() throws Exception {
int port = //extracting port from container.getJdbcUrl()
PGSimpleDataSource ds = new PGSimpleDataSource();
ds.setServerNames(new String[]{container.getHost()});
ds.setPortNumbers(new int[]{port});
ds.setUser(container.getUsername());
ds.setPassword(container.getPassword());
ds.setDatabaseName("test");
ds.setCurrentSchema("store");
ResultSet resultSet = ds.getConnection().createStatement().executeQuery("SELECT * FROM product");
resultSet.next();
assertThat(resultSet.getInt(1), equalTo(1));
assertThat(resultSet.getString(2), equalTo("test product"));
}
While using PGSimpleDataSource, the driver uses public schema as a default if we don't set schema.
3.4. @Table annotation from javax.persistence package
If we use JPA in our project, we can specify schema on entity level using @Table annotation. This annotation can hold value for schema or defaults to empty a String. Let's map our product table to the Product entity:
@Entity
@Table(name = "product", schema = "store")
public class Product {
@Id
private int id;
private String name;
// getters and setters
}
To verify this behavior, we set up the EntityManager instance to query the product table:
@Test
public void settingUpSchemaUsingTableAnnotation(){
Map<String,String> props = new HashMap<>();
props.put("hibernate.connection.url", container.getJdbcUrl());
props.put("hibernate.connection.user", container.getUsername());
props.put("hibernate.connection.password", container.getPassword());
EntityManagerFactory emf = Persistence.createEntityManagerFactory("postgresql_schema_unit", props);
EntityManager entityManager = emf.createEntityManager();
Product product = entityManager.find(Product.class, 1);
assertThat(product.getName(), equalTo("test product"));
}
As we previously mentioned in section 3, it's best to avoid coupling schema to the code for various reasons. Because of that, this feature is often overlooked, but it can be advantageous when accessing multiple schemas.
4. Conclusion
In this tutorial, first, we covered basic theory about database schemas. After that, we described multiple ways of setting database schema using different approaches and technologies. As usual, all the code samples are available over on GitHub.