26 Ağustos 2022 Cuma

JPA IDENTITY identifier GeneratorType - Veri tabanına Özeldir

Giriş
Açıklaması şöyle. Bu seçenekte dikkatli olmak lazım, çünkü yavaş kalabilir. Bu konuyla ilgili bir yazı burada.
The IDENTITY option simply allows the database to generate a unique primary key for your application. No sequence or table is used to maintain the primary key information, but instead, the database will just pick an appropriate, unique number for Hibernate to assign to the primary key of the entity. With MySQL, the first lowest numbered primary key available in the table in question is chosen, although this behavior may differ from database to database.
Hibernate
Açıklaması şöyle. Yani IDENTITY eğer Hibernate ile batch insert yapılıyorsa iyi değil.
The problem with the IDENTITY entity identifier strategy is that it prevents Hibernate from batching INSERT statements at flush time. 
...

Kullanım
Örnek
MySql'de identity şöyle kullanılır. id sütununun auto_increment olarak yaratılması gerekir.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "id")
private Long id;
Örnek
PosgreSQL'de sütunun "Serial" olarak yaratılması gerekir. Şöyle yaparız.
CREATE TABLE employee
(
  id serial NOT NULL,
  firstname CHARACTER VARYING(20),
lastname CHARACTER VARYING(20),
birth_date DATE, cell_phone CHARACTER VARYING(15), CONSTRAINT employee_pkey PRIMARY KEY(id) )
Örnek
Elimizde şöyle bir kod olsun
@Entity
@Table(name = "entity")
data class Entity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val metadata: String,
) : TenantEntity()
PosgreSQL'de sütunu "IDENTITY" olarak ta yaratabiliriz. Şöyle yaparız. Bu durumda aslında bizim için tablo başına bir sequence oluşturuyor
CREATE TABLE IF NOT EXISTS entity
(
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  ...
)
Sequnce'ı görmek için şöyle yaparız
SELECT * FROM pg_sequence WHERE seqrelid = 'entity_id_seq'::regclass;
Sequence'a geçmek için şöyle yaparız
private const val TABLE = "entity"
private const val SEQUENCE = "${TABLE}_id_seq"

@Entity
@Table(name = TABLE)
data class Entity(
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
    @SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 50)
    @Column(name = "id")
    val id: Long? = null,
		val metadata: String,
) : TenantEntity()

JPA AUTO identifier GeneratorType - Varsayılan Yöntem

Giriş
Açıklaması şöyle. Yani aslında JPA ile gelen SEQUENCE veya TABLE yöntemlerinden birisini seçiyor.
The AUTO generation strategy is the default, and this setting simply chooses the primary key generation strategy that is the default for the database in question, which quite typically is IDENTITY, although it might be TABLE or SEQUENCE depending upon how the database is configured. The AUTO strategy is typically recommended, as it makes your code and your applications most portable.
Ancak Hibernate ile burada bir problem var. Açıklaması şöyle. Yani mümkünse SEQUENCE kullanmaya çalışır. Ancak SEQUENCE yoksa TABLE yöntemini kullanır
According to the developer’s manual, if we use an ID type different from UUID (such as Long, Integer, etc.) and set the strategy to AUTO, Hibernate will do the following (since version 5.0):

 - Try to use the SEQUENCE ID generation strategy
- If sequences are not supported (i.e., we use MySQL), it will use TABLE (or IDENTITY, prior to Hibernate 5.0) strategy for ID generation
Hibernate için bu tablonun ismi hibernate_sequence.
Örnek
Post nesnesini kaydederken çıktısı şöyle. Burada hibernate_sequence tablosu üzeriden bir sürü işlem yapılıyor
@Entity
@Table(name = "post")
public class Post {
 
    @Id
    @GeneratedValue(strategy=GenerationType.TABLE)
    private Long id;
}   

SELECT tbl.next_val FROM hibernate_sequences tbl
WHERE tbl.sequence_name=default
FOR UPDATE
 
INSERT INTO hibernate_sequences (sequence_name, next_val)
VALUES (default, 1)
 
UPDATE hibernate_sequences SET next_val=2
WHERE next_val=1 AND sequence_name=default
 
SELECT tbl.next_val FROM hibernate_sequences tbl
WHERE tbl.sequence_name=default
FOR UPDATE
 
UPDATE hibernate_sequences SET next_val=3 
WHERE next_val=2 AND sequence_name=default

 
INSERT INTO post (id) values (1, 2)
Alan Tipi
Eğer bu seçeneği kullanıyorsak alan tipinin primitive bir tip yerine (long, int vs.) wrapper bir tip olmasına (Long, Integer vs.) dikkat etmek gerekir. Açıklaması şöyle
... it is recommended to use object types instead of primitives .... There is no way of distinguishing if the entity is new or pre existing with a primitive identifier.
Örnek - Kullanmayın
Şöyle yaparız. Bu yöntemin TABLE'a dönüşebilme ihtimali var ve performansı düşürebiliyor. Açıklaması yukarıda var
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "room_id")
private Integer id;
Örnek - Kullanın
Şöyle yaparız
@Id
@GeneratedValue(strategy= GenerationType.AUTO, generator="native")
@GenericGenerator(name = "native", strategy = "native")
private Long id;



17 Ağustos 2022 Çarşamba

Method Signature

Checked Exception
Açıklaması şöyle. Yani throws imzanın bir parçası değil. Kalıtan sınıfta Exception 
1. kaldırılabilir veya 
2. daha özelleştirilebilir. 
3. Ancak daha genel yapılamaz.
You can definitely have more precise or no exception, but you cannot have a more general one
Örnek 
Şöyle yaparız
interface A {
  void meth() throws IOException;
}

class B implements A {
  @Override
  void meth() throws FileNotFoundException { } // compiles fine
}

class C implements A  {
  @Override
  void meth() { } // compiles fine
}

class D implements A  {
  @Override
  void meth() throws Exception { } // compile error
}
Örnek 
Metodun imzasındaki exception'ı değiştirmek için şöyle yaparız.
interface A {
  void meth() throws IOException;
}

interface B implements A {
  void meth() throws FileNotFoundException; //Change signature
}

class C implements B  {
  @Override
  void meth() throws FileNotFoundException { } //need to use FileNotFoundException
}
Örnek
Metodu imzasında exception yoksa ancak biz checked exception fırlatmak istiyorsak şöyle yaparız
public class AnyThrow {

  public static void throwUnchecked(Throwable e) {
    AnyThrow.<RuntimeException>throwAny(e);
  }

  @SuppressWarnings("unchecked")
  private static <E extends Throwable> void throwAny(Throwable e) throws E {
    throw (E)e;
  }
}
Açıklaması şöyle
The trick relies on throwUnchecked "lying" to the compiler that the type E is RuntimeException with its call to throwAny. Since throwAny is declared as throws E, the compiler thinks that particular call can just throw RuntimeException. Of course, the trick is made possible by throwAny arbitrarily declaring E and blindly casting to it, allowing the caller to decide what its argument is cast to - terrible design when coding sanely. At runtime, E is erased and has no meaning.

As you noted, doing such a thing is a huge hack and you should document its use very well.
Kullanmak için şöyle yaparız
public void getSomething(){
  AnyThrow.throwUnchecked(new IOException(...));
}


15 Ağustos 2022 Pazartesi

java.time API ile Test

Giriş
4 tane yöntem var. Bunlar şöyle
1. Wrap calls to the java.time API now() methods into a custom static class
2. Use an injectable custom DateTime service.
3. Inject a subclass of java.time.Clock.
4. Intercept static calls to the now() methods with a mocking framework.
1. Static Wrapper yöntemi
Elimizde şöyle bir kod olsun. Bu kod test edebilmek içn setFixed() metodu sağlıyor. Bence çok iyi bir yöntem değil.
public class DateTimeWrapper {
 
  private static LocalDateTime instant; 
  // For testing only
  public static void setFixed(Instant instant) {
    instant = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"));
  }
 
  public static LocalDateTime currentDateTime() {
    if (instant != null) {
      return instant;
    } else {
      return LocalDateTime.now();
    }
   }
 }
Bu kodun biraz daha gelişmiş hali şöyle
public class DateTimeWrapper {
 
  private static LocalDateTime instant;
  private static Duration offset;
 
  public static void setFixed(Instant instant) {
    DateTimeWrapper.instant = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"));
    offset = null;
  }
 
  public static void setOffset(Duration duration) {
    offset = duration;
    instant = null;
  }
 
  public static LocalDateTime currentDateTime() {
    if (instant != null) {
      return instant;
    } else if (offset != null) {
      return LocalDateTime.now().plus(offset);
    } else {
      return LocalDateTime.now();
    }
  }
}
2: Custom Mutable DateTimeService Yöntemi

Örnek - Spring
Şöyle yaparız. Burada test için kendi LocalDateTime nesnemizi dönüyoruz.
public interface DateTimeService {
  LocalDateTime currentLocalDateTime();
}

@Profile("!test")
@Service
public class DateTimeServiceImpl implements DateTimeService {
  @Override
  public LocalDateTime currentLocalDateTime(){
    return LocalDateTime.now();
  }
}

// The test implementation contains the same logic as the static wrapper
@Profile("test")
@Service
public class MutableDateTimeService implements DateTimeService {
 
  @Override
  public LocalDateTime currentLocalDateTime() {
    //...
  }
}
Testte şöyle yaparız
@SpringBootTest
@Import(MutableDateTimeService.class)
@ActiveProfiles("test")
public class DateTimeServiceIntegrationTest {

}
3: java.time.Clock Instance Kullanmak
Burada test için kendi Clock nesnemizi dönüyoruz. 
Örnek - Spring
Elimizde şöyle bir kod olsun
@Autowired
Clock clock;

public ExecutionResult runBatchWithClock() {
  LocalDateTime started = LocalDateTime.now(clock);
}
Şöyle yaparız
@Configuration
public class TestConfig {
  @Bean
  Clock fixedClock(){
    return Clock.fixed(Instant.from(someDateTime), ZoneId.systemDefault());}
  }
}
Örnek
Test için elimizde şöyle bir kod olsun
@Slf4j
@Configuration
public class ClockConfig {
  @Value("${time-travel.instant:null}")
  private String timeTravelInstant;
  @Value("${time-travel.zone:null}")
  private String timeTravelZone;
  //@Bean
  //public Clock clock() {
  //  return Clock.system(ZoneOffset.UTC);
  //}
  @Bean
  @ConditionalOnProperty(value = "time-travel.enabled", 
                        havingValue = "false", 
                        matchIfMissing = true)
  public Clock defaultClock() {
    log.info("Using system default zone clock");
    return Clock.systemDefaultZone();
  }
  //@Bean
  //public Clock clock() {
  //  return Clock.fixed(Instant.parse("2022-09-01T12:00:00.00Z"), 
                         ZoneId.of("Europe/Warsaw"));
  //}
  @Bean
  @ConditionalOnProperty(value = "time-travel.enabled", havingValue = "true")
  public Clock clock() {
    log.info("Using fixed clock {} {}", timeTravelInstant, timeTravelZone);
    return Clock.fixed(Instant.parse(timeTravelInstant), ZoneId.of(timeTravelZone));
  }
}
Fixed clock için şöyle yaparız
time-travel.enabled=true
time-travel.instant=2022-09-01T12:00:00.00Z
time-travel.zone=UTC
4. Mockito Kullanmak
Şöyle yaparız. Bence en kolayı bu
@Test
void runMockedDateTimeWithFixedTime() {
  try (MockedStatic<LocalDateTime> mockedStatic=Mockito.mockStatic(LocalDateTime.class)) {
    mockedStatic.when(() -> LocalDateTime.now(ArgumentMatchers.any(Clock.class)))
                .thenReturn(fixedLocalDateTime);

    var result = service.runBatchWithClock();
    assertThat(result.started()).isEqualTo(result.finished());
  }

  // notice that the static mocking is only in effect within the above try block, 
  // which is of course how we would want it.
  assertThat(LocalDateTime.now()
              .getYear())
    .isGreaterThanOrEqualTo(2022);
}




11 Ağustos 2022 Perşembe

GoF - Strategy Örüntüsü - Functional Java

Giriş
Strategy arayüzü yerine Functional Programming'den gelen closure da kullanılabilir. Açıklaması şöyle.
Some patterns became anti-pattern or irrelevant (e.g. the strategy pattern lost all value after Java 8 due to the Supplier interface)
Ancak arada önemli bir fark var. Strategy arayüzü tek bir metoddan ibaret olmayabilir. Bu durumda closure yetersiz kalıyor.

Örnek - Sadece Consumer
Şöyle yaparız
private HashMap<String, Consumer> map = new HashMap<>();

public Demo() {
  map.put("csv", response -> doDownloadCsv());
  map.put("excel", response -> doDownloadExcel());
  map.put("txt", response -> doDownloadTxt());
}

@GetMapping("/exportOrderRecords")
public void downloadFile(User user, HttpServletResponse response) {

  String fileType = user.getFileType();
  Consumer consumer = map.get(fileType);
  if (consumer != null) {
    consumer.accept(response);
  } else {
    doDownloadCsv();
  }
}
Örnek - 
Şöyle yaparız
interface Picker<T> {
  T pick(CameraControls settings, LightMeter meter);
}

@lombok.Getter
@lombok.RequiredArgsConstructor
enum Mode {

    MANUAL(Picker::apertureFixed, Picker::shutterFixed),
    APERTURE_PRIORITY(Picker::apertureFixed, Picker::pickShutter),
    SHUTTER_PRIORITY(Picker::pickAperture, Picker::shutterFixed);

    private final Picker<Aperture> aperturePicker;
    private final Picker<Shutter> shutterPicker;
}

@lombok.Value
class CameraControls {

    Mode mode;

     Aperture pickAperture(LightMeter meter) {
        return mode.getAperturePicker().pick(...);
    }

    Shutter pickShutter(LightMeter meter) {
        return mode.getShutterPicker().pick(...);
    }
}

8 Ağustos 2022 Pazartesi

SimpleTimeZone Sınıfı

Giriş
Açıklaması şöyle
The SimpleTimeZone class is a subclass of the TimeZone class meant for the Gregorian calendar. This class considers the start and end rules for daylight saving time schedules.

3 Ağustos 2022 Çarşamba

Lombok @Accessors Anotasyonu - @Getter ve @Setter'ları Fluent Yapar

Giriş
Şu satırı dahil ederiz
import lombok.experimental.Accessors;
chain Alanı
Setter'lar void yerine this döner.
Örnek
Şöyle yaparız
@Data @Accessors(chain = true, fluent = true) public class Student { private String firstName; private String lastName; private String studentId; private Integer year; private List<Integer> marks; public Student(@NonNull String firstName) { this.firstName = firstName; this.studentId = generateStudentId(firstName); } private String generateStudentId(String firstName) { return firstName + new Random().nextInt(1_000); } } Student student = new Student("John").lastName("Doe").year(2);
fluentAlanı
Getter ve Setter'lar getX(), setX() yerine direkt alan ismini kullanırlar. Yani sadece x()