25 octombrie 2025

Training Java performance - Ziua 1 (JPA)

Hibernate - performanta down, magie

Orice @Entity trebuie sa aiba @Id (Integer, Long sau String - cheie naturala, ideal)

@OneToMany(mappedBy="numele companiei in cealalta clasa")

@Inheritance(strategy=TABLE_PER_CLASS, SINGLE_TABLE, JOINED)

Exemplu: 2 tabele Pisica/miaunat & Caine/latrat
SINGLE_TABLE: simplu, unele coloane sunt degeaba, nu poti pune NOT NULL pe coloanele specific; se foloseste daca dif dintre clase sunt minime; @DiscriminatorValue & @DiscriminatorColumn
TABLE_PER_CLASS (2 tabele): nu poti face select pe Animal; faci UNION (scump)
JOINED (3 tabele): join la orice query (ca sa incluzi si Animal) ---> nu se foloseste in practica

PARTITION BY - sparge datele in datafile separate (dupa an sau luna de obicei);
se face dupa cum vei face cautarea
daca cauti dupa altceva, merge rau
- nu ocupa spatiu in plus, precum indecsii

INDEX e ceva complementar (~ hashMap); prea multi afecteaza performanta

Employee si EmployeeDetails - pot avea shared primary key (FK al lui Details sa fie si PK)
- datele masive pot fi tinute in alt loc daca nu sunt frecventate des

Evitare referinte reciproce Employee si EmployeeDetails - depinde de caz - daca selectezi details si vrei si employee-ul initial, pui
altfel @OneToOne pt details, in Employee; daca modifici unul poti uita de celalalt

@ManyToMany
@JoinTable

@Enumerated - cand campul este un enum

ProjectType { LIB("L") } - in db ajunge "L", nu 0 sau LIB

XML/JSON - salvat ca CLOB (character large object)
- editare xml din clob risca stricaciuni
- poti selecta bucati din XML cu instr speciale Oracle
- poate fi prea mare, ocupa discul -- OOME (out of memory error)
Trebuie citit/scris in BD via fisier (cu InputStream/OutputStream)
Best practice: nu incarcat in BD, ci arhivat si salvat pe disc (ftp sau pe acelasi disc), cu referinta in BD;

like pe coloana CLOB: dureaza f mult pt ca CLOB nu e indexabil

https://github.com/victorrentea/jpa/tree/systematic25
run StartDatabase, JpaApplication
JpaPlayground - cu Lombok
fol. EntityManager in loc de Repository (EE)
em.persist(new Teacher());

@Transactional in Spring & Jakarta

Creare 2 entitati inrudite: trebuie legate both-way - daca uiti?
Relatiile bidirectionale sunt greu de intretinut.

Design mai bun: getter sa fie unmodifiable - blochezi get/add; faci o metoda de add in care legi dublu;
in cealalta clasa setter-ul e de tip default (nivel de package) - sa nu poti accesa set din afara pachetului

@OneToOne (cascade = CascadeType.ALL)
- TeacherDetails nu are ref la Teacher

CascadeType.ALL intre parinte si copiii exclusivi ai lui (creare, stergere automata)

campuri List vs Set
- ordonare cu @OrderBy("type ASC, value desc") pt List, sortare din query JPA
@OrderColumn(name = "INDEX") - salveaza in DB ordinea manual setata de user in UI
altfel ordinea nu e garantata

hashCode/equals pe:
- id?
- campuri?
- id + campuri? - situatie campuri identice si unul are id=null; ambele circula
=> nu impl hashCode/equals pe entitati, facem propriul equals; mai tricky la HashSet

@NotNull - previne sa inserezi prin java (Jakarta validation la persist)
nullable = false - previne sa inserezi "pe sub mana" -- folosit in POC, nu in proiecte mature (nu creezi DB din java); pot ramane out of sync

@AssertTrue pe metoda ~ @NotNull cand validarea e doar pe campul unui copil

@OneToMany poate fi def unidirectional, cu @JoinColumn(name = "loanerId")

constructor in entity:
protected cu constructor gol - ca sa nu planga Hibernate
public cu parametri - este cel relevant

getter/setters: nu neaparat necesare, Hibernate foloseste reflectie pt a popula campurile

--> BalanceEntityListener ??
--> @Transient in BalanceEntity?

http://annas-archive.org/ -- book "sql performance explained"

URL BD: jdbc:mssql ----> jdbc:p6spy:mssql , cu username, parola, dependency la p6spy
-> alternativa mai buna la show-sql=true

@OneToMany
N+1 queries = 1 pt parinte, N pt copii (loading lazy info)
fetch = FetchType.LAZY by default (doar pe colectii); la fel si pt @ManyToMany
EAGER daca eviti N+1 queries -> incarca tot dinainte - a nu se folosi aproape niciodata
@BatchSize(size=20) - este Hibernate specific - incarca entitati inrudite in calupuri

Ia toate info odata (pt @ManyToOne)
select p from Person p
LEFT JOIN FETCH p.children
LEFT JOIN FETCH p.country ---- nu este colectie

@ManyToOne - si asta aduce N+1 queries

fetch = EAGER by default

entityManager.createQuery("SELECT pFROM errorProne"); // app porneste, vezi bug-ul tarziu
@NamedQuery (name = "", query = "") ; // detecteaza devreme, nu compileaza

Sub-select: select doar anumite campuri
@Subselect("query...")
public record.... - in spring

@NamedNativeQuery - pt EE
(query= "", resultClass = , resultSetMapping = )
- cu COALESCE
- to VIEW ----> declari un nou @Entity in Java cu @Table("VIEW_NAME")

Teste pe query-uri

FetchGraph - sa incarci partial entitati

Paginare: ORDER BY, LIMIT __, OFFSET __
- in BD, in Java sau pe client

Problema: daca se insereaza/sterge in timpul frunzaririi (cached)
problema: el face select tot dar iti da numai ce ceri
Nu poti pagina in DB daca faci FETCH, pt ca se strica cardinalitatea
pe doua directii: iese produs cartezian

LEFT JOIN FETCH ALL PROPERTIES - ia in cascada

@ManyToOne private Country country; ==> private Long countryId; // pastreaza FK
-> pune numeric/string ref in loc de object reference

entityManager
.createNamedQuery(..)
.setFirstResult(0) // offset
.setMaxResults(2) // limit
.getResultList();

@ElementCollection -- entitati-copii fara id

Cross join: N x N x N results

Intrerupere query in timpul rularii - exista api de jdbc "abort" - nu din JPA, doar JDBC (statement.cancel())
sau: timeout in JPA

13 septembrie 2025

Jupiter Parameterized Test

import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

public class ServerStreamingInputValidationTest extends AbstractTest {

@ParameterizedTest
@MethodSource("testData")
void testBlockingInputValidation(WithdrawRequest request, Status.Code code) {
var ex = Assertions.assertThrows(StatusRuntimeException.class, () -> this.blockingStub.withdraw(request).hasNext());
Assertions.assertEquals(code, ex.getStatus().getCode());
}

@ParameterizedTest
@MethodSource("testData")
void testAsyncInputValidation(WithdrawRequest request, Status.Code code) {
var observer = new ResponseObserver<Money>();
this.asyncStub.withdraw(request, observer);
observer.await();

Assertions.assertTrue(observer.getItems().isEmpty());
Assertions.assertNotNull(observer.getThrowable());
Assertions.assertEquals(code, ((StatusRuntimeException) observer.getThrowable()).getStatus().getCode());
}

private Stream<Arguments> testData() {
return Stream.of(
// input, expectation
Arguments.of(WithdrawRequest.newBuilder().setAccountNumber(11).setAmount(10).build(), Status.Code.INVALID_ARGUMENT),
Arguments.of(WithdrawRequest.newBuilder().setAccountNumber(1).setAmount(17).build(), Status.Code.INVALID_ARGUMENT),
Arguments.of(WithdrawRequest.newBuilder().setAccountNumber(1).setAmount(120).build(), Status.Code.FAILED_PRECONDITION)
);
}
}

07 august 2025

Kill process in Windows & Linux

Windows:

> netstat -ano | findstr :8080

> taskkill PID <pid> /F


Linux:

> sudo lsof -i 8080

> sudo kill -9 <pid>

02 august 2025

Proto vs Json performance test

 


public class PerformanceTest {
private static final Logger LOGGER = LoggerFactory.getLogger(PerformanceTest.class);
private static final ObjectMapper MAPPER = new ObjectMapper();

public static void main(String[] args) throws Exception {
var protoPerson = Person.newBuilder()
.setLastName(
"T")
.setAge(
32)
.setEmail(
"t@email.com")
.setEmployed(
true)
.setSalary(
123004.5)
.setBankAccountNumber(
38495959093040944L)
.setBalance(-
100)
.build();
LOGGER.info("how many bytes protoPerson has? {}", protoPerson.toByteArray().length); // 41

var jsonPerson = new JsonPerson("T", 32, "t@email.com", true, 123004.5, 38495959093040944L, -100);
var bytes = MAPPER.writeValueAsBytes(jsonPerson);
LOGGER.info("how many bytes jsonPerson has? {}", bytes.length); // 130 = 3x more!

for (int i=0; i<5; i++) { // first run to be ignored (warmup JVM)
runTest("json", () -> json(jsonPerson));
runTest("proto", () -> proto(protoPerson)); // 7x faster!
}
}

private static void runTest(String testName, Runnable runnable) {
var start = System.currentTimeMillis();
for (int i=0; i<5_000_000; i++) {
runnable.run();
}
var end = System.currentTimeMillis();
LOGGER.info("time taken for {} = {} ms", testName, (end - start));
}

private static void proto(Person person) {
try {
var bytes = person.toByteArray();
Person.
parseFrom(bytes);
}
catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
}

private static void json(JsonPerson person) {
try {
var bytes = MAPPER.writeValueAsBytes(person);
MAPPER.readValue(bytes, JsonPerson.class);
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
}

04 decembrie 2024

Implementarea unui Rate Limiter

public class RateLimiter {
private static final int ONE_SECOND = 1000;
private final Map<String, List<Long>> requests;
private boolean enabled = false;
private int requestsPerSecond;

public RateLimiter(Environment environment) {
requests = new HashMap<>();
if (environment.getProperty("enable.rate.limiter") != null) {
this.enabled = Boolean.parseBoolean(environment.getProperty("enable.rate.limiter"));
}
if (this.enabled) {
String property = environment.getProperty("max.requests.per.user.per.second");
Assert.notNull(property, "Max requests per second not defined!");
this.requestsPerSecond = Integer.parseInt(property);
}
}

public boolean allows(String ipAddress) {
if (!enabled) {
return true;
}

if (!requests.containsKey(ipAddress)) {
requests.put(ipAddress, new ArrayList<>());
}

Long now = System.currentTimeMillis();
cleanup(ipAddress, now);
requests.get(ipAddress).add(now);

return requests.get(ipAddress).size() <= requestsPerSecond;
}

private void cleanup(String ipAddress, Long now) {
List<Long> markedForDeletion = new ArrayList<>();
for (Long timestamp : requests.get(ipAddress)) {
if (now - timestamp > ONE_SECOND) {
markedForDeletion.add(timestamp);
}
}
requests.get(ipAddress).removeAll(markedForDeletion);
}
}