Geleceği Yazanlar

Java Veri Tabanı Bağlantısının Evrimi

Günümüzde Java ile veri tabanı uygulaması yazarken kullanabileceğiniz pek çok farklı alternatif mevcut. Bu alternatiflerden en çok kullanılanlarından biri de şüphesiz JPA. İnternette JPA hakkında pek çok makale ve tutorial bulmanız mümkün. Bu yazıda benim amacı size JPA öğretmek değil de, JPA’dan önce kullanılan diğer teknolojilere bir göz atarak JPA’nın bizi nereden nereye taşıdığını vurgulama...

Zeliha Canan Durmuş |

27.08.2019

Günümüzde Java ile veri tabanı uygulaması yazarken kullanabileceğiniz pek çok farklı alternatif mevcut. Bu alternatiflerden en çok kullanılanlarından biri de şüphesiz JPA. İnternette JPA hakkında pek çok makale ve tutorial bulmanız mümkün. Bu yazıda benim amacı size JPA öğretmek değil de, JPA’dan önce kullanılan diğer teknolojilere bir göz atarak JPA’nın bizi nereden nereye taşıdığını vurgulamak, böylelikle hangi probleme çözüm için geliştirildiğini anlamanıza yardımcı olmak.

Bunun için basit bir problemin farklı yöntemler ile çözümüne bir göz atalım. Diyelim ki kütüphanemizdeki tüm kitapların listesini yönetmek için kullandığımız bir uygulamamız olsun. Bunun için veri tabanımızda Books isimli bir tablomuz olsun ve uygulamamız aracılığı ile bu tablo üzerinde çeşitli CRUD (Create, Read, Update, Delete) operasyonları yürütelim.

Aşağıdaki ilk örneğimiz, Java’nın temel veri tabanı bağlantı modeli olan JDBC’nin kullanıldığı basit bir Java sınıfı. 

package com.lets.learn.jdbc;

//STEP 1 --> import libraries
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import com.in28minutes.database.databasedemo.entity.Book;
 
public class JDBCExample {	
	// Driver
     static final String dbURL = "jdbc:oracle:thin:@localhost:1521:testDB"; 
     
    // Connection URL
     static final String jdbcDriver = "oracle.jdbc.driver.OracleDriver";
     
 	// Database credentials
 	static final String userName = "testDbUser";
 	static final String password = "password1";
 
    public static void main(String[] args) {
    	Book book = new Book(1, "J.K. ROWLING", "Harry Potter");
    	updateBook(book);
    	Book myBook = queryBook(1);
    }
    
    private static void updateBook(Book book) {
    	// Define variables
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
 
        try {
        	 // Step 2 --> Load and register the driver
			Class.forName(jdbcDriver);
			
			// Step 3 --> Create connection
			connection = DriverManager.getConnection(dbURL, userName, password); 
			
			// Step 4 --> Create statement
			statement = connection.prepareStatement("UPDATE BOOKS SET AUTHOR=?,TITLE=? WHERE ID=?");
			statement.setString(1, book.getAuthor());
			statement.setString(2, book.getTitle());
			statement.setInt(3, book.getId());
	
			
			// Step 5 --> Execute query
			// preparedStatement'a �evir, parametrelerin nas�l set edildi�ini g�ster
			resultSet = statement.executeQuery();
			
			// Step 6 --> Retrieve data
			// noop here
			
			// Step 7: Clean-up the environment
			resultSet.close();
			statement.close();
			connection.close();
			
        } catch (SQLException se) {
			// Handle errors for JDBC
			se.printStackTrace();
		} catch (ClassNotFoundException e) {
			// Handle errors for Class.forName
			e.printStackTrace();
		} finally {
			// use finally block to close resources
			try {
				if (statement != null) {
					statement.close();
				}
				if (connection != null) {
					connection.close();
				}
			} catch (SQLException se) {
				se.printStackTrace();
			} 
		} 
    }
    
    private static Book queryBook(int id) {
    	// Define variables
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
 
        Book book = null;
        try {
        	 // Step 2 --> Load and register the driver
			Class.forName(jdbcDriver);
			
			// Step 3 --> Create connection
			connection = DriverManager.getConnection(dbURL, userName, password); 
			
			// Step 4 --> Create statement
			statement = connection.prepareStatement("SELECT * FROM BOOKS WHERE ID=?");
			statement.setInt(1, id);
			
			// Step 5 --> Execute query
			resultSet = statement.executeQuery();
			
			// Step 6 --> Retrieve data
			
			if (resultSet.next()) {
				// populate a java object
				book = new Book();
				book.setId(resultSet.getInt("ID"));
                book.setTitle(resultSet.getString("TITLE"));
                book.setAuthor(resultSet.getString("AUTHOR"));
            }
			
			// Step 7: Clean-up the environment
			resultSet.close();
			statement.close();
			connection.close();
	
        } catch (SQLException se) {
			// Handle errors for JDBC
			se.printStackTrace();
		} catch (ClassNotFoundException e) {
			// Handle errors for Class.forName
			e.printStackTrace();
		} finally {
			// use finally block to close resources
			try {
				if (statement != null) {
					statement.close();
				}
				if (connection != null) {
					connection.close();
				}
			} catch (SQLException se) {
				se.printStackTrace();
			} 
		} 
        return book;
    }
    
}

İlk bakışta gözünüze çarpacağı gibi basit bir güncelleme ve basit bir sorgulama operasyonu için bile oldukça uzun bir kod yazılmış. Peki, bu kod parçacığı neler yapıyor?

  • Veri tabanı bağlantısını kuruyor
  • Veri tabanı üzerinde bir veriyi güncelliyor ya da bir veri sorguluyor
  • Veri tabanı bağlantısı için kullandığı tüm nesnelerin hayat döngülerini yönetiyor
  • Bağlantı kurarken ya da kapatırken oluşabilecek hata durumlarını yönetiyor

Görebileceğiniz gibi JDBC ile program yazmak her CRUD işleminde tekrar tekrar yapmak istemeyeceğiniz rutin bazı adımlar içeriyor. Eski yazılımcıların çok iyi hatırlayabileceği şekilde bu adımlardan bağlantı yönetimi ile ilgili olanlarda yapılabilecek hatalar zaman zaman tüm uygulamanın çökmesine neden olabilen Connection Pool sorunlarına neden olabiliyordu. Bu nedenle bağlantıyı kuran ve kapatan kod blokları bir kez kendi fonksiyonları içinde tanımlanır, daha sonra bağlantı kurmak ve kapamak gereken yerlerde bu fonksiyonlar çağrılarak kod tekrarının önüne geçilmeye çalışılırdı.

Bütün bunlardan başka olarak yukarıdaki kodda yaptığımız bir şey daha var. O da Java’daki veri modelimiz ile veri tabanındaki veri modelimiz arasında sürekli bir dönüştürme yapmak. Bu dönüştürme veriyi okurken de, yazarken de zorunlu hale geliyor. Sorgulama için veri tabanına giderken sorgumuzda çeşitli parametreler kullanıyoruz, bu parametrelerin değerleri programımızdaki başka Java nesnelerinden geliyor. Bu değerleri ilgili Java nesnelerinden okuyarak sorgudaki yerlerine yerleştiriyoruz. Sorguyu çalıştırdıktan sonra elde ettiğimiz veri seti içindeki değerleri buradan okuyup tekrar Java nesnelerindeki yerlerine yazıyoruz. Bu sorunun çözümü bir önceki kadar kolaylıkla gelmiyor. Burada imdadımıza Spring JDBC yetişiyor

Dünya çapında binlerce kullanıcısı olan Spring, Spring Data JDBC ile bu soruna güzel bir çözüm getiriyor. Aşağıdaki örnekte aynı sorunun Spring Data JDBC ile çözümünü görüyorsunuz.

package com.lets.learn.jdbc;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

public class SpringJDBCExample {
	// Driver
	static final String dbURL = "jdbc:oracle:thin:@localhost:1521:testDB";

	// Connection URL
	static final String jdbcDriver = "oracle.jdbc.driver.OracleDriver";

	// Database credentials
	static final String userName = "testDbUser";
	static final String password = "password1";

	private static DriverManagerDataSource dataSource = new DriverManagerDataSource();
	private static JdbcTemplate jdbcTemplate = new JdbcTemplate(getDataSource());
		
	public static void main(String[] args) throws Exception {

		Book book = new Book(1, "J.K. ROWLING", "Harry Potter");

		updateBook(book);
		Book myBook = queryBook(1);

	}
	
	public static void updateBook(Book book) {
		jdbcTemplate.update("UPDATE BOOKS SET AUTHOR=?,TITLE=? WHERE ID=?", book.getAuthor(), book.getTitle(), book.getId());
	}
	
	public static Book queryBook(int id) {
		return jdbcTemplate.queryForObject(
			    "SELECT * FROM BOOKS WHERE ID = ?", new Object[] { id }, new BookRowMapper());
	}
	
	
    private static DriverManagerDataSource getDataSource() {
        
        dataSource.setDriverClassName(jdbcDriver);
        dataSource.setUrl(dbURL);
        dataSource.setUsername(userName);
        dataSource.setPassword(password);
 
        return dataSource;
    }

}
package com.lets.learn.jdbc;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.jdbc.core.RowMapper;

public class BookRowMapper implements RowMapper<Book> {
   @Override
    public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
        Book book = new Book();
 
        book.setId(rs.getInt("ID"));
        book.setTitle(rs.getString("TITLE"));
        book.setAuthor(rs.getString("AUTHOR"));
 
        return book;
    }
}

Kodu incelediğinizde birkaç farkın hemen gözünüze çarpacağını düşünüyorum. Bunlar neler?

  • Her işlemden önce bağlantı kurmak ve işlem bitince bağlantıyı kapatmak görevi artık programcıdan alınıp Spring Context’ine devredilmiş durumda. Kod tekrarı minimize edilmiş
  • Sorgu sonucunda sonuç setinde dönen nesneler Java nesnelerine elle dönüştürülmemiş, bunun yerine RowMapper yapısı kullanılmış. Bu sayede dönüşümün nasıl yapılacağı bir kez tanımlanmış ve kod tekrarının önüne geçilmiş.

Fakat halen çözülemeyen bir sorun var, sorgularımız hala Java sınıflarının içinde. Veri tabanındaki tablolarımızda bir değişiklik olduğunda bu sorguları bulmak ve güncellemek büyük ölçekli uygulamalar için oldukça büyük bir maliyet doğruyor. İşte burada da JPA devreye giriyor.

Aynı sorunun JPA ile çözümüne bir göz atalım.

package com.lets.learn.jpa;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import com.lets.learn.jpa.entity.Book;
import com.lets.learn.jpa.repository.BookJpaRepository;

@SpringBootApplication
public class JpaExample implements CommandLineRunner {

	@Autowired
	BookJpaRepository repository;

	public static void main(String[] args) {
		SpringApplication.run(JpaDemoApplication.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
		
		Book firstBook =repository.findById(10001);
		
		// insert new book 
		repository.insert(new Book(1 ,"Hobbit", "J.R.R Tolkien"));
		
		// update a book 
		repository.update(new Book(10003, "A Tale Of Two Cities", "Charles Dickens"));
		
		// delete a book
		repository.deleteById(10002);

		//get all books
		repository.findAll();
	}
}

package com.lets.learn.jpa.repository;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.transaction.Transactional;

import org.springframework.stereotype.Repository;

import com.lets.learn.jpa.entity.Book;


@Repository
@Transactional
public class BookJpaRepository {

	// connect to the database
	@PersistenceContext
	EntityManager entityManager;

	public List<Book> findAll() {
		TypedQuery<Book> namedQuery = entityManager.createNamedQuery("find_all_books", Book.class);
		return namedQuery.getResultList();
	}

	public Book findById(int id) {
		return entityManager.find(Book.class, id);// JPA
	}

	public Book update(Book book) {
		return entityManager.merge(book);
	}

	public Book insert(Book book) {
		return entityManager.merge(book);
	}

	public void deleteById(int id) {
		Book book = findById(id);
		entityManager.remove(book);
	}
}

package com.lets.learn.jpa.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.NamedQuery;

@Entity
@NamedQuery(name="find_all_books", query="select b from Book b")
public class Book {

	@Id
	@GeneratedValue
	private int id;

	private String title;
	private String author;

	public Book() {

	}

	public Book(int id, String title, String author) {
		super();
		this.id = id;
		this.title = title;
		this.author = author;

	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getAuthor() {
		return author;
	}

	public void setAuthor(String author) {
		this.author = author;
	}
}

Yukarıdaki örnekte görebileceğiniz gibi JPA :

  • CRUD operasyonları için daha sade, daha anlaşılır kodlar yazmamıza olanak sağlıyor
  • Sık kullanılan işlemler için hazırda sunduğu yapılar ile tekerleği yeniden icat etmemizin önüne geçiyor
  • Java nesnesi ile veri tabanı tabloları arasında sürekli bir dönüşüm ihtiyacını ortadan kaldırıyor
  • Veri tabanındaki model değiştiğinde, değişikliği Java katmanına yansıtmayı kolaylaştırıyor.

Umarım bu kısa makale Java ile veri tabanı uygulaması geliştirme yolunda kat ettiğimiz yolu anlamanıza yardımcı olmuştur.