UUID, Version i Spring Data - na co trzeba uważać.

2

Cześć. Napiszę teraz na co natknęłam się korzystając z UUID, Version i Spring Data

Co to jest UUID?
UUID to taki naprawdę unikalny identyfikator -we wszechświecie i nie musimy przejmować się, że np. dwie osobne maszyny piszące do jednej tabelki wygenerują ten sam UUID. Możemy też np. scalić ze sobą dwie te same tabelki z różnych baz danych nie martwiąc się o naruszenie unikalności Primary Key.
UUID jest unikalny dla obiektu w kontekście całego świata a nie tylko tabeli. Brzmi fajnie prawda? Ja np. teraz wygeneruje sobie UUID:8a686cc0-4e65-4f6c-b932-b8b2ed209f4b <- o taki mi się wygenerował i mogę się założyć, że nikt inny na świecie takiego nie ma -wpisałam go nawet w google i zero wyników. Więc widać, że to działa.
UUID stosowany jest już przy klastrowaniu, w oznaczaniu partycji w Linuxie a jedna z najlepszych baz relacyjnych -czyli PostgreSQL, posiada już nawet taki typ jak UUID: https://www.postgresql.org/docs/9.1/static/datatype-uuid.html
O zaletach stosowania UUID w bazach danych, encjach możecie sobie poczytać w internecie a w kontekście np. equals i hashCode posłuchać tutaj: .

Jak można tworzyć UUID w encji?
Można naprzykład stworzyć sobie klasę abstrakcyjną SimpleEntity, po której dziedziczą wszystkie nasze encje. Klasa ta nadaje UUIDa w momencie tworzenia obiektu a nie przy save. Jest to dość istotna własność, ale o tym potem. Przykładowa implementacja może wyglądać tak:

import lombok.Getter;
import lombok.ToString;

import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.UUID;

@Getter
@ToString
@MappedSuperclass
public abstract class SimpleEntity implements Serializable {
    
    @Id
    @Column(updatable = false, nullable = false, unique = true)
    private final UUID id = UUID.randomUUID();

    @Version
    private Integer version;

    private LocalDateTime creationDate;

    private LocalDateTime modificationDate;

    @PrePersist
    private void prePersist() {
        modificationDate = creationDate = LocalDateTime.now();
    }

    @PreUpdate
    private void preUpdate() {
        modificationDate = LocalDateTime.now();
    }

    @Override
    public boolean equals(Object that) {
        return this == that || that instanceof SimpleEntity
                && Objects.equals(id, ((SimpleEntity) that).id);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id);
    }
}

Repozytorium dla encji
Korzystając z Spring Data również możemy stworzyć sobie własny interfejs dla tych encji z UUIDem , po którym będą dziedziczyć
repozytoria.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.NoRepositoryBean;

import java.util.UUID;

@NoRepositoryBean
public interface SimpleRepo<T> extends JpaRepository<T, UUID> {

}

Na co trzeba uważać w kontekście korzystania z Spring Data?
W Springu metoda save przy wyborze czy użyć persist czy merge patrzy na ID i skoro ID za każdym razem jest już ustawione to używa niepotrzebnie i niewydajnie merge.

Metoda do zapisu w SimpleJpaRepository wygląda tak:

 	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#save(java.lang.Object)
	 */
	@Transactional
	public <S extends T> S save(S entity) {

		if (entityInformation.isNew(entity)) {
			em.persist(entity);
			return entity;
		} else {
			return em.merge(entity);
		}
	}

W przypadku gdy zadeklarowaliśmy pole version w encji metoda, która sprawdza czy encja jest nowa wygląda tak:

 /* 
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.core.support.AbstractEntityInformation#isNew(java.lang.Object)
	 */
	@Override
	public boolean isNew(T entity) {

		if (versionAttribute == null || versionAttribute.getJavaType().isPrimitive()) {
			return super.isNew(entity);  // TUTAJ WCHODZI DO STANDARDOWEGO SPRAWDZENIA PO TYM CZY ID() == null
		}

		BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
		Object versionValue = wrapper.getPropertyValue(versionAttribute.getName());

		return versionValue == null;
	}

Więc, jeżeli ten UUID mamy przypisany na etapie tworzenia obiektu, przed wywołaniem save Spring myśli, że ma updateować i wywołuje za każdym razem merge, bo ID != null. Można to chyba obejść przeciążając metodę save w naszym interfejsie (nwm czy tak się da).
Drugim sposobem patrząc na ten kod jest zadeklarowanie pola version - w tym przypadku Spring Data używa tego kodu, który wkleiłam wyżej.
Tylko uwaga pole version nie może być prymitywem (int). I to tyle już mi się nie chce pisać, ale trzeba na to uważać i to jest straszne jak duże znaczenie w tym przypadku mają takie szczegóły. Nie jestem też pewna co do tego, że to jest prawidłowe obejście, bo zmienią kiedyś implementacje Springa i znowu przestanie to działać, więc polecam dopisać sobie na to test jednostkowy.

0

Wydaje mi się, że UUID to bardzo słaby primary key, ponieważ niektóre bazy słabo indeksują stringi. Za to dobry hashcode dla encji: nie zmienia się przez cały cykl życia encji. Chyba wolę zwykły long na PK i do tego odpowiednia strategia np. TABLE lub SEQUENCE.

   private String uuid = UUID.randomUUID().toString();

   @Override
   public int hashCode() {
       return Objects.hashCode(uuid);
   }
0

@margor90: można użyć typu binarnego jak pisał @Koziołek i różnica w wydajności jest bardzo mała. Jeszcze jako ciekawostka typ UUID obsługuje już nawet baza H2 (obsługuje bodajże część dialektu Postgresa po prostu) - przydatne w przypadku testowania. Odnośnie porównania wydajności znalazłam jeszcze coś takiego:
http://kccoder.com/mysql/uuid-vs-int-insert-performance/
i http://stackoverflow.com/a/13146662

Wierzę, że w przypadku PostgreSQL albo typu binarnego wygląda to lepiej, ale nie muszę się przejmować takimi małymi stratami wydajności, więc nie chce mi się tego testować

0

@margor90: Jeżeli String jest stosunkowo krótki, to różnica pomiędzy PK na String, a PK na na przykład Long nie przekracza 5%. Patrz https://www.depesz.com/2012/06/07/123-vs-depesz-what-is-faster/

UUID w bazie można trzymać w typie binarnym i indeks będzie wtedy inaczej zakładany. Różnica w wydajności? Żadna.

0

Bazy coraz lepiej indeksują stringi: to prawda. Mi by jednak szkoda było tych 5% szybkości na start: zwłaszcza, że JPA/Hibernate pozwala na łatwe podpięcie sequence lub unikalny klucz na tabelę a BIGINTEGER sprawdza się jak sztuczny klucz (czasem fajnie mieć klucze naturalne, ale to inna sprawa).

Często podczas pracy piszemy SELECT-y w natywnym SQLu, chociażby w celu diagnostycznym co siedzi w bazie. Wydaje mi się, że:

  • binarny klucz strasznie utrudnia pisanie zapytań
  • UUID to jest za długie ID, aby wygodnie pisać selecty
  • mam wrażenie, że to nie ułatwia pracy bazodanowcowi

W moich encjach UUID to dodatkowe pole do haszowania nic więcej.

Zdarzało się nam używać naturalnych kluczy jako stringów. Ale to była zupełnie inna bajka niż UUID i wynikała wprost z domeny.

0

Tyle tylko, że jeżeli używasz sekwencji do generowania PK, to musisz się nakombinować z poprawnym zaimplementowaniem hashCode i equals. Podobnie ma się sprawa z ręcznym pisaniem SQLi pod konkretny obiekt w bazie. Jeżeli musisz to zrobić, to znaczy, że masz coś nie tak z modelem. Szczególnie jeżeli robisz to w celach diagnostycznych, bo może to oznaczać, że model pozwala na stworzenie niespójnego stanu w bazie danych (na poziomie biznesowym).

0

Ręczne pisanie SQL-i jest ok i może być użyteczne poza ORM-em. Mam na myśli oczywiście raporty i DBAdminów. Poza tym robienie trudnych zapytań w HQL/JPQL to jest droga przez mękę. Założenie, że muszę mieć coś nie tak z modelem jeśli sporadycznie używam SQL wydaje mi się co najmniej dziwne. Często zdarza się, że baza którą używa aplikacja jest też używana do raportowania. Albo przynajmniej służy do zasilania hurtowni. Poza tym b. lubię alternatywy dla ORM np. jOOQ zamiat JPA/Hibernate: preferuje.

Możesz używać UUID do hashCode/equals i jednocześnie posiadać klucz bazujący np. na long. Po prostu UUID to dodatkowe pole, które musisz utrzymywać (nieindeksowane, bo po co utrzymywać taki indeks na unikalność). @karolinaa podała link do prezentacji Jakuba Kubryńskiego 32:10. Można przechowywać UUID jako hashcode w dodatkowym polu niezależnym od PK (nie zmienia się przez cały cykl życia encji). Jednocześnie nie robić śmietnika z PK. Taka redundancja prawie nic nas nie kosztuje. Co innego utrzymywanie ogromnego indeksu na PK jako UUID: chyba drzewo, które trzeba utrzymywać albo bitmapa w zależności od inplementacji bazy.

0

To które podejście w końcu preferujecie?

może takie ? https://github.com/indrabasak/jpa-postgres-spring/blob/master/jpa-postgres-spring-service/src/main/java/com/basaki/example/postgres/spring/data/entity/BookEntity.java

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "books", schema = "example")
public class BookEntity implements Serializable {

    @Id
    @Column(name = "id")
    @GenericGenerator(name = "uuid-gen", strategy = "uuid2")
    @GeneratedValue(generator = "uuid-gen")
    @Type(type = "pg-uuid")
    private UUID id;

    @Column(name = "title", nullable = false)
    private String title;

    @Column(name = "author", nullable = false)
    private String author;

    @Column(name = "genre", nullable = false)
    @Type(type = "com.basaki.example.postgres.spring.data.usertype.PGEnumUserType",
            parameters = {@Parameter(name = "enumClassName",
                    value = "com.basaki.example.postgres.spring.model.Genre")})
    private Genre genre;
}

czy może z powyższej prezentacji:

@MappedSuperclass
public abstract class BaseEntity implements Serializable {

    @Id
    @GeneretedValue
    private Long id;

    private String uuid = UUID.randomUUID().toString();

    public int hashCode() {
		return Objects.hash(uuid);
	}

    public boolean equals( Object that) {
		boolean this == that }} that instanceof BaseEntity 
               && Objects.equals(uuid, ((BaseEntity) that).uud);
	}

czy jeszcze inaczej?

0

Wolę drugie, czyli dodatkowe pole na UUID w celu hashcodu, do tego normalny PK i sensowną strategię generownia PK (np. unikalny w kontekście tabeli, ale od biedy może być i bazy czemu nie).

0
margor90 napisał(a):

Wolę drugie, czyli dodatkowe pole na UUID w celu hashcodu, do tego normalny PK i sensowną strategię generownia PK (np. unikalny w kontekście tabeli, ale od biedy może być i bazy czemu nie).

i taki Long też jest wygodniejszy w operowaniu tym. Pozniej ktos wpadnie na genialny pomysl, ze może zamiast UUID to moze byc po prostu string i pozniej sie okazuje, ze we wszystkich aplikacjach mamy stringi... i dostajemy id "324325345" ...

0

Pozniej ktos wpadnie na genialny pomysl, ze może zamiast UUID to moze byc po prostu string

I niech później zmienia typ kolumny w bazie danych. Dużo zdrowia życzę. UUID jako typ bazodanowy ma inaczej zakładane indeksy.

0

btw. jak trzymacie te uuidy dla equalsow, hashcodow w bazie np. postgresie, ktory wspiera typ uuid?

0

Moze i lekki odkop, ale mysle ze adekwatne do tematu..

Problem polega na tym ze Spring Data nie potrafi znalezc Customera po prawidlowym id

Mam jakas tam encje sobie:


@Value
@EqualsAndHashCode(of = "id", callSuper = false)
@NoArgsConstructor(force = true)
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class Customer extends AbstractAggregateRoot<Customer> implements UserDetails {

    UUID id = UUID.randomUUID();
    String firstName, lastName;
    CustomerData data;
    Address address;

repo:


public interface CustomerRepository extends MongoRepository<Customer, UUID> {}

controller:


@RestController
@RequiredArgsConstructor
@RequestMapping("/customers")
class CustomerController {


    private final CustomerRepository repository;
    
    @GetMapping("/{id}")
    Customer getCustomer(@PathVariable UUID id) {
        return repository.findById(id).orElseThrow(IllegalAccessError::new);
    }
}

generalnie jak uzywam String dla Id, to mongo i tam sobie zrobi te ObjectId, ale teraz chcialem sprobowac z UUID = UUID.randomUUID();
niestety nie znajduje mi customera. co jest nie tak? probowalem juz odbierac @PathVariable jako String i robic UUID.fromString(id); ale dalej nic :/

why is it like dat?

0

A nie brakuje Ci przypadkiem annotacji @ID nad polem ID?

0
podroznik napisał(a):

A nie brakuje Ci przypadkiem annotacji @ID nad polem ID?

adnotacja nie pomaga.

0

A czy przypadkiem spring data mongo nie próbuje ci serializowac tego UUIDa jako zwykły obiekt i wychodzą krzaki? Zobacz co siedzi w bazie, jak na moje UUID jako string jest OK, jeżeli koniecznie chcesz mieć obiekt typu UUID to dorzuć do springa konwerter, który będzie to zamieniał na stringa pod spodem

0

Jesli chodzi o to co zostaje zapisywanie to jest to np: LUUID("674c9081-41b2-f688-d8a3-d4f121bf94a7")
W jaki sposob mialby wygladac ten konwerter?

0

a próbowałeś z @RequestParam ?

0
Pablitto77 napisał(a):

a próbowałeś z @RequestParam ?

a co mialoby to zmienic? problem nie lezy w tym jak sobie przesle id na serwer, bo debugujac id jest poprane, tylko w tym ze spring data nie potrafi jakos tego wyszukac :/

0

Oznacz kolumnę zawierającą UUID jako:

@Column(columnDefinition = "BINARY(16)", length = 16)

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