10 Ocak 2023 Salı

SpringData JpaRepository.findById - Anti-Pattern Kullanmayın

Giriş
@Id olarak işaretli alana göre arama yapar. 

Problem1 - findById() metodunun Döngü İçinde Kullanılması
Eğer birden fazla nesne almak istiyorsak findAllById() kullanılır
Örnek
Şöyle yaparız
@Service
public class UserService {

  @Autowired
  private UserRepository userRepository;

  public List<User> getUsersByIds(List<Long> ids) {
    return userRepository.findAllById(ids);
  }
}
Üretilen SQL şöyle
SELECT * FROM User user WHERE user.id IN :ids
Problem 2
Açıklaması şöyle. Yani findById() lazy değildir. OneToMany, ManyToOne gibi ilişkilerde diğer taraftaki nesneleri de getirir.
Another issue with findById is that it can lead to the creation of many unnecessary objects. Each time you call findById, Spring Data JPA creates a new entity object, even if the entity is already in the persistence context. This can lead to a significant increase in memory usage and garbage collection overhead.
findById() yerine getReferenceById() tercih edilmeli
getReferenceById() metodunun kodu şöyle. Yeni bir proxy dönüyor
public T getReferenceById(ID id) {
    Assert.notNull(id, "The given id must not be null!");
    return this.em.getReference(this.getDomainClass(), id);
}
getReferenceById metodunun hikayesi şöyle. Yani önce getOne() -> daha sonra getById() -> daha sonra getReferenceById() haline gelmiş.
Initially, Spring Data JPA offered a getOne method that we should call in order to get an entity Proxy. But we can all agree that getOne is not very intuitive.

So, in the 2.5 version, the getOne method got deprecated in favor of the getById method alternative, that’s just as unintuitive as its previous version.

Neither getOne nor getById is self-explanatory. Without reading the underlying Spring source code or the underlying Javadoc, would you know that these are the Spring Data JPA methods to call when you need to get an entity Proxy?

Therefore, in the 2.7 version, the getById method was also deprecated, and now we have the getReferenceById method instead,...
Örnek - OneToMany İlişki
Elimizde şöyle bir kod olsun. Department nesnesi Employee nesnesine OneToMany ile bağlı. Employee nesnesi de Department nesnesine ManyToOne ile bağlı
@Entity
@Table(name = "departments")
public class Department {

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

  private String name;

  @OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
  private List<Employee> employees = new ArrayList<>();

  // getters and setters
}

@Entity
@Table(name = "employees")
public class Employee {

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

  private String name;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "department_id")
  private Department department;

  // getters and setters
}
Şöyle kullanalım
@Service
public class DepartmentService {

  @Autowired
  private DepartmentRepository departmentRepository;

  public Department getDepartmentById(Long id) {
    return departmentRepository.findById(id)
    .orElseThrow(() -> new EntityNotFoundException("Department not found with id " + id));
  }
}
Üretilen SQL şöyle
SELECT * FROM departments WHERE id = ?

SELECT * FROM employees WHERE department_id = ?
Düzeltmek için şöyle yaparız. Burada getOne() kullanılıyor ancak getReferenceById() de aynı şey zaten
@Service
public class DepartmentService {

  @Autowired
  private DepartmentRepository departmentRepository;

  public Department getDepartmentReferenceById(Long id) {
    return departmentRepository.getOne(id);
  }
}
Üretilen SQL şöyle
SELECT department FROM Department department WHERE department.id = ?
Açıklaması şöyle
Note that the actual SQL query to fetch the related Employee objects will only be executed when we access the employees property of the Department object, or call a method on one of its related employees that requires database access.
Örnek - ManyToOne İlişki
Elimizde şöyle bir kod olsun
@Entity
@Table(name = "post")
public class Post {
 
  @Id
  private Long id;
 
  private String title;
 
  @NaturalId
  private String slug;
}

@Entity
@Table(name = "post_comment")
public class PostComment {
 
  @Id
  @GeneratedValue
  private Long id;
 
  private String review;
 
  @ManyToOne(fetch = FetchType.LAZY)
  private Post post;
}
Şöyle kullanalım. Aslında yeni bir PostComment nesnesi ekliyoruz ve bunun için ilişkili olduğu Post nesnesine ihtiyaç var.
@Transactional(readOnly = true)
public class PostServiceImpl implements PostService {
 
  @Autowired
  private PostRepository postRepository;
 
  @Autowired
  private PostCommentRepository postCommentRepository;
 
  @Transactional
  public PostComment addNewPostComment(String review, Long postId) {           
    PostComment comment = new PostComment()
      .setReview(review)
      .setPost(postRepository.findById(postId)
        .orElseThrow(
                   ()-> new EntityNotFoundException(
                     String.format("Post with id [%d] was not found!", postId)
                    )
                )
            );
 
postCommentRepository.save(comment);
    return comment;
  }
}
Üretilen SQL şöyle
SELECT
  post0_.id AS id1_0_0_,
  post0_.slug AS slug2_0_0_,
  post0_.title AS title3_0_0_
FROM
  post post0_
WHERE
  post0_.id = 1
 
SELECT nextval ('hibernate_sequence')
 
INSERT INTO post_comment (
  post_id,
  review,
  id
)
VALUES (
  1,
  'Best book on JPA and Hibernate!',
  1
)
Kodu şöyle yapalım. Burada artık getReferenceById() kullanılıyor
@Transactional
public PostComment addNewPostComment(String review, Long postId) {
  PostComment comment = new PostComment()
    .setReview(review)
    .setPost(postRepository.getReferenceById(postId));
 
  postCommentRepository.save(comment);
 
  return comment;
}
Üretilen SQL şöyle
SELECT nextval ('hibernate_sequence')
 
INSERT INTO post_comment (
    post_id,
    review,
    id
)
VALUES (
    1,
    'Best book on JPA and Hibernate!',
    1
)




Hiç yorum yok:

Yorum Gönder