Guide of Spring Data integration with ScalarDB
The usage of Spring Data integration with ScalarDB basically follows Spring Data JDBC - Reference Documentation. This guide describes several important topics to use Spring Data integration with ScalarDB and its limitations.
Configurations
Spring Data integration with ScalarDB is supposed to be used as a part of Spring application. The following properties are needed at least.
spring.datasource.driver-class-name
This needs to be set to fixed value com.scalar.db.sql.jdbc.SqlJdbcDriver.
spring.datasource.driver-class-name=com.scalar.db.sql.jdbc.SqlJdbcDriver
spring.datasource.url
This value follows ScalarDB JDBC connection URL configuration. See also ScalarDB JDBC Guide.
spring.datasource.url=jdbc:scalardb:?scalar.db.contact_points=jdbc:mysql://localhost:3306/my_app_ns&scalar.db.username=root&scalar.db.password=mysql&scalar.db.storage=jdbc&scalar.db.consensus_commit.isolation_level=SERIALIZABLE&scalar.db.consensus_commit.async_commit.enabled=false
scalardb.namespace
This is an optional. But if you use a single ScalarDB namespace in an application, it's recommended to set this property so that you don't need to specify @Table(schema = schema_name) annotation parameter on all Spring Data model classes.
scalardb.namespace=my_app_ns
Annotations
@EnableScalarDbRepositories annotation is needed on the JVM application to use Spring Data integration with ScalarDB as follows.
@SpringBootApplication
@EnableScalarDbRepositories
public class MyApplication {
// These repositories are described in the next section in details
@Autowired GroupRepository groupRepository;
@Autowired UserRepository userRepository;
:
Persistent entity model
The users of Spring Data JDBC for ScalarDB needs to write classes for object mapping to ScalarDB tables. How to write those classes are written in Persisting Entities, so this section describes some limitations on the ScalarDB integration.
These are example model classes:
domain/model/User
// This model class corresponds to the following table schema:
//
// create table my_app_ns.user (id bigint, group_id bigint, name text, primary key (id));
//
// -- UserRepository can use `name` column as a condition in SELECT statement
// -- as the column is a ScalarDB secondary index.
// create index on my_app_ns.user (name);
// Set `schema` parameter in @Table annotation if you don't use `scalardb.namespace` property.
//
// Spring Data automatically decides the target table name based on a model class name.
// You can also specify a table name by setting `value` parameter.
//
// @Table(schema = "my_app_ns", value = "user")
@Table
public class User {
@Id
public final Long id;
public final Long groupId;
// Spring Data automatically decides the target column name based on an instance variable name.
// You can also specify a column name by setting `value` parameter in @Column annotation.
// @Column("name")
public final String name;
public User(Long id, Long groupId, String name) {
this.id = id;
this.groupId = groupId;
this.name = name;
}
}
domain/model/Group
// This model class corresponds to the following table schema:
//
// create table my_app_ns.group (account_id int, group_type int, balance int, primary key (account_id, group_type));
@Table
public class Group {
// This column `account_id` is a part of PRIMARY KEY in ScalarDB SQL
//
// Spring Data JDBC always requires a single @Id annotation while it doesn't allow multiple @Id annotations.
// The corresponding ScalarDB SQL table `group` has a primary key consisting of multiple columns.
// So, Spring Data @Id annotation can't be used in this case, but @Id annotation must be put on any instance variable
// (@Id annotation can be put on `balance` as well.)
@Id
public final Integer accountId;
// This column `group_type` is also a part of PRIMARY KEY in ScalarDB SQL
public final Integer groupType;
public final Integer balance;
public Group(Integer accountId, Integer groupType, Integer balance) {
this.accountId = accountId;
this.groupType = groupType;
this.balance = balance;
}
}
domain/repository/UserRepository
@Transactional
@Repository
public interface UserRepository extends
PagingAndSortingRepository<User, Long>, ScalarDbHelperRepository {
// `insert()` and `update()` are automatically enabled with `ScalarDbHelperRepository`.
// Many APIs of `CrudRepository` and `PagingAndSortingRepository` are automatically enabled.
// https://docs.spring.io/spring-data/commons/docs/3.0.x/api/org/springframework/data/repository/CrudRepository.html
// https://docs.spring.io/spring-data/commons/docs/3.0.x/api/org/springframework/data/repository/PagingAndSortingRepository.html
// Also, you can prepare complicated APIs with the combination of the method naming conventions.
// https://docs.spring.io/spring-data/jdbc/docs/3.0.x/reference/html/#repositories.definition-tuning
// These APIs use the ScalarDB secondary index
List<User> findByName(String name);
List<User> findTop2ByName(String name);
// Current ScalarDB SQL doesn't support range scan or order using secondary indexes
// List<User> findByNameBetween(String name);
// List<User> findByGroupIdOrderByName(long groupId);
default void reverseName(long id) {
Optional<User> model = findById(id);
if (model.isPresent()) {
User existing = model.get();
User updated =
new User(
existing.id,
existing.groupId,
existing.name.reverse());
update(updated);
}
}
default void deleteAfterSelect(long id) {
Optional<User> existing = findById(id);
existing.ifPresent(this::delete);
}
}
domain/repository/GroupRepository
@Transactional
@Repository
public interface GroupRepository
extends PagingAndSortingRepository<Group, Long>, ScalarDbHelperRepository {
// @Id annotation is put only on Group.accountId, but ScalarDB SQL expects the combination of
// `account_id` and `group_type` columns as the table uses them as a primary key. So `findById()` can't be used.
Optional<Group> findFirstByAccountIdAndGroupType(int accountId, int groupType);
List<Group> findByAccountIdAndGroupTypeBetweenOrderByGroupTypeDesc(
int accountId, int groupTypeFrom, int groupTypeTo);
List<Group> findTop2ByAccountIdAndGroupTypeBetween(
int accountId, int groupTypeFrom, int groupTypeTo);
// `update()` method also depends on @Id annotation as well as `findById()`,
// so users need to write ScalarDB SQL in @Query annotation.
@Modifying
@Query(
"UPDATE \"my_app_ns\".\"group\" SET \"balance\" = :balance \n"
+ " WHERE \"my_app_ns\".\"group\".\"account_id\" = :accountId \n"
+ " AND \"my_app_ns\".\"group\".\"group_type\" = :groupType \n")
int updateWithAttributes(
@Param("accountId") int accountId,
@Param("groupType") int groupType,
@Param("balance") int balance);
default void incrementBalance(int accountId, int groupType, int value) {
Optional<Group> model = findFirstByAccountIdAndGroupType(accountId, groupType);
model.ifPresent(
found ->
updateWithAttributes(
found.accountId, found.groupType, found.balance + value));
}
default void transfer(
int accountIdFrom, int groupTypeFrom, int accountIdTo, int groupTypeTo, int value) {
incrementBalance(accountIdFrom, groupTypeFrom, -value);
incrementBalance(accountIdTo, groupTypeTo, value);
}
// This method name and signature results in issuing an unexpected SELECT statement and
// results in query failure. It looks a bug of Spring Data...
//
// void deleteByAccountIdAndGroupType(int accountId, int groupType);
@Modifying
@Query(
"DELETE FROM \"my_app_ns\".\"group\" \n"
+ " WHERE \"my_app_ns\".\"group\".\"account_id\" = :accountId \n"
+ " AND \"my_app_ns\".\"group\".\"group_type\" = :groupType \n")
int deleteByAccountIdAndGroupType(
@Param("accountId") int accountId, @Param("groupType") int groupType);
default void deleteByAccountIdAndGroupTypeAfterSelect(int accountId, int groupType) {
Optional<Group> entity = findFirstByAccountIdAndGroupType(accountId, groupType);
entity.ifPresent(found -> deleteByAccountIdAndGroupType(accountId, groupType));
}
}
Limitations
Multiple column PRIMARY KEY
As you see in the above example, Spring Data JDBC's @Id annotation doesn't support multiple columns. So, if a table has a primary key consisting of multiple columns, users can't use the following APIs and may need to write Scalar SQL DB query in @Query annotation
findById()existsById()update(T entity)delete(T entity)deleteById(ID id)deleteAllById(Iterable<? extends ID> ids)
Advanced features
Multi-storage transaction
ScalarDB supports Multi-storage Transaction, and users can use the feature via this Spring Data integration. The following needs to be configured to use the feature.