Skip to content
Daniel Zou edited this page Sep 15, 2020 · 1 revision

Cloud Spanner Hibernate Best Practices

This guide contains a variety of best practices for using Hibernate with Spanner which can significantly improve the performance of your application.

Schema Creation and Entity Design

Hibernate generates statements based on your Hibernate entity’s design. Following these practices can result in better DDL and DML statement generation which can improve performance.

Use Generated UUIDs for ID Generation

The Universally Unique Identifier (UUID) is the preferred ID type in Cloud Spanner because it avoids hotspots as the system divides data among servers by key ranges. A monotonically increasing integer key would also work, but can be less performant.

For this reason, if you are using generated IDs in your code, you should configure UUID generation like this:

@Entity
public class Employee {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Type(type="uuid-char")
  public UUID id;
}

The @Type(type="uuid-char") annotation specifies that this UUID value will be stored in Cloud Spanner as a STRING column. Leaving out this annotation causes a BYTES column to be used.

Hibernate’s @GeneratedValue annotation for numeric fields is supported but not recommended:

@Entity
public class Employee {

  @Id
  @GeneratedValue   // Not Recommended.
  public Long id;
}

This results in sequential IDs that are not optimal for Cloud Spanner and makes use of the hibernate_sequence table for generating IDs.

Generate Schema for faster development

It is often useful to generate the schema for your database, such as during the early stages of development. The Spanner dialect supports Hibernate’s hibernate.hbm2ddl.auto setting which controls the framework’s schema generation behavior on start-up.

The following settings are available:

  • none: Do nothing.

  • validate: Validate the schema, makes no changes to the database.

  • update: Create or update the schema.

  • create: Create the schema, destroying previous data.

  • create-drop: Drop the schema when the SessionFactory is closed explicitly, typically when the application is stopped.

Hibernate performs schema updates on each table and entity type on startup, which can take more than several minutes if there are many tables. To avoid schema updates keeping Hibernate from starting for several minutes, you can update schemas separately and use the none or validate settings.

Use Constraints Generation to leverage Spanner foreign keys

The dialect supports all of the standard entity relationships:

  • @OneToOne

  • @OneToMany

  • @ManyToOne

  • @ManyToMany

These can be used via @JoinTable or @JoinColumn.

The Cloud Spanner Hibernate dialect will generate the correct foreign key DDL statements during schema generation for entities using these annotations. However, Cloud Spanner currently does not support cascading deletes on foreign keys, therefore database-side cascading deletes are not supported via the @OnDelete(action = OnDeleteAction.CASCADE).

The dialect also supports unique column constraints applied through @Column(unique = true) or @UniqueConstraint. In these cases, the dialect will create a unique index to enforce uniqueness on the specified columns.

Use Interleaved Tables for Parent-Child entities for better performance

Cloud Spanner offers the concept of Interleaved Tables which allows you to co-locate the rows of an interleaved table with rows of a parent table for efficient retrieval. This feature enforces the one-to-many relationship and provides efficient queries and operations on entities of a single domain parent entity.

If you would like to generate interleaved tables in Cloud Spanner, you must annotate your entity with the @Interleaved annotation. The primary key of the interleaved table must also include at least all of the primary key attributes of the parent. This is typically done using the @IdClass or @EmbeddedId annotation.

The Hibernate Basic Sample contains an example of using @Interleaved for the Singer and Album entities. The code excerpt of the Album entity below demonstrates how to declare an interleaved entity in the Singer table.

@Entity
@Interleaved(parentEntity = Singer.class, cascadeDelete = true)
@IdClass(AlbumId.class)
public class Album {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Type(type = "uuid-char")
  private UUID albumId;

  @Id
  @ManyToOne
  @JoinColumn(name = "singerId")
  @Type(type = "uuid-char")
  private Singer singer;

  // Constructors, getters/setters

  public static class AlbumId implements Serializable {

    // The primary key columns of the parent entity
    // must be declared first.
    Singer singer;

    @Type(type = "uuid-char")
    UUID albumId;

    // Getters and setters
  }
}

Performance Optimizations

There are some practices which can improve the execution time of your statements as well.

Be Clear about Inserts or Updates

Sometimes you may generate IDs for their entities in your code instead of relying on the @GeneratedValue annotation.

DbEntity entity = new DbEntity();
entity.id = UUID.randomUUID();
session.saveOrUpdate(entity);

This practice may result in Hibernate executing additional SELECT statements that you may not have expected because it will perform a SELECT on the table to determine if a record in the table already has the entity’s ID. If so, it will perform an update, otherwise it will save a new entity.

These extra SELECT statements could yield large performance issues because they may lock an entire table depending on how they are generated.

In these cases, we recommend to either:

  • Let Hibernate generate the ID by leaving the entity’s id null and annotate the field with @GeneratedValue. Hibernate will know that the record did not exist prior if it generates a new ID.

  • Or use session.persist() which explicitly will simply attempt the insert without generating an ID.

Enable Hibernate Batching

Batching SQL statements together allows you to optimize the performance of your application by including a group of SQL statements in a single remote call. This allows you to reduce the number of round-trips between your application and Cloud Spanner.

By default, Hibernate does not batch the statements that it sends to the Cloud Spanner JDBC driver.

Batching can be enabled by configuring hibernate.jdbc.batch_size in your Hibernate configuration file:

<property name="hibernate.jdbc.batch_size">100</property>

The property is set to 100 as an example; you may experiment with the batch size to see what works best for your application.

Query Optimization

The Cloud Spanner SQL syntax offers a variety of query hints to tune and optimize the performance of queries. If you find that you need to take advantage of this feature, you can achieve this in Hibernate using native SQL queries.

This is an example of using the @{FORCE_JOIN_ORDER=TRUE} hint in a native Spanner SQL query.

SQLQuery query = session.createSQLQuery("SELECT * FROM Singers AS s
                                         JOIN@{FORCE_JOIN_ORDER=TRUE} Albums AS a
                                         ON s.SingerId = a.Singerid
                                         WHERE s.LastName LIKE '%x%'
                                         AND a.AlbumTitle LIKE '%love%';");

// Executes the query.
List<Object[]> entities = query.list();

Also, you may consult the Cloud Spanner documentation on general recommendations for optimizing performance.