Hibernate - Using @Struct Annotation Examples

This tutorial shows you how to use the @Struct annotation in Hibernate for mapping struct or composite types.

Beside basic column types, some databases allow us to define struct or composite types. It allows a column to have a more complex data type with several fields that comply with a defined structure. That kind of data type is stricter than JSON and usually you need to define it in the database. If you use Hibernate version 6.2 or above, there is an easy way to tell Hibernate which struct type should be used for a field. You can do it by using the @Struct annotation

Using @Struct Annotation

For example, we want to define a struct or composite type named location_coordinate. The type has three attributes whose names in order are altitude, latitude, and longitude. Below are the queries for creating the type and a table that has a column using the type. I'm going to use PostgreSQL database for this tutorial. You can use other databases as long as it's supported by Hibernate.

  CREATE TYPE location_coordinate AS (
      altitude    int,
      latitude    double precision,
      longitude   double precision
  );
  
  CREATE TABLE locations (
      id          uuid        NOT NULL,
      address     text        NOT NULL,
      coordinate  location_coordinate    NOT NULL,
      CONSTRAINT "pk_locations" PRIMARY KEY (id)
  );

Here is the Java class that represents the type.

  @Embeddable
  public class Coordinate {
  
    private Integer altitude;
  
    private Double longitude;
  
    private Double latitude;

    // Consturctor, getters, and setters
  }

Besides using a class, it's also possible to use a record instead.

Just like using other column types, the column has to be defined as a field in the entity class. However, if you only do that, Hibernate won't be able to know that it's a column that uses a struct type. As a solution, annotate the field with the @Struct annotation.

The annotation can be put on types (classes), fields, and methods. It requires you to pass the struct name. Optionally, you can also define the order of the attributes if necessary.

Set Struct Name

When adding this annotation to a field, it's required to set the value of the name parameter. It has to match the name of the struct or composite type defined in the database. Below is an example that uses the location_coordinate type. You also need to ensure that the class or record used as the field type has the same attribute names as the actual type's attribute defined on the database. Not only the names, the types of each attribute must be compatible as well.

  @Entity
  @Table(name = "locations")
  public class Location {
  
    @Id
    @UuidGenerator
    private UUID id;
  
    private String address;
  
    @Struct(name = "location_coordinate")
    private Coordinate coordinate;

    // Constructor, getters, and setters
  }

Next, let's test it by running a code for inserting a new row to the table.

  @Transactional
  public void create(CreateLocationRequestDto requestDto) {
    Coordinate coordinate = Coordinate.builder()
        .altitude(requestDto.getAltitude())
        .latitude(requestDto.getLatitude())
        .longitude(requestDto.getLongitude())
        .build();

    Location location = Location.builder()
        .address(requestDto.getAddress())
        .coordinate(coordinate)
        .coordinateV2(coordinate)
        .build();

    this.entityManager.persist(location);
  }

Hibernate logs:

  insert into locations (address,coordinate,id) values (?,?,?)
  binding parameter [1] as [VARCHAR] - [My address]
  binding parameter [2] as [STRUCT] - [com.woolha.hibernate.model.Coordinate@4ef09166]
  binding parameter [3] as [UUID] - [78a8ac10-893b-4455-a8b2-02f0ab9be2a4]

PostgreSQL logs:

  insert into locations (address,coordinate,id) values ($1,$2,$3)
  parameters: $1 = 'My address', $2 = '(10,-0.7893,113.9213)', $3 = '78a8ac10-893b-4455-a8b2-02f0ab9be2a4'

Below is another example showing that you can use the attribute of the struct type as a query criteria.

  public void getSouthernHemisphereLocations() {
    String sql = "SELECT l FROM Location l WHERE l.coordinate.latitude < 0";
    List<Location> locations = this.entityManager.createQuery(sql)
        .getResultList();

    for (Location location : locations) {
      System.out.println(location.getAddress());
    }
  }

Hibernate logs:

  select l1_0.id,l1_0.address,(l1_0.coordinate).altitude,(l1_0.coordinate).latitude,(l1_0.coordinate).longitude from locations l1_0 where (l1_0.coordinate).latitude<0

PostgreSQL logs:

  select l1_0.id,l1_0.address,(l1_0.coordinate).altitude,(l1_0.coordinate).latitude,(l1_0.coordinate).longitude from locations l1_0 where (l1_0.coordinate).latitude<0

Set Attributes Order

Another thing that you need to know is regarding the attributes order. In the database, the value of a column with struct or composite type is stored as a list of values. Therefore, Hibernate needs to know the order of the attributes.

The default order depends on the embeddable. If you use a class, the default order is the fields in alphabetical order. If you use a record, the default order depends on the record definition. In the example above, the attributes of the composite type are in alphabetical order. As a result, we could get the expected behavior.

In case you need to use a different order but don't want to modify the class or record, the solution is to set the attributes value of the annotation.

For example, we have another type called location_coordinate_v2 which is similar to location_coordinate, but the altitude attribute is put as the last one.

  CREATE TYPE location_coordinate_v2 AS (
      latitude    double precision,
      longitude   double precision,
      altitude    int
  );
  
  ALTER TABLE locations ADD COLUMN coordinate_v2 location_coordinate_v2;

Below is an example that passes the attributes value to the annotation.

  public class Location {
  
    @Column(name = "coordinate_v2")
    @Struct(name = "location_coordinate_v2", attributes = {"latitude", "longitude", "altitude"})
    private Coordinate coordinateV2;
  
    // Constructor, getters, setters, and other fields
  }

Summary

Hibernate's @Struct annotation can be used to easily map a field to a struct or composite type in the database. You only need to annotate the field by passing the database's type name as the value of the name parameter. The order of the attributes matters. You may need to pass the value of the attributes parameter for defining the order.

You can also read about: