Эксперт Spring АйО и по совместительству Spring Data контрибьютор Михаил Поливаха прокомментировал статью, переведенную командой Spring АйО, про поддержку составных ключей со стороны Spring Data JDBC и R2DBC, начиная с версии 4.0.0-M4 — то, чего так не хватало при работе с моделями, где первичный ключ состоит из нескольких полей. Теперь достаточно просто описать record с нужными полями, пометить его как @Id, и Spring Data сам корректно построит SQL-сущность. В статье наглядно показано, как использовать новую возможность, какие аннотации пригодятся и как обойти ограничение с автоинкрементом через BeforeConvertCallback.


Рады сообщить, что начиная с версии 4.0.0-M4 Spring Data JDBC и R2DBC наконец-то получили поддержку составных идентификаторов.

Комментарий от Михаила Поливахи

Обратите внимание, что Spring Data JDBC/R2DBC 4.x ещё не вышла, ещё нет GA релиза. Речь в статье идёт про milestone, то есть эта фича точно будет срелижена в 4-ой версии Spring Data JDBC/R2DBC, что будет уже очень скоро, наряду с Spring Framework 7

Скорее всего, большинство из вас уже знает, но на всякий случай повторим: с точки зрения базы данных составной (композиционный) идентификатор (или составной ключ) — это первичный ключ, состоящий из нескольких столбцов. В Java такие столбцы мапятся на сущность, содержащую соответствующее поле для каждого столбца. Использование должно быть интуитивно понятным, и в этой статье я продемонстрирую его на примере JDBC. Использование в R2DBC аналогично.

Для начала просто добавьте аннотацию @Id к полю в корне агрегата, которое ссылается на составной ключ, а не на примитив, обёртку над примитивом и т.п.

class Employee {

	@Id
	EmployeeId id;

	String name;

	// ...
}

record EmployeeId(
		Organization organization,
		Long employeeNumber) {
}

enum Organization {
	RND,
	SALES,
	MARKETING,
	PURCHASING
}

Такая ссылка автоматически встраивается в сущность и превращается в составной ключ. Иными словами, поля сущности превращаются в столбцы таблицы в корне агрегата.

create table employee
(
    organization    varchar(20),
    employee_number int,
    name            varchar(100)
);

Если вы хотите изменить имена столбцов, можно добавить аннотацию @Embedded, которая позволяет указать префикс. Это может выглядеть немного странно — указывать, что должно происходить при загрузке сущности, когда все её значения равны null. Но с @Embedded это необходимо, хотя первичный ключ, состоящий из null-значений, приведёт к возникновению проблем практически везде и просто не будет работать.

Комментарий от Михаила Поливахи

Здесь речь идет о том, как изначально работала аннотация Embedded, а конкретно про её параметр onEmpty: https://github.com/spring-projects/spring-data-relational/blob/main/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Embedded.java#L52

Дело в том, что изначально эта аннотация использовалась для представления embedded типов. Используя @Embedded вы были обязаны указывать аттрибут onEmpty чтобы явно сказать Spring Data JDBC, создавать ли пустой enclosing-embedded объект, если все поля в рамках этого embedded объекта и так null. Для простых embedded типов это реально имело смысл.

Когда эту аннотацию переиспользовали в рамках композитных ключей, то стало ясно, что ни одно значение в рамках аттрибута onEmpty смысла не имеет, т.к. в большинстве случаев не бывает такого композитного Primary Key, который содержит везде null в своих колонках. 

Но Вам все равно надо указать onEmpty аттрибут, пусть и фиктивно. Или использовать шорткаты типа @Embedded.Nullable как делает Jens.

Об этом и говорит Team Lead проекта.

Мы ещё подумаем, что с этим сделать. Но пока в релиз скорее всего оно уйдёт именно таким.

class Employee {

	@Id
	@Embedded.Nullable(prefix = "id_")
	EmployeeId id;

	String name;
	
	// ... 
}
create table employee
(
    id_organization    varchar(20),
    id_employee_number int,
    name            varchar(100)
);

Как и в случае с обычными идентификаторами, Spring Data Relational использует значение идентификатора как признак новой сущности: если значение самого embedded идентификатора, а не его составных частей, равно null, сущность считается новой и будет выполнена операция вставки. Если значение идентификатора не null, выполняется обновление.

Комментарий от команды Spring АйО

Spring Data Relational – это такой термин "зонтик" для обозначения как Spring Data JDBC, так и Spring Data R2DBC.

При сохранении новой сущности с составным идентификатором возникает небольшая проблема: составные идентификаторы не могут быть легко сгенерированы с помощью IDENTITY колонки в БД, поскольку по определению они состоят из нескольких столбцов. Один из способов решения этой задачи — использование BeforeConvertCallback.

@Bean
BeforeConvertCallback<Employee> idGeneration() {
	return new BeforeConvertCallback<>() {
		AtomicLong counter = new AtomicLong();

		@Override
		public Employee onBeforeConvert(Employee employee) {
			if (employee.id == null) {
				employee.id = new EmployeeId(Organization.RND, counter.addAndGet(1));
			}
			return employee;
		}
	};
}
repository.save(new Employee("Mark Paluch"));

В большинстве случаев при использовании составного идентификатора, вероятно, проще задать его заранее и либо использовать optimistic locking — то есть null в поле версии будет означать, что сущность новая — либо явно вызвать JdbcAggregateTemplate.insert.

interface EmployeeRepository extends Repository<Employee, EmployeeId>, InsertRepository<Employee> {
	Employee findById(EmployeeId id);

	Employee save(Employee employee);
}
interface InsertRepository<E> {
	E insert(E employee);
}
class InsertRepositoryImpl<E> implements InsertRepository<E> {
	@Autowired
	private JdbcAggregateTemplate template;
	@Override
	public E insert(E employee) {
		return template.insert(employee);
	}
}
@Autowired
EmployeeRepository repository;

// ...

repository.insert(new Employee(new EmployeeId(Organization.RND, 23L), "Jens Schauder"));

Полный исходный код примеров, использованных в этой статье, доступен по адресу.


Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Комментарии (0)