İlk olarak Spring Initializr sitesine gidiyoruz.
Burada proje yapılandırması için seçimler yapıyoruz:
- Project: Maven
- Language: Java
- Spring Boot: (Önerilen en güncel versiyon)
"Add Dependencies" butonuna basarak aşağıdaki bağımlılıkları ekliyoruz:
- Spring Boot DevTools
- Lombok
- Spring Web
- Spring Data JPA
- Validation
- PostgreSQL Driver
-
Spring Boot DevTools (Developer Tools):
Hızlı geliştirme için otomatik uygulama restart ve LiveReload sağlar. -
Lombok (Developer Tools):
@Getter,@Settergibi anotasyonlarla Java’da tekrarlayan kod yazımını azaltır. -
Spring Web (Web):
Web uygulamaları ve REST API geliştirmek için kullanılır. İçinde Spring MVC ve gömülü Tomcat sunucusu bulunur. -
Spring Data JPA (SQL):
SQL yazmadan Java sınıfları ile veritabanı işlemleri yapmamızı sağlar. Arkada Hibernate çalışır. -
Validation (I/O):
Giriş verilerini doğrulamak için kullanılır (@NotNull,@Size, vs.). -
PostgreSQL Driver (SQL):
PostgreSQL veritabanına Java ile bağlantı kurmak için gerekli JDBC sürücüsüdür.
Not: Bu bağımlılıklar
pom.xmldosyası içinde bulunur.
Projeyi düzenli ve anlaşılır hale getirmek için katmanlı mimari kullanıyoruz.
Bu mimaride her katman tek bir işten sorumludur.
- Entities Layer
- Data Access Layer
- Business Layer
- Controller(Web API) Layer
- Veritabanındaki tabloların Java sınıflarındaki karşılıkları burada bulunur.
- Sınıflar
@Entityanotasyonu ile işaretlenir. - Bu katmanda sadece veri yapısı tutulur, iş mantığı (business logic) olmaz.
Örnek:
@Entity
public class User {
private Long id;
private String name;
// getter ve setter metodları
}- Veritabanı işlemleri bu katmanda gerçekleştirilir.
- Repository veya DAO (Data Access Object) sınıfları burada yer alır.
- Genellikle Spring Data JPA kullanılıyorsa
@Repositoryanotasyonu ile işaretlenir.
Örnek:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Özelleştirilmiş sorgular yazılabilir
}- Uygulamanın iş kuralları (business logic) burada bulunur.
- Genellikle
@Serviceanotasyonu kullanılır. - Veri erişimi ve iş mantığı burada birleştirilir.
Örnek:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User createUser(User user) {
return userRepository.save(user);
}
}- Kullanıcıdan gelen HTTP isteklerini karşılar ve cevaplar.
- Genellikle
@RestControlleranotasyonu kullanılır. - Servis katmanıyla iletişim kurar ve sonuçları döner.
Örnek:
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User createdUser = userService.createUser(user);
return ResponseEntity.ok(createdUser);
}
}Her katmanda iki alt paket bulunur:
| Paket | Açıklama |
|---|---|
| abstract | Interface veya abstract class'lar bulunur. Sadece sözleşme veya şablon tanımlar. |
| concrete | Interface veya abstract class'ların gerçek implementasyonları bulunur. |
- 🔄 İleride farklı bir implementasyon gerektiğinde sadece concrete kısmı değiştirmek yeterli olur.
- 🧪 Test yazarken kolayca mock sınıflar oluşturulabilir.
- 🔗 Bağımlılıklar azalır, proje daha esnek hale gelir.
- 📈 Dependency Injection (Bağımlılık Enjeksiyonu) prensibine uygun çalışır.
ModelMapper, Java Spring Boot projelerinde kullanılan bir nesne dönüştürme (object mapping) kütüphanesidir. Temel amacı, bir nesnedeki verileri başka bir nesneye otomatik ve kolay şekilde kopyalamaktır. Özellikle DTO (Data Transfer Object) ile Entity sınıfları arasında dönüşüm yaparken oldukça kullanışlıdır.
- 🔄 Entity – DTO dönüşümünü kolaylaştırır.
- ✂️ Gereksiz getter-setter, manuel kopyalama kodlarını azaltır.
- 📦 Kodun daha temiz, okunabilir ve sürdürülebilir olmasını sağlar.
- 🔍 Alan isimleri aynıysa otomatik eşleştirme yapar.
- 🧩 Gerekirse özel eşleştirme (custom mapping) yapılabilir.
UserEntitysınıfındanUserDTOsınıfına veri aktarımı- Formdan gelen
CreateUserRequestnesnesiniUserEntity'ye dönüştürme işlemi
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.1</version>
</dependency>Spring'de ModelMapper'ı kullanmadan önce bir bean olarak tanımlanması gerekir.
Spring'de bir sınıfı @Bean ile tanımlamak, bu nesnenin Spring tarafından yönetilmesini sağlar. Böylece her yerde otomatik olarak (@Autowired ile) kullanılabilir hale gelir.
- Tek bir
ModelMappernesnesi kullanılır (singleton) - Her yerde yeniden oluşturmak gerekmez
- Özelleştirmeler merkezi olarak yapılabilir
Application.java ya da bir @Configuration sınıfı içerisine aşağıdaki kod eklenir:
@Bean
public ModelMapper getModelMapper() {
return new ModelMapper();
}Artık ModelMapper sınıfını aşağıdaki gibi projede kullanabilirsin:
@Autowired
private ModelMapper modelMapper;
public UserDTO convertToDto(UserEntity user) {
return modelMapper.map(user, UserDTO.class);
}🔁 Bu yapı sayesinde kod tekrarı azalır, temiz ve sürdürülebilir bir mimari elde edilir.
Spring Boot projelerinde business/requests ve business/responses paketlerinin kullanılmasının temel amacı, katmanlı mimaride veri akışını net bir şekilde ayırmak ve kontrol altına almaktır. Bu yapı, özellikle kurumsal ve büyük projelerde kodun okunabilirliğini, sürdürülebilirliğini ve güvenliğini artırır.
requests: Dışarıdan (örneğin bir kullanıcıdan veya başka bir API'den) gelen verileri temsil eder. Örnek:CreateBrandRequest,UpdateBrandRequestresponses: Kullanıcıya ya da başka servislere geri döndürülen verileri temsil eder. Örnek:GetAllBrandsResponse,GetByIdBrandResponse
Bu ayrım, hem frontend hem de backend için veri kontrolünü kolaylaştırır.
Entity sınıflarınız (örneğin Brand) veritabanıyla birebir eşleşir. Ancak her alanı kullanıcıya göstermek ya da dışarıdan almak istemeyebilirsin.
Örneğin:
// Entity'de olabilir:
private Long id;
private String name;
private LocalDateTime createdAt;
private String createdBy;Ama bir GetAllBrandsResponse'da sadece şunu döndürmek isteyebilirsin:
private Long id;
private String name;Bu sayede kullanıcıya gereksiz ya da hassas veri sunulmamış olur.
Swagger gibi araçlar sayesinde, CreateBrandRequest veya GetAllBrandsResponse gibi sınıflar otomatik dökümantasyon sağlar.
Ayrı sınıflar sayesinde API daha anlaşılır olur.
Yeni alanlar eklendiğinde ya da farklı işlemler (create, update vs.) için özel alanlar gerektiğinde, entity'yi değiştirmek yerine sadece ilgili request/response sınıfını düzenlemen yeterlidir.
Bu yapının avantajı, az önce incelediğimiz ModelMapperService ile de ortaya çıkar:
Brand brand = modelMapperService.forRequest().map(createBrandRequest, Brand.class);
GetAllBrandsResponse dto = modelMapperService.forResponse().map(brand, GetAllBrandsResponse.class);| Amaç | Açıklama |
|---|---|
| Katman ayrımı | Veri giriş (request) ve çıkışını (response) ayırmak |
| Güvenlik | Gereksiz/hassas verileri gizlemek |
| Temizlik | Kodun okunabilirliğini ve bakımını kolaylaştırmak |
| Dökümantasyon | Swagger gibi araçlarla uyumlu, açık API tasarımı |
| Esneklik | Farklı operasyonlar için özelleştirilmiş veri modelleri |
Gerçek hayattaki ilişkiyi düşün:
- Arabanın modeli: Toyota Corolla
- Markası: Toyota
Ancak:
- Bir arabanın markası tek başına yetersiz bir bilgidir. "Toyota" marka diyerek arabayı tarif edemezsin.
- Ama "Corolla" modeli (ve bu modelin zaten bir markası var) arabayı tanımlamak için yeterlidir.
@ManyToOne
@JoinColumn(name = "model_id")
private Model model;Model zaten içinde şu şekilde Brand bilgisi barındırır:
@ManyToOne
@JoinColumn(name = "brand_id")
private Brand brand;Yani dolaylı olarak:
Car → Model → Brand
Şeklinde bir zincir olur. Böylece her Car nesnesi hem model hem de marka bilgisine sahiptir ama veritabanında fazladan foreign key tutmadan bu ilişki kurulmuş olur. Bu da doğru veri modelleme açısından en iyi yaklaşımdır.
Cardoğrudan birModel’e bağlıdır.Modelise birBrand’e bağlıdır.- Bu yapı hem sade hem de gerçek dünyayı en doğru şekilde yansıtır.
ERROR: update or delete on table "brands" violates foreign key constraint "fk..." on table "models"
Detail: Key (id)=(1) is still referenced from table "models".brands tablosundaki bir kaydı silmeye çalışıyorsun. Ancak bu kaydı models tablosundaki kayıtlar hâlâ kullanıyor. Veritabanı, referanslı veri kaybolmasın diye silme işlemine izin vermiyor.
modelRepository.deleteAllByBrandId(1);
brandRepository.deleteById(1);
deleteAllByBrandId(int id)metodunuModelRepositoryiçerisine yazmalısın.
Marka silinince, bağlı modellerin de otomatik silinmesini istiyorsan:
@OneToMany(mappedBy = "brand", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Model> models;Bu ilişkiyi
Brandentity’sinde tanımla.
Silme işleminden önce marka ile ilişkili model var mı diye kontrol et:
if (modelRepository.existsByBrandId(brandId)) {
throw new BusinessException("Bu markaya bağlı modeller olduğu için silinemez.");
}
existsByBrandId(int id)methodunuModelRepositoryiçinde tanımla.
| İhtiyacın | Kullanman Gereken |
|---|---|
| Modeller de silinsin | CascadeType.ALL ve orphanRemoval |
| Önce modeller silinip sonra marka silinsin | Servis katmanında önce modelleri sil |
| Silmeye izin verilmesin | Kullanıcıya uyarı ver, silmeyi engelle |
İsteğine göre yukarıdaki üç yöntemden birini uygulayabilirsin.


