1. Overview

A database view is a table-like structure in a relational database system in which the data source is from one or more tables joined together.

While Spring Data repositories are commonly used for database tables, they can be effectively applied to database views as well. In this tutorial, we’ll explore adopting Spring Data repositories for database views.

2. Database Table Setup

In this tutorial, we’ll adopt the H2 database system for data definition and demonstrate the database view concept using two example tables — SHOP and SHOP_TRANSACTION.

The SHOP table stores the shop information:

CREATE TABLE SHOP
(
    shop_id             int             AUTO_INCREMENT,
    shop_location       varchar(100)    NOT NULL UNIQUE,
    PRIMARY KEY(shop_id)
);

And the SHOP_TRANSACTION table stores transaction records associated with shops and references to the SHOP table via the shop_id:

CREATE TABLE SHOP_TRANSACTION
(
    transaction_id      bigint          AUTO_INCREMENT,
    transaction_date    date            NOT NULL,
    shop_id             int             NOT NULL,
    amount              decimal(8,2)    NOT NULL,
    PRIMARY KEY(transaction_id),
    FOREIGN KEY(shop_id) REFERENCES SHOP(shop_id)
);

In the Entity-Relationship (ER) model, we can illustrate it as a one-to-many relationship where one shop can have multiple transactions. Still, each transaction is associated with one shop only. We can represent this visually using an ER diagram:

Entity-Relationship (ER) model

3. Database View

A database view provides a virtual table that gathers data from the result of a predefined query. There are advantages to using a database view instead of using a join query:

  • Simplicity – Views encapsulate complex joins, eliminating the need to rewrite the same join query repeatedly
  • Security – Views may only include a subset of data from the base tables, reducing the risk of exposing sensitive information from base tables
  • Maintainability – Updating view definitions when the base table structure changes prevents the need to modify queries referencing the altered base table in our application

3.1. Standard View and Materialized View

There are two common types of database views, and they serve different purposes:

  • Standard Views – These are generated by executing a predefined SQL query when queried. They do not store data themselves. All data is stored in the underlying base tables.
  • Materialized Views – These are similar to standard views, which are also generated from a predefined SQL query. In contrast, they copy the query result to a physical table in the database. Subsequent queries retrieve data from this table rather than generating it dynamically.

The following comparison table highlights the varying characteristics of standard and materialized views, aiding in selecting the appropriate view type based on specific requirements:

Standard View

Materialized View

Data Source

Dynamically generated from based tables via the predefined query

A physical table containing data from the predefined query

Performance

Slower due to dynamic query generation

Faster due to data retrieval from a physical table

Staleness

Always return fresh data

May become stale and require periodic refresh

Use Case

Suitable for real-time data

Suitable for computationally expensive queries, when data freshness is not critical

3.2. Standard View Example

In our example, we would like to define a view that concludes the total sales amount of shops for each calendar month. The materialized view proves suitable since past sales amounts from previous months remain unchanged. Real-time data is unnecessary for calculating total sales unless the current month’s data is required.

However, the H2 database does not support materialized views. We’ll create a standard view instead:

CREATE VIEW SHOP_SALE_VIEW AS
SELECT ROW_NUMBER() OVER () AS id, shop_id, shop_location, transaction_year, transaction_month, SUM(amount) AS total_amount
FROM (
    SELECT 
        shop.shop_id, shop.shop_location, trans.amount, 
        YEAR(transaction_date) AS transaction_year, MONTH(transaction_date) AS transaction_month
    FROM SHOP shop, SHOP_TRANSACTION trans
    WHERE shop.shop_id = trans.shop_id
) SHOP_MONTH_TRANSACTION
GROUP BY shop_id, transaction_year, transaction_month;

Upon querying the view, we should obtain data like the following:

id

shop_id

shop_location

transaction_year

transaction_month

amount

1

1

Ealing

2024

1

10.78

2

1

Ealing

2024

2

13.58

3

1

Ealing

2024

3

14.48

4

2

Richmond

2024

1

17.98

5

2

Richmond

2024

2

8.49

6

2

Richmond

2024

3

13.78

4. Entity Bean Definition

We can now define the entity bean for our database view SHOP_SALE_VIEW. Indeed, the definition is almost the same as defining an entity bean for a normal database table.

In JPA, an entity bean has a requirement that it must have the primary key. There are two strategies that we can consider to define a primary key in a database view.

4.1. Physical Primary Key

In most scenarios, we can pick one or multiple columns in the view to identify the uniqueness of a row in the database view. In our scenario, the shop ID, year, and month can uniquely identify each row in the view.

Hence, we can derive the composite primary key by columns shop_id, transaction_year, and transaction_month. In JPA, we have to first define a separate class to represent the composite primary key:

public class ShopSaleCompositeId {
    private int shopId;
    private int year;
    private int month;
    // constructors, getters, setters
}

Subsequently, we embed this composite ID class into the entity class with @EmbeddedId and define the column mappings by annotating the composite ID by @AttributeOverrides:

@Entity
@Table(name = "SHOP_SALE_VIEW")
public class ShopSale {
    @EmbeddedId
    @AttributeOverrides({
      @AttributeOverride( name = "shopId", column = @Column(name = "shop_id")),
      @AttributeOverride( name = "year", column = @Column(name = "transaction_year")),
      @AttributeOverride( name = "month", column = @Column(name = "transaction_month"))
    })
    private ShopSaleCompositeId id;

    @Column(name = "shop_location", length = 100)
    private String shopLocation;

    @Column(name = "total_amount")
    private BigDecimal totalAmount;

    // constructor, getters and setters
}

4.2. Virtual Primary Key

In certain scenarios, defining a physical primary key is not feasible due to the absence of column combinations that can ensure the uniqueness of each row within the database view. We can generate a virtual primary key to emulate row uniqueness as a workaround.

In our database view definition, we have an additional column id that utilized ROW_NUMBER() OVER () to generate row numbers as identifiers. This is the entity class definition when we adopt a virtual primary key strategy:

@Entity
@Table(name = "SHOP_SALE_VIEW")
public class ShopSale {
    @Id
    @Column(name = "id")
    private Long id;

    @Column(name = "shop_id")
    private int shopId;

    @Column(name = "shop_location", length = 100)
    private String shopLocation;

    @Column(name = "transaction_year")
    private int year;

    @Column(name = "transaction_month")
    private int month;

    @Column(name = "total_amount")
    private BigDecimal totalAmount;

    // constructors, getters and setters
}

It’s crucial to note that these identifiers are specific to the current result set. The row numbers assigned to each row could be different upon re-query. As a result, the same row number in subsequent queries may represent different rows in the database view.

5. View Repository

Depending on the database, systems such as Oracle may support updatable views that allow data updates on them under some conditions. However, database views are mostly read-only.

For read-only database views, it’s unnecessary to expose data modifying methods such as save() or delete() in our repositories. Attempting to call these methods will throw an exception since the database system doesn’t support such operations:

org.springframework.orm.jpa.JpaSystemException: could not execute statement [Feature not supported: "TableView.addRow"; SQL statement:
insert into shop_sale_view (transaction_month,shop_id,shop_location,total_amount,transaction_year,id) values (?,?,?,?,?,?) [50100-224]] [insert into shop_sale_view (transaction_month,shop_id,shop_location,total_amount,transaction_year,id) values (?,?,?,?,?,?)]

In such rationale, we’ll exclude these methods and expose only data retrieval methods when defining our Spring Data JPA Repository.

5.1. Physical Primary Key

For views with a physical primary key, we can define a new base repository interface that only exposes data retrieval methods:

@NoRepositoryBean
public interface ViewRepository<T, K> extends Repository<T, K> {
    long count();

    boolean existsById(K id);

    List<T> findAll();

    List<T> findAllById(Iterable<K> ids);

    Optional<T> findById(K id);
}

The @NoRepositoryBean annotation indicates this interface is a base repository interface and instructs Spring Data JPA not to create an instance of this interface at runtime. In this repository interface, we include all data retrieval methods from ListCrudRepository and exclude all data-changing methods.

For our entity bean with composite ID, we extend ViewRepository and define an additional method for querying the shop sale for the shopId:

public interface ShopSaleRepository extends ViewRepository<ShopSale, ShopSaleCompositeId> {
    List<ShopSale> findByIdShopId(Integer shopId);
}

We’ve defined the query method as findByIdShopId() instead of findByShopId() because it derives from the property id.shopId in the ShopSale entity class.

5.2. Virtual Primary Key

Our approach has a slight difference when we’re dealing with the repository design for database views with a virtual primary key since the virtual primary key is an artificial one that cannot truly identify the uniqueness of data rows.

Due to this nature, we’ll define another base repository interface that excludes the query methods by primary key as well. It’s because we’re using a virtual primary key, and it makes no sense for us to retrieve data using a fake primary key:

public interface ViewNoIdRepository<T, K> extends Repository<T, K> {
    long count();

    List<T> findAll();
}

Subsequently, let’s define our repository by extending it to ViewNoIdRepository:

public interface ShopSaleRepository extends ViewNoIdRepository<ShopSale, Long> {
    List<ShopSale> findByShopId(Integer shopId);
}

Since the ShopSale entity class defines the shopId directly this time, we can use findByShopId() in our repository.

6. Conclusion

This article has provided an introduction to database views, offering a brief comparison between standard views and materialized views.

Furthermore, we’ve described applying different primary key strategies on database views depending on the nature of the data. Finally, we explored the definition of an entity bean and base Repository interfaces based on the key strategies we had chosen.

As usual, the examples discussed are available over on GitHub.