Эта статья является частью книги про Spring Framework, которая по планам должна выйти где-нибудь в 2024 году, ну в крайнем случае в 2025, если не все будет получаться.
Hibernate позволяет отобразить связь один ко многим не только в виде реализаций интерфейса List и Set, но и в качестве реализации Map с определённым ключом.
В качестве примера возьмём следующую базу данных:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
create sequence food_id_seq start with 1 increment by 1; create sequence pet_food_id_seq start with 1 increment by 1; create sequence pet_id_seq start with 1 increment by 1; create table food ( id integer not null, version integer, code varchar(255) check ( code in ( 'CARROT', 'DRY_FOOD', 'FISH', 'ICE_CREAM', 'APPLE', 'CABBAGE', 'CHOCOLATE', 'FRENCH_FRIES', 'JAPANESE_ROLLS', 'PIE', 'POTATOES', 'SANDWICH', 'BANANA', 'WATERMELON')), primary key (id), unique (code) ); create table pet ( id integer not null, version integer, name varchar(255), primary key (id), unique (name) ); create table pet_food ( food_count integer, food_id integer, id integer not null, pet_id integer, version integer, primary key (id) ); alter table if exists pet_food add constraint FKdbeoe7ihpun0qc6w8alpk1qjw foreign key (food_id) references food; alter table if exists pet_food add constraint FK5gr72r1949wbfpb3rn7o2dos6 foreign key (pet_id) references pet; |
Пусть вас не сильно пугают страшные названия вторичных ключей, эта схема была сгенерирована Hibernate, я не сам их так называл, всё это просто для примера.
А теперь представьте, что мы хотим сделать так, чтобы в сущности Pet (питомец) можно было вытащить количество еды определённого типа PetFood по известному коду этой еды food.code. Имеется в виду, чтобы можно было сделать как-то так:
1 |
pet.foods.get(CARROT).getCount(); |
Подобное вполне можно осуществить с помощью Hibernate.
Для начала сделаем класс сущности Food:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@Entity @Table(name = "food") public class Food implements Serializable { /** * */ private static final long serialVersionUID = 8791181701061581183L; @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="food_seq") @SequenceGenerator(name="food_seq", sequenceName="food_id_seq", allocationSize=1) private Integer id; @Enumerated(value = EnumType.STRING) @Column(name = "code") @NaturalId private FoodType code; @Version @Column(name = "version") private Integer version; // ... getters, setters, equals and hashCode |
Колонку code сделали перечислением FoodType, можно было использовать String, но тогда была бы возможность попытаться вставить в это поле какое-нибудь совершенно левое значение. Само перечисление FoodType:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public enum FoodType { CARROT, DRY_FOOD, FISH, ICE_CREAM, APPLE, CABBAGE, CHOCOLATE, FRENCH_FRIES, JAPANESE_ROLLS, PIE, POTATOES, SANDWICH, BANANA, WATERMELON } |
Сущность PetFood:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
@Entity @Table(name = "pet_food") public class PetFood implements Serializable { /** * */ private static final long serialVersionUID = -8225590371680345671L; @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="pet_food_seq") @SequenceGenerator(name="pet_food_seq", sequenceName="pet_food_id_seq", allocationSize=1) private Integer id; @ManyToOne @JoinColumn(name = "pet_id") private Pet pet; @ManyToOne @JoinColumn(name = "food_id") private Food food; @Column(name = "food_count") private Integer foodCount; @Version @Column(name = "version") private Integer version; // ... getters, setters, hashCode, equals |
Основная часть в сущности Pet. Мы используем аннотацию @MapKey, чтобы указать поле из PetFood, в котором хранится ключ нашей java.util.Map со списком доступных кормов питомца, а также аннотацию @OneToMany, в которой указываем поле из PetFood, в котором хранится ссылка на нашу сущность Pet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
@Entity @Table(name = "pet") public class Pet implements Serializable { /** * */ private static final long serialVersionUID = 2699175148933987413L; @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="pet_seq") @SequenceGenerator(name="pet_seq", sequenceName="pet_id_seq", allocationSize=1) private Integer id; @Column @NaturalId private String name; @Version @Column(name = "version") private Integer version; @OneToMany(mappedBy = "pet", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) @MapKey(name="food") private Map<Food, PetFood> foods = new HashMap<>(); //... getters, setters, equals. hadhCode |
А теперь сделаем небольшой пример использования наших сущностей:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public static void main(String[] args) { StandardServiceRegistry standardRegistry = new StandardServiceRegistryBuilder() .configure().build(); MetadataSources sources = new MetadataSources(standardRegistry) .addAnnotatedClass(Pet.class).addAnnotatedClass(Food.class) .addAnnotatedClass(PetFood.class); Metadata metadata = sources.buildMetadata(); SessionFactory sessionFactory = metadata.getSessionFactoryBuilder() .build(); try (EntityManager em = sessionFactory.createEntityManager(); Session session = em.unwrap(Session.class)) { method1(session); method2(session); } } |
В коде выше мы просто инициализируем Hibernate и добавляем аннотированные сущности.
Сами тесты:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public static void method1(Session session) { Transaction transaction = session.beginTransaction(); Pet pet = new Pet(); pet.setName("Mask"); session.persist(pet); Food apple = new Food(); apple.setCode(FoodType.APPLE); session.persist(apple); Food carrot = new Food(); carrot.setCode(FoodType.CARROT); session.persist(carrot); pet = session.bySimpleNaturalId(Pet.class).load("Mask"); PetFood petFood = new PetFood(); petFood.setFood(apple); petFood.setPet(pet); petFood.setFoodCount(10); pet.getFoods().put(apple, petFood); session.flush(); transaction.commit(); } public static void method2(Session session) { Transaction transaction = session.beginTransaction(); Pet pet = session.bySimpleNaturalId(Pet.class).load("Mask"); Food apple = session.bySimpleNaturalId(Food.class).load(FoodType.APPLE); Food carrot = session.bySimpleNaturalId(Food.class) .load(FoodType.CARROT); PetFood petFood = new PetFood(); petFood.setFood(carrot); petFood.setPet(pet); petFood.setFoodCount(999); pet.getFoods().put(carrot, petFood); session.flush(); transaction.commit(); } |
Здесь мы просто выполняем простенькие операции с нашими сущностями и нашей Map в Pet. При этом в логе мы можем увидеть SQL-команды вставки значений в базу данных и выборки значений:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
Hibernate: select next value for pet_id_seq Hibernate: select next value for food_id_seq Hibernate: select next value for food_id_seq Hibernate: select next value for pet_food_id_seq Hibernate: /* insert for ru.urvanov.javaexamples.hibernateonetomanymap.Pet */insert into pet (name,version,id) values (?,?,?) Hibernate: /* insert for ru.urvanov.javaexamples.hibernateonetomanymap.Food */insert into food (code,version,id) values (?,?,?) Hibernate: /* insert for ru.urvanov.javaexamples.hibernateonetomanymap.Food */insert into food (code,version,id) values (?,?,?) Hibernate: /* insert for ru.urvanov.javaexamples.hibernateonetomanymap.PetFood */insert into pet_food (food_id,food_count,pet_id,version,id) values (?,?,?,?,?) Hibernate: select next value for pet_food_id_seq Hibernate: /* insert for ru.urvanov.javaexamples.hibernateonetomanymap.PetFood */insert into pet_food (food_id,food_count,pet_id,version,id) values (?,?,?,?,?) |