Hibernate z trzema tabelami @OneToOne

0

Cześć. Mam trzy tabele w bazie danych oraz trzy odpowiadające im modele klas.
Client.java

@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "client")
public class Client {

    @NotBlank
    @NotNull
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    private Long id;

    @NotBlank
    @Column(nullable = false)
    private String name;

    @Column(nullable = false, name = "is_company")
    private Boolean isCompany;

    @Column(length = 10, name = "reg_no")
    private String regNo;

    @Column(length = 10, name = "vat_id")
    private String vatId;

    @Column
    private Set<Contact> contacts = new HashSet<>();

    @Column(length = 100)
    private Address address;

    @Column
    private Set<Address> shippingAddresses = new HashSet<>();
}

Contact.java

@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "contact")
public class Contact {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @NotNull
    Long id; 

    @NotBlank
    @Column(nullable = false)
    String name; 

    @Column(length = 10)
    String phone;

    @Column(length = 30)
    String email;

    @Column(length = 10)
    String fax;
}

Address.java

@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "address")
public class Address {

    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    Long id;

    @Column(nullable = false, length = 50)
    String street;

    String zip;

    String state;

    @Column(nullable = false)
    String city;

    @Column(nullable = false)
    String country;
}

**application.properties
**

spring.thymeleaf.cache = false

spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost/task?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useSSL=false
spring.datasource.username = root
spring.datasource.password = root

# Configure Hibernate DDL mode: create / update
spring.jpa.properties.hibernate.hbm2ddl.auto = create

Teraz mam kilka pytań:

  1. Klasa Client.java robi problem (Unable to build Hibernate SessionFactory):

2018-01-18 1338.569 WARN 3524 --- [ restartedMain] ationConfigEmbeddedWebApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory
2018-01-18 1338.574 INFO 3524 --- [ restartedMain] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
2018-01-18 1338.587 INFO 3524 --- [ restartedMain] utoConfigurationReportLoggingInitializer :

Error starting ApplicationContext. To display the auto-configuration report re-run your application with 'debug' enabled.
2018-01-18 1338.595 ERROR 3524 --- [ restartedMain] o.s.boot.SpringApplication : Application startup failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory

Jeśli zakomentuję tę klasę to wszystko jest OK i aplikacja buduje się, w czym problem? Hibernate działa poprawnie bo z klasy Address.java mogę wyświetlać dane w Thymeleaf z bazy. Do bazy też wstawiają mi się dane z pliku w resources więc tutaj jest OK.

  1. W jaki sposób mogę połączyć te trzy tabele w relacjach jeden do jednego po id (chyba że użyć jeden do wielu bo w sumie przy relacji w bazie jeden do jednego wynik będzie taki sam)? Starałem się to robić adnotacją @OneToOne jednak mimo różnych prób nie udało się to. Mogę połączyć tabelę address z tabelą client po id a tabelę client z tabelą contact również pod id? Czy potrzebuję jakichś dodatkowych id i tak nie można zrobić?

  2. Czy typ Set<Address> i Set<Contact> w Client.java normalnie zostaną zmapowane przez Hibernate jak każda inna zmienna np typu String czy trzeba tu wykonywać jakąś operację dodatkowo?

  3. Zrobiłem sobie interfejs ClientRepository który rozszerza JpaRepository<Client, Long>. Czy teraz wykonując jakieś metody CRUD operację będą wykonywały się na wszystkich tabelach bo zadba o to Hibernate (dzięki połączeniu danych z tabel wg punktu 2)?

0

Ad 1 /2
Pokaż jak robiłeś to adnotacją @OneToOne.
Poza tym Client -> Adress masz Seta więc @OnaToMany?
Ad 3. Tak. Ale akurat String jest tragicznym przykladem bo Set<String> wymaga dodatkowej adnotacji @ElementCollection
Ad 4. Jak będzie odpowiedni typ Cascade to tak.

0
  1. Adnotacja @Column bez żadnego parametru nic nie robi, możesz ją usunąć.
  2. W klasie Client brakuje ci mapowania relacji, dlatego kontekst Hibernate'a nie może wstać. Pokaż te swoje próby mapowania.
0
artur52 napisał(a):

Ad 1 /2
Pokaż jak robiłeś to adnotacją @OneToOne.

bames napisał(a):
  1. W klasie Client brakuje ci mapowania relacji, dlatego kontekst Hibernate'a nie może wstać. Pokaż te swoje próby mapowania.

Podejście miałem takie że Client.java będzie ownerem relacji więc w tej klasie dałem referencje typu Address i Contact a nad nimi adnotację OneToOne. Dokładnie w ten sposób:

@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "client")
public class Client {

    @NotBlank
    @NotNull
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    private Long id;

    @NotBlank
    @Column(nullable = false)
    private String name;

    @Column(nullable = false, name = "is_company")
    private Boolean isCompany;

    @Column(length = 10, name = "reg_no")
    private String regNo;

    @Column(length = 10, name = "vat_id")
    private String vatId;

    @Column
    private Set<Contact> contacts = new HashSet<>();

    @OneToOne
    private Address address;

    @OneToOne
    private Contact contact;

    @Column
    private Set<Address> shippingAddresses = new HashSet<>();
}

Kończy się to Unable to build Hibernate SessionFactory.

artur52 napisał(a):

Poza tym Client -> Adress masz Seta więc @OnaToMany?

Chyba źle sobie wyobrażam działanie tych relacji, zakładałem że ten Set automatycznie mi się zmapuje a relację robię tylko po to aby dopasować wartości w tabelach np tylko po unikalnym id (wcześniej OneToOne robiłem nad id klas). Że relacje jeden do wielu to jeden wiersz z pierwszej tabeli pasujący do wielu wierszy z drugiej tabeli, a tutaj mamy jedno pole (z klasy Client.java) do wielu pasujących pól z klasy Address.java i Contact.java. Hibernate dopasowuje rekordy do siebie ze wszystkich tabel po unikalnym id?

artur52 napisał(a):

Ad 4. Jak będzie odpowiedni typ Cascade to tak.

OK, to jest dobra wiadomość bo już się bałem o "rozjazdy" między tabelami. Przez chwilę miałem pomysł aby wszystkie te trzy klasy rozszerzały jeden interfejs, wtedy w interfejsie który rozszerza JpaRepository miałbym typ wspólny tych trzech klas...

bames napisał(a):
  1. Adnotacja @Column bez żadnego parametru nic nie robi, możesz ją usunąć.

OK, tak zrobię.

1

To:

@Column
private Set<Contact> contacts = new HashSet<>();

nie jest kolumna tylko relacja, powinno być

@OneToMany
private Set<Contact> contacts = new HashSet<>();

to tak samo:

@Column
private Set<Address> shippingAddresses = new HashSet<>();

Jak to zmienisz to Hibernate wstanie.

0

Obecnie mam problem z tym aby tabele w bazie miały ze sobą relację. Kolumny wygenerowane przez Hibernate automatycznie mają nulle.
workbench_data.PNG

Coś powinienem zmienić w adnotacjach?

Obecnie moje klasy wyglądają z adnotacjami w ten sposób:
Address.java

@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "address")
public class Address {

    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    Long id;

    @Column(nullable = false, length = 50)
    String street;

    String zip;

    String state;

    @Column(nullable = false)
    String city;

    @Column(nullable = false)
    String country;

    @ManyToOne(fetch = FetchType.LAZY)
    Client client;
}

Contact.java

@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "contact")
public class Contact {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @NotNull
    private Long id; 

    @NotBlank
    @Column(nullable = false)
    private String name; 

    @Column(length = 10)
    private String phone;

    @Column(length = 30)
    private String email;

    @Column(length = 11)
    private String fax;

    @ManyToOne(fetch = FetchType.LAZY)
    Client client;
}

Client.java

    @NotBlank
    @NotNull
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    private Long id; 

    @NotBlank
    @Column(nullable = false)
    private String name;

    @Column(nullable = false, name = "is_company")
    private Boolean isCompany;

    @Column(length = 10, name = "reg_no")
    private String regNo;

    @Column(length = 10, name = "vat_id")
    private String vatId;

    @OneToMany(mappedBy = "client", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Contact> contacts = new HashSet<>();

    @OneToOne
    private Address address;

    @OneToOne
    private Contact contact;

    @OneToMany(mappedBy = "client", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Address> shippingAddresses = new HashSet<>();
}
0

Chodzi ci o adress_id i contact_id ? A ustawiłeś, żeby baza danych automatycznie przypisywała indeksy ? Jak nie, to zmień strategy na GenerationType.IDENTITY w @GeneratedValue.

0

Pokaż jeszcze kod, którym wrzucasz dane do bazy.

0
slayer9 napisał(a):

zmień strategy na GenerationType.IDENTITY w @GeneratedValue.

Tak zrobiłem i po przebudowaniu dalej są nulle. Usunąłem też cały Schema i stworzyłem na nowo i jest to samo.

bames napisał(a):

Pokaż jeszcze kod, którym wrzucasz dane do bazy.

To jest mój kod sql:

INSERT INTO `client` (`id`, `name`, `is_company`, `reg_no`, `vat_id`)
VALUES
  (1, "CompanyOne", 1, "123456785", "5648987456"),
  (2, "HP Poland", 0, "569874785", "8628954756");

INSERT INTO `address` (`id`, `city`, `country`, `state`, `street`, `zip`)
VALUES
  (1, "Lodz", "Poland", "Lodz Baluty", "Piotrkowska", "22-456"),
  (2, "Warsaw", "Poland", "Center", "Marszalkowska", "45-879");

INSERT INTO `contact` (`id`, `name`, `phone`, `email`, `fax`)
VALUES
  (1, "Jan Kowalski", "608607606", "[email protected]", "(33)6341100"),
  (2, "Roman Kobalczyk", "550656989", "[email protected]", "(11)7588997");

Spróbowałem jeszcze ręcznie dodać przykładowo do tabeli client wartości w kolumny address_id oraz contact_id:

INSERT INTO `client` (`id`, `name`, `is_company`, `reg_no`, `vat_id`, `address_id`, `contact_id`)
VALUES
  (1, "CompanyOne", 1, "123456785", "5648987456", 1, 1),
  (2, "HP Poland", 0, "569874785", "8628954756", 2, 2),

Kończy się to

Cannot add or update a child row: a foreign key constraint fails (task.client, CONSTRAINT FKb137u2cl2ec0otae32lk5pcl2 FOREIGN KEY (address_id) REFERENCES address (id))

Wydaje się że jednak skoro Hibernate automatycznie tworzy te kolumny to również powinien automatycznie wypełnić odpowiednio rekordy w tej kolumnie wartościami?

0

Wykonujesz ręcznie sqla na bazie i dziwisz się, że masz w kolumnach nulle?

0

Kilka rzeczy tutaj się zebrało, które są nie tak.

  1. Błąd przy wstawianiu wartości do tabeli client, mówi tyle, że chcesz się odwołać do nieistniejącego rekordu w tabeli address.
  2. SQL którego wyżej podałeś jest niepoprawny składniowo bo ma " zamiast '. Po poprawieniu, działa.
INSERT INTO 
  address (id, city, country, state, street, zip)
VALUES
  (1, 'Lodz', 'Poland', 'Lodz Baluty', 'Piotrkowska', '22-456'),
  (2, 'Warsaw', 'Poland', 'Center', 'Marszalkowska', '45-879');
INSERT INTO 
  contact (id, name, phone, email, fax)
VALUES
  (1, 'Jan Kowalski', '608607606', '[email protected]', '(33)6341100'),
  (2, 'Roman Kobalczyk', '550656989', '[email protected]', '(11)7588997');
INSERT INTO 
  client (id, name, is_company, reg_no, vat_id, address_id, contact_id)
VALUES
  (1, 'CompanyOne', 1, '123456785', '5648987456', 1, 1),
  (2, 'HP Poland', 0, '569874785', '8628954756', 2, 2);
  1. Wykonując zapytanie SQL bezpośrednio na bazie omijasz całego Hibernate'a. On nie jest świadomy tego co robisz. W takim wypadku generuje tylko DDL dla encji podczas uruchamiania i nic poza tym. Błąd, który zobaczyłeś był z DBMSa.

  2. Nie uważasz że relacja Client -> Adres i Client -> Contact powinna być jednokierunkowa?

  3. Do zapisu danych powinieneś użyć Hibernate'a, np. Spring Data Repository albo po prostu EntityManagera. Utwórz te obiekty encji i zapisz. Pamiętaj że musisz ustawić relacje po obu stronach, żeby nie było nulli. Robi się to poprzez dopisanie metody do dodawania obiektu do encji która ustawia relacje po obu stronach.

0

To mi dużo wyjaśnia co napisałeś.

bames napisał(a):
  1. Nie uważasz że relacja Client -> Adres i Client -> Contact powinna być jednokierunkowa?

Racja, poprawiłem to poprzez usunięcie w Address.java i Contact.java:

    @ManyToOne(fetch = FetchType.LAZY)
    Client client;

a w Client.java przy adnotacjach nad Setami usunąłem mappedBy = "client" i jest OK.

bames napisał(a):
  1. Do zapisu danych powinieneś użyć Hibernate'a, np. Spring Data Repository albo po prostu EntityManagera. Utwórz te obiekty encji i zapisz. Pamiętaj że musisz ustawić relacje po obu stronach, żeby nie było nulli. Robi się to poprzez dopisanie metody do dodawania obiektu do encji która ustawia relacje po obu stronach.

Staram się użyć Spring Dara Repository jednak wyrzuca mi:

List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='may not be null', propertyPath=id, rootBeanClass=class task.model.Client, messageTemplate='{javax.validation.constraints.NotNull.message}'}
ConstraintViolationImpl{interpolatedMessage='may not be empty', propertyPath=id, rootBeanClass=class task.model.Client, messageTemplate='{org.hibernate.validator.constraints.NotBlank.message}'}
ConstraintViolationImpl{interpolatedMessage='may not be empty', propertyPath=name, rootBeanClass=class task.model.Client, messageTemplate='{org.hibernate.validator.constraints.NotBlank.message}'}

Do zapisywania stworzyłem w kontrolerze:

    @RequestMapping("/create")
    public String newClient(Model model) {
        model.addAttribute("client", new Client());

        return "create";
    }

    @RequestMapping(value = "add", method = RequestMethod.POST)
    public String saveClient(Client client) {
        clientRepository.save(client);

        return "index";
    }

Mój cały szablon z create.html wygląda tak:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css"
          href="../webjars/bootstrap/3.3.7/css/bootstrap.min.css" />
    <link rel="stylesheet" th:href="@{/css/main.css}"
          href="../css/main.css" />
</head>
<body class="container">
<h1>Create</h1>
<form class="form-horizontal" th:action="@{/add}" method="post" th:object="${client}">
    <div class="row">
        <div class="col-lg-6">
            <input type="hidden" th:field="*{id}" />
            <div class="input-group">
                <label for="name">Name</label>
                <input class="form-control" type="text" id="name" />
            </div>

            <div class="input-group">
                <input type="checkbox" id="isCompany" name="active" />
                <label for="isCompany"> Is Company?</label>
            </div>

            <div class="input-group">
                <label for="RegNo">Reg No</label>
                <input class="form-control" type="text" id="RegNo" />
            </div>

            <div class="input-group">
                <label for="VatID">Vat ID</label>
                <input class="form-control" type="text" id="VatID" />
            </div>
            <h2>Address:</h2>
            <div class="input-group">
                <label for="street">Street</label>
                <input class="form-control" type="text" id="street" />
            </div>
            <div class="input-group">
                <label for="zip">Zip</label>
                <input class="form-control" type="text" id="zip" />
            </div>

            <div class="input-group">
                <label for="city">City</label>
                <input class="form-control" type="text" id="city"/>
            </div>

            <div class="input-group">
                <label for="state">State</label>
                <input class="form-control" type="text" id="state"/>
            </div>

            <div class="input-group">
                <label for="country">Country</label>
                <input class="form-control" type="text" id="country"/>
            </div>
        </div>
        <div class="table-responsive">
            <h2>Contacts:</h2>
            <table class="table table-hover table-striped table-bordered">
                <thead class="thead-light">
                <tr>
                    <th><a href="#">
                        <span class="glyphicon glyphicon-chevron-down"></span>
                    </a>Name
                    </th>

                    <th><a href="#">
                        <span class="glyphicon glyphicon-chevron-down"></span>
                    </a>Phone
                    </th>

                    <th><a href="#">
                        <span class="glyphicon glyphicon-chevron-down"></span>
                    </a>Email
                    </th>

                    <th><a href="#">
                        <span class="glyphicon glyphicon-chevron-down"></span>
                    </a>Fax
                    </th>
                </tr>
                </thead>
                <tr>
                    <td><input class="form-control" type="text" /></td>
                    <td><input class="form-control" type="text" /></td>
                    <td><input class="form-control" type="text" /></td>
                    <td><input class="form-control" type="text" /></td>
                </tr>
            </table>
        </div>

        <div class="table-responsive">
            <h2>Shipping Addresses</h2>
            <table class="table table-hover table-striped table-bordered">
                <thead class="thead-light">
                <tr>
                    <th><a href="#">
                        <span class="glyphicon glyphicon-chevron-down"></span>
                    </a>Street
                    </th>

                    <th><a href="#">
                        <span class="glyphicon glyphicon-chevron-down"></span>
                    </a>ZIP
                    </th>

                    <th><a href="#">
                        <span class="glyphicon glyphicon-chevron-down"></span>
                    </a>City
                    </th>

                    <th><a href="#">
                        <span class="glyphicon glyphicon-chevron-down"></span>
                    </a>State</th>

                    <th><a href="#">
                        <span class="glyphicon glyphicon-chevron-down"></span>
                    </a>Country</th>
                </tr>
                </thead>
                <tr>
                    <td><input class="form-control" type="text" /></td>
                    <td><input class="form-control" type="text" /></td>
                    <td><input class="form-control" type="text" /></td>
                    <td><input class="form-control" type="text" /></td>
                    <td><input class="form-control" type="text" /></td>
                </tr>
            </table>
        </div>

        <button type="submit" class="btn btn-primary">Submit</button>
        <a th:href="@{/}" type="button" class="btn btn-default">Cancel</a>
    </div>
</form>
</body>
</html>

Spróbowałem usunąć dane aby zobaczyć czy dane zostaną usunięte ze wszystkich tabel, ale niestety usuwane są tylko z tabeli client. Do usuwania używam:

    @Transactional
    @RequestMapping("/delete/{id}")
    public String delelete(@PathVariable("id") Long id) {
        clientRepository.delete(id);

        return "index";
    }

Brakuje jakiejś adnotacji? Wydawało mi się że usunięcie ze wszystkich tabel załatwia 'cascade = CascadeType.ALL' w Client.java.

1

Chyba próbujesz robić za dużo rzeczy na raz i na każdym kroku masz jakiś błąd. Próbuj to robić mniejszymi krokami to ci łatwiej będzie debugować samemu.

Tak na początek to źle jest napisany formularz w templatce. Jego zatwierdzenie nie powoduje wysłania danych w body requesta. To powoduje, że pola encji są puste i leci wyjątek, bo używasz walidacji. Użyj do bindowania inputów th:field.

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