Unique index or primary key violation

0

Cześć,

Zacznę od tego, że szukałem na różnych forach rozwiązania tego problemu, niestety wszystkie proponowane rozwiązania u mnie nie działały. Piszę projekt imitujący zachowanie bloga, chciałem zacząć pisać testy integracyjne z wykorzystaniem JUnit 5 i bazy H2 inmemory.

Problem jest taki:

Failed to execute SQL script statement #1 of URL [file:/E:/IntelliJ%20IDEA/projects/blog_mj/out/test/resources/data.sql]: insert into user (created_date, email, last_updated_date, password, username) values ( '2018-12-28 00:51:33', '[email protected]', '2018-05-03 13:31:25', 'nkfmVFGsXF', 'munwins0'); nested exception is org.h2.jdbc.JdbcSQLException: Unique index or primary key violation: "UK_OB8KQYQQGMEFL0ACO34AKDTPE_INDEX_2 ON PUBLIC.USER(EMAIL) VALUES ('[email protected]', 1)"; SQL statement:
insert into user (created_date, email, last_updated_date, password, username) values ( '2018-12-28 00:51:33', '[email protected]', '2018-05-03 13:31:25', 'nkfmVFGsXF', 'munwins0')

Dzieje się to przy próbie odpalenia testu integracyjnego, wtedy, kiedy aplikacja próbuje załadować kontekst. Plik .sql, z którego robię inserty:

insert into user (created_date, email, last_updated_date, password, username)
values ( '2018-12-28 00:51:33', '[email protected]', '2018-05-03 13:31:25', 'nkfmVFGsXF', 'munwins0');
insert into user (created_date, email, last_updated_date, password, username)
values ('2019-03-10 14:14:49', '[email protected]', '2018-08-18 23:35:15', 'U6qZxNg7MHLY', 'fknibb1');
insert into user ( created_date, email, last_updated_date, password, username)
values ( '2018-08-26 22:38:12', '[email protected]', '2018-06-25 06:20:44', 'x4ZgJPYdr0', 'vchippindale2');
insert into user ( created_date, email, last_updated_date, password, username)
values ( '2018-08-02 07:39:46', '[email protected]', '2018-09-06 13:34:16', 'PNL635jpw', 'ccanniffe3');
insert into user ( created_date, email, last_updated_date, password, username)
values ( '2018-05-31 22:28:19', '[email protected]', '2018-04-20 05:51:20', 'khL3XK', 'epochin4');

Z tych komunikatów, to jeśli dobrze rozumiem, zaburzyłem unikalność na kolumnie email, tylko że w insertach nigdzie nie mam duplikatów. Wie ktoś jak rozwiązać problem? Byłbym bardzo wdzięczny :D

0

Kiedy odpalasz tę SQLkę? Być może wykonuje się ona po prostu wielokrotnie (np. za każdym razem przed każdym kolejnym testem). Tak to wygląda bo sypie się już na pierszym insercie:)

0

Ta sqlka jest z pliku data.sql, który wykonuje się przy starcie aplikacji, jeśli się nie mylę?

Edit: to samo na MySQL wykonuje się bezproblemowo

0

Być może nie jest tworzona nowa za każdym razem. Spróbuj dać na początku drop table if exists user;

0

Dodałem tą linijkę przed każdą serią insertów iii:

Failed to execute SQL script statement #2 of URL [file:/E:/IntelliJ%20IDEA/projects/blog_mj/out/test/resources/data.sql]: insert into user (created_date, email, last_updated_date, password, username) values ( '2018-12-28 00:51:33', '[email protected]', '2018-05-03 13:31:25', 'nkfmVFGsXF', 'munwins0'); nested exception is org.h2.jdbc.JdbcSQLException: Table "USER" not found; SQL statement:
insert into user (created_date, email, last_updated_date, password, username) values ( '2018-12-28 00:51:33', '[email protected]', '2018-05-03 13:31:25', 'nkfmVFGsXF', 'munwins0')

Czyli tak jakby Hibernate nie tworzył tej bazy danych przy starcie?

1

To usuwa Ci całą tabelę, po tym musisz ją stworzyć (create table ...) lub zamiast usuwania tabeli usunąć wszystkie dane - delete from user;

Edit: Nie zauważyłem, że używasz Hibernate'a. W takim razie użyj właśnie delete from user; przed dodawaniem:)

2

Tak na boku - weź zastąp trzymanie haseł w plaintext jakimś haszem z saltem - choćby BCrypt czy PBKDF2, zanim to komukolwiek pokażesz :P Zaraz będą niewygodne pytania dlaczego tak i czy jesteś pewien, że to bezpieczne i dlaczego nie haszujesz choć wiesz, że powinieneś.

To są dane wrzucane przy starcie aplikacji i co się później dzieje? Testy mają jakieś swoje dodatkowe migracje, dodające dane testowe, czy po prostu odpalasz test jakimś runnerem i tyle? Masz jakieś dodatkowe migracje specyficzne dla testów? Jeśli tak, czym je zaciągasz?

Spróbuj dać na początku drop table if exists user;

Miałeś chyba na myśli truncate table user? :P W sumie to dobre pytanie, czy H2 w ogóle wspiera truncate i jeśli tak, to czy ma w ogóle składnię taką samą, jak MySQL.

Ewentualnie możesz spróbować z insert ignore.

0

W module main już te hasła są zahashowane BCryptem, tutaj tylko chcę, żeby zaczęło działać na bazie H2, później zajmę się poprawkami :)

Testy nie mają żadnych dodatkowych migracji, dodają to samo, co main przy starcie serwera.
Może to pomoże w rozwiązaniu problemu, klasa testowa wygląda tak:

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class PostControllerITest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext wac;

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build();
    }

    @Test
    @WithMockUser
    void findAllTest() throws Exception {
        mockMvc.perform(get("/posts"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(jsonPath("$.posts.length()").value(10))
                .andExpect(jsonPath("$.posts.comments").doesNotExist())
                .andExpect(jsonPath("$.posts[0].tags.length()").value(3))
                .andExpect(jsonPath("$.posts[0].user").exists());
    }
}

@jwrabel delete from user; dla bazy H2 nie działa, wywala błąd: Referential constraint integrity violation (czy jakoś tak :D)

1

Ewentualnie możesz jeszcze pokombinować z @DirtiesContext, może gdzieś boczkiem przesiąkają jakieś babole od testu do testu? Czy to jest jedyny test póki co?

Wujek Baeldung też może pomoże :P

0

To jest póki co jedyny test jaki mam (oprócz contextLoads() ). Dodałem też adnotację java @DirtiesContext ale nic nie pomogło.
Patrząc po logach, to ten plik data.sql tak jakby wykonywał się dwa razy, może tu jest problem?

1

Pełny log: [...]

To akurat nie wygląda, jakby dwa razy wykonał się skrypt, po prostu wyjątek z JDBC jest owrapowany - chyba, że gdzieś wcześniej masz jakiś ślad po tym, że jednak gdzieś już migracja się wykonała. Nie logujesz tego, kiedy i które migracje są odpalane? Może faktycznie w tym kierunku trzeba podrążyć.

Prawdę mówiąc nie mając pełnego obrazu tego, jak masz wszystko skonfigurowane etc. nie jestem za bardzo w stanie Ci podpowiedzieć, co właściwie jest na rzeczy, ale może ktoś kto spędził więcej czasu na placu boju coś wymyśli. Nigdy nie próbowałem ręcznie bawić się z migracjami, w pracy używamy https://github.com/flyway/flyway-test-extensions i @FlywayTest do oznaczania testów integracyjnych itp, migracjami zajmuje się Flyway i dotąd nie wpędziliśmy się w podobny problem.

2

Pokaż jdbc.url jaki używasz do tej bazy. Być może baza H2 utworzyła Ci bazę w pliku i nie skasowała.

Jedno rozwiązanie to by było tak jak wcześniej ktoś pisał:
drop table user if exists dać przed komendą create table user lub truncate table user dać po komendzie create table user. Zrobiłeś tak?

Drugie to zmienić jdbc.url aby H2 uruchamiało się in memory i razem z procesem JVM niszczyło.

Połączenie obu będzie najlepsze o ile nie robisz czegoś czego H2 mem nie wspiera.

0

A spróbuj jeszcze ustawić spring.jpa.hibernate.ddl-auto=create-drop w application.properties czy lepiej w propertiesach odpowiednich dla Twojego profilu testowego (tam gdzie masz dane do h2 wpisane). Warto od razu zrobić sobie dwa profile, żebyś później przypadkiem produkcyjnej bazy nie wywalił.

1

Nie masz tak, że testy wykonywane są na wielu wątkach i dlatego ładujesz do H2 te same dane równolegle?

0

@nie100sowny:
Mój jdbc url to: jdbc:h2:mem:test , a co do tych skryptów, to ja używam Hibernate, nie da się jakoś tego ominąć, żeby nie pisać tych DDLów? Wtedy Hibernate utworzyłby mi wszystkie tabele, usunął je i ponownie utworzył, ma to sens?

@kkojot:
Dokładnie tak mam, mam oddzielny profil (application.properties i data.sql) dla testów

@yarel:
Nie wiem, a jak to sprawdzić? :D

1

Może zaczekajmy aż OP poda więcej szczegółów, póki co wiemy tylko tyle, że

  • używa H2 in-memory lub przynajmniej miał taki zamiar, ale mógł źle skonfigurować - tego nie wiemy, bo nie wiemy co siedzi w URL do bazy
  • póki co ma tylko ten jeden test i nic więcej
  • raczej nie korzysta z jakichś dedykowanych narzędzi do zarządzania i wersjonowania migracji, skoro ten skrypt SQL odpala się po prostu zawsze co start aplikacji. Migracja jest wrzucona do jakiegoś skryptu data.sql odpalanego defaultowo przez Spring Boot.

W ramach małęgo podsumowania, skoro wychodzi na to, że gdy migracja w teście rusza, to dane już tam są, to opcje są pewnie dwie trzy:

  • z jakiegoś powodu w teście skrypt odpala się dwukrotnie - np. dwukrotnie ładowany jest kontekst - zerknij tu i tu. OP - upewnij się, że ApplicationContext nie jest ładowany dwukrotnie w testach, że nie masz jakichś ustawień / configów mogących spowodować dwukrotne jego ładowanie itp.
  • baza faktycznie wcale nie jest in-memory i po testach / uruchomieniu aplikacji w bazie zostają śmieci które później kładą inicjalizację testu. OP - upewnij się, że baza H2 na 100% jest in-memory i że URL do bazy wskazuje faktycznie na coś w rodzaju jdbc:h2:mem:baza_testowa;OPCJA_1=ILEŚ;ETC
  • EDIT: zduplikowane dane w skryptach

Żeby to zfiksować, pewnie czeka Cię min. 1 z poniższych:

  • poprawić konfigurację i/lub wprowadzić poprawki - jeśli np. masz kilka implementacji SpringBootServletInitializer - np. jedną "developerską/produkcyjną", a drugą do testów etc. Jeśli nie możesz wywalić/zunifikować wielu implementacji, to uzależnij tworzenie beanów np. od profilu - @Profile("test"), @Profile("dev") itp., lub od jakichś ustawień w propertiesach i przestawiaj je do testów: @ConditionalOnProperty(name = ["my.custom.prop..testing"], havingValue = "true", matchIfMissing = false)
  • jak jeszcze nie masz, to utworzyć sobie osobny profil testowy np. test i odpowiedni config w application-test.properties - tak na zapas, żeby mieć porządek w projekcie.
  • dorzuć w pliku ze schemą bazy linijkę DROP TABLE IF EXISTS .. przed CREATE TABLE ... lub dorzuć TRUNCATE TABLE / DELETE FROM przed ładowaniem danych do bazy
  • nie powierzaj ładowania tych skryptów Spring Bootowi. Będzie to robił przy każdym starcie aplikacji, przy każdym ładowaniu kontekstu, będzie odwalał Ci różne cyrki, wyczyści bazę w której już coś masz albo wywali się, próbując utworzyć już istniejącą tabelę. Spróbuj się pobawić jakimś toolem do migracji np. Flyway, o którym już wspomniałem, albo np. Liquibase. Utwórz sobie osobne migracje dla aplikacji (schema) oraz osobne dla testów (dane testowe). Wykorzystaj narzędzia, by zapewnić sprzątanie bazy przed każdym testem, choćby tymi flyway-test-extensions, oznacz testy jako @FlywayTest jeśli zdecydujesz się na to narzędzie albo użyj odpowiednika z innych narzędzi. @FlywayTest pozwala zdefiniować dodatkowe lokalizacje dla migracji testowych - to się przydaje.

EDIT - post się troszkę zdezaktualizował przez to, że OP uprzedził moje pytania tuż przed zamieszczeniem posta, no ale już zostawię.

0
Maciej Jeleń napisał(a):

@kkojot:
Dokładnie tak mam, mam oddzielny profil (application.properties i data.sql) dla testów

Czy ten skrypt jest zduplikowany i masz go w kilku miejscach? Może przy ładowaniu testów znajdowane są dwa skrypty (jeden w resourcach normalnych i jeden w testowych) i odpalane są oba, przez co przy drugim się wykłada?

Ten oddzielny profil to po prostu nadpisane application.properties, tak? To raczej powinien być całkiem oddzielny profil application-test.properties + wskazywanie na profil test przez @Profile w beanach tylko dla testów, wtedy masz większą kontrolę nad tym, co się dzieje (rzeczy testowe ładują się warunkowo, jeśli aktywny jest profil testowy itp)

2

Tak, jest dokładnie tak jak napisałeś - zduplikowany plik SQL, tylko w innej lokalizacji w strukturze i application.properties w test/resources, konfigurujące H2

Dla pewności wrzuć dla odmiany jakieś inne dane do testowego, niż miałeś w oryginalnym (by nie wywalił się na duplikatach) i sprawdź, czy w trakcie testu w bazie siedzą dane z obu. Obstawiam, że to będzie to :P W trakcie odpalania testów przeglądane są oba foldery resources (aplikacji i testowe), w obu znajdowane są pliki domyślnie ładowane przez Spring Boota (data.sql, schema.sql, data-platform.sql itp) więc prawdopodobnie wciągane są obydwa i gdy drugi raz ładowane są rekordy z tymi samymi emailami, wywala się constraint ;)

W ramach quick-fixa możesz zmienić nazwę skryptów z danymi na nie-defaultowe, by nie ładowały się z marszu, i wskazywać na odpowiednie skrypty explicite w kofiguracji profilu np.:

spring.datasource.data=test_specific_data.sql

Do poczytania:
https://www.baeldung.com/spring-boot-sql-import-files

0

Okej, wychodzi na to że naprawdę wykonuje się dwa razy ten plik - jeden z main/resources, drugi - z test/resources, zrobiłem tak jak @superdurszlak napisał i wszystko działą :D

Czyli mogę usunąć plik data.sql z test/resources i będzie to samo?

1
Maciej Jeleń napisał(a):

Czyli mogę usunąć plik data.sql z test/resources i będzie to samo?

Tak, ale lepiej będzie, jeśli skonfigurujesz sobie tak, aby:

  • normalnie żadne dane nie ładowały się do bazy przy starcie aplikacji, chyba że masz jakieś np. dane "słownikowe" które muszą być wprowadzone do bazy by aplikacja działała poprawnie - ale i tu dobrze by było skonfigurować to tak, by te skrypty nie odpalały się za każdym razem, bo po paru uruchomieniach aplikacji zrobi Ci się śmietnik. Narzędzia do migracji mogą być tu pomocne.
  • te dane testowe ładowały się wyłącznie w testach
  • wprost wskazać źródło danych dla testów, zamiast polegać na defaultach - co, jeśli inne testy będziesz chciał odpalać na zupełnie innych danych? ;)

1 użytkowników online, w tym zalogowanych: 0, gości: 1