More information on how to use Google Cloud Spanner with JPA/Hibernate can be found here: http://www.googlecloudspanner.com.
This post is a short description of a real-world example of an application using JPA/Hibernate and Spring Boot in combination with Google Cloud Spanner. As it is based on a real application, its code base is large and somewhat complex. If you are looking for a simple tutorial on how to set up Google Cloud Spanner with JPA/Hibernate, then have a look here: https://olavloite.github.io/2017/03/11/Google-Cloud-Spanner-with-Spring-Boot-JPA-and-Hibernate.html
Introduction
The combination of the two above projects show a real world example of how an application can be developed using JPA/Hibernate in combination with Google Cloud Spanner.
The project depends on two important libraries:
Google Cloud Spanner JDBC Driver
An open source JDBC Driver for Google Cloud Spanner that supports a number of features not supported by the official JDBC Driver from Google:
-
DML-statements (UPDATE, INSERT, DELETE)
-
DDL-statements (CREATE TABLE, ALTER TABLE, …)
-
Transactions
More information on this driver can be found here: https://github.com/olavloite/spanner-jdbc
Google Cloud Spanner Dialect
A Hibernate Dialect for Google Cloud Spanner. More information on this dialect can be found here: https://github.com/olavloite/spanner-hibernate
The dialect supports schema generation.
Example Project
The example project is a web application that reads GitHub Repositories from GitHub and stores these in a Google Cloud Spanner database. The main goal of this project is to show how you could develop a real world application with Google Cloud Spanner / JPA / Hibernate. The functionality of the example project itself is secondary.
Base Entity
The project uses a general application framework that has been used to develop other applications using PostgreSQL. The base of the framework has only been altered slightly in order to get it compatible with Cloud Spanner. The most important change is the way database id’s are generated. PostgreSQL supports sequences, Cloud Spanner does not. Id’s are therefore generated by the application framework instead of by the database.
The base entity is shown in the below code snippet (some methods have been removed for brevity)
@MappedSuperclass @Cacheable(value = true) @BatchSize(size = 20) public abstract class FriggEntity implements Serializable { private static final long serialVersionUID = 1L; private static final NoArgGenerator GENERATOR = Generators.randomBasedGenerator(); /** * ID is generated from the UUID */ @Id @FriggDomainDescriptor(excludedFromDataPanel = true) private Long id; @Column(nullable = false, unique = true, length = 20) @FriggDomainDescriptor(excludedFromDataPanel = true) private UUID uuid; @FriggDomainDescriptor(defaultVisibleInDataPanel = false, defaultVisibleInAutoForm = false) @Column(nullable = false) private Date created; @FriggDomainDescriptor(defaultVisibleInDataPanel = false, defaultVisibleInAutoForm = false) @Column(nullable = false) private Date updated; protected FriggEntity() { } public UUID getUuid() { if (uuid == null) uuid = GENERATOR.generate(); return uuid; } public void setUuid(UUID uuid) { this.uuid = uuid; } @PrePersist protected void onCreate() { created = new Date(); updated = new Date(); if (uuid == null) uuid = GENERATOR.generate(); if (id == null) id = uuid.getMostSignificantBits() ^ uuid.getLeastSignificantBits(); } @PreUpdate protected void onUpdate() { updated = new Date(); } @Override public boolean equals(Object other) { if (this == other) return true; if (!(other instanceof FriggEntity)) return false; return ((FriggEntity) other).getUuid().equals(this.getUuid()); } @Override public int hashCode() { return getUuid().hashCode(); } @Override public boolean isSaved() { return getId() != null; } }
-
The primary key of the base entity is a long without any functional meaning.
-
The entity contains a UUID that is generated by the application. This makes it possible to compare entities with each other that have not yet been saved. This UUID is also used to generate the primary key value for the entity.
-
The entity has created/updated timestamps that are automatically filled.
-
The entity uses the @PrePersist and @PreUpdate annotations of JPA to generate values for id, uuid, created and updated.
Account Entity
The Account entity is an extension of the base entity that represents a user account. Once again, large parts of the code has been left out for brevity.
@Entity @Table(indexes = { @Index(unique = true, columnList = "username") }) public class Account extends FriggEntity { private static final long serialVersionUID = 1L; @Column(length = 30, nullable = false) private String username; @FriggDomainDescriptor(header = "Password", description = "Password", passwordField = true) @Transient private String password; @FriggDomainDescriptor(excludedFromDataPanel = true, defaultVisibleInAutoForm = false) @Column(length = 200, nullable = true) private String passwordHash; @FriggDomainDescriptor(excludedFromDataPanel = true, defaultVisibleInAutoForm = false) @Column(length = 200, nullable = true) private String salt; @Column(length = 30, nullable = true) private String activeDirectoryDomain; @OneToMany(fetch = FetchType.LAZY, mappedBy = "account") private List<RoleAccount> roles = new ArrayList<RoleAccount>(); public Account() { } }
The Account entity extends the base entity and adds additional columns. The entity is stored in a table with a default name (ACCOUNT), and includes a unique index on the column username. The name of the index is generated.
RoleAccount Entity
RoleAccount stores relations between accounts and roles. I consider it good practice to explicitly define these many-to-many relations as a stand alone entity, and not using a many-to-many annotation with an automatically generated relations table. You gain more control over the relation by defining it as an entity.
@Entity @Table(indexes = { @Index(name = "IDX_ROLEACCOUNT_ACCOUNT", columnList = "account"), @Index(name = "IDX_ROLEACCOUNT_ROLE", columnList = "role") }) public class RoleAccount extends FriggEntity { private static final long serialVersionUID = 1L; @FriggDomainDescriptor(defaultEditableInAutoForm = false) @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "account", nullable = false) private Account account; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "role", nullable = false) private Role role; public RoleAccount() { } public RoleAccount(Account account, Role role) { setAccount(account); setRole(role); } }
The properties account and role have been annotated with @JoinColumn. Normally, this would lead to the generation of a table with two foreign key constraints. Google Cloud Spanner does however not support traditional foreign key constraints, and these are therefore also not generated.
Google Cloud Spanner does support Interleaved Tables (https://cloud.google.com/spanner/docs/schema-and-data-model#creating_interleaved_tables). Interleaved tables are never generated by the schema generation of the Google Cloud Spanner Hibernate dialect. If you want a schema using interleaved tables, you will have to create that part of the schema manually.
Getting the Project
The example project is a multi-module Maven project. It also depends on another multi-module Maven project (General Application Framework, gaf, https://github.com/olavloite/gaf). You should get both from GitHub and import them into your IDE.