Bu makalede, Android platformunda Java programlama dili ve yerel SQLite veritabanı altyapısı kullanılarak geliştirilen Kütüphane Üye ve Yönetim Sistemi'nin mimarisi, veri tabanı göç (migration) stratejileri, rol tabanlı kimlik doğrulama süreçleri ve gelişmiş kitap kiralama/rezervasyon iş mantığı incelenmektedir. Çalışmada, kütüphane otomasyonunun ilk aşama özellikleri ile projeye sonradan kazandırılan gelişmiş modüller ve arayüz modernizasyonu karşılaştırmalı olarak ele alınmaktadır.
1. GİRİŞ VE PROJE ÖZETİ
Mobil tabanlı kütüphane otomasyon sistemleri, fiziksel kütüphane işlemlerinin dijitalleştirilmesinde önemli bir rol oynamaktadır. Bu çalışmada geliştirilen kütüphane uygulaması, kullanıcıların kitap aramasını, ödünç almasını, kitapları 2 günlük sürelerle rezerve etmesini ve okudukları kitapları puanlamasını sağlarken; kütüphane yöneticilerine (Admin) ise kitap ekleme, silme, düzenleme ve detaylı istatistikleri görüntüleme yetkisi sunmaktadır.
Uygulamanın teknik omurgasını oluşturan ana bileşenler şunlardır:
- SQLite Veritabanı Katmanı (kutuphanedb.java): Tüm veri depolama ve işleme sorgularının yönetildiği katman.
- Kimlik Doğrulama Katmanı (MainActivity.java & yeniUye.java): Kayıt olma ve çok kriterli giriş kontrol mekanizmaları.
- Kitap Listeleme ve Arama Modülü (kitapListe.java & KitapAdapter.java): Sesli arama, dinamik kategori filtreleri, durum süzgeçleri ve sıralama işlevleri.
- Profilim Modülü (profilim.java): Oturum bilgilerinin güncellenmesi ve şifre görünürlüğü yönetimi.
- E-posta Entegrasyonu (EmailSender.java): Unutulan şifrelerin SMTP aracılığıyla güvenli bir şekilde kullanıcı e-postasına gönderilmesi.
2. VERİTABANI MİMARİSİ VE VERİ GÖÇÜ (MIGRATION) STRATEJİSİ
Uygulama, verilerini cihazın yerel depolama alanında kutuphane.db adıyla saklar. Veritabanının ilk kurulumunda varsayılan kitap ve kullanıcı verileri otomatik olarak yüklenir.
2.1 SQLite Şema Tasarımı
Uygulama iki temel tablo üzerinden veri ilişkilerini kurar:
- uyeler: Kullanıcıların kişisel bilgilerini, kimlik doğrulama verilerini ve rollerini saklar.
- kitaplar: Kitap künyelerini, ödünç/rezervasyon durumlarını ve derecelendirme istatistiklerini tutar.
// kutuphanedb.java - Tablo Oluşturma Şemaları
@Override
public void onCreate(SQLiteDatabase db) {
// Kitaplar Tablosu Oluşturulması
db.execSQL("CREATE TABLE kitaplar (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT," +
"kitapadi TEXT," +
"yazar TEXT," +
"sayfa TEXT," +
"kategori TEXT," +
"durum TEXT DEFAULT 'rafta'," + // rafta, odunc_verildi, rezerve_edildi
"odunc_alan TEXT," +
"iade_tarihi TEXT," +
"puan_toplam REAL DEFAULT 0," +
"yorum_sayisi INTEGER DEFAULT 0," +
"resim_yolu TEXT)");
// Üyeler Tablosu Oluşturulması
db.execSQL("CREATE TABLE uyeler (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT," +
"ad_soyad TEXT," +
"mail TEXT," +
"sifre TEXT," +
"okul TEXT," +
"sinif TEXT," +
"ogr_no TEXT," +
"rol TEXT DEFAULT 'Uye')");
// Sisteme İlk Kurulumda Varsayılan Verileri Yükle
varsayilanVerileriYukle(db);
}
2.2 Veri Göçü (Data Migration) Yönetimi
Mobil uygulamalarda sürüm güncellemeleri sırasında veritabanı şemasının değişmesi (yeni sütun veya tablolar eklenmesi) durumunda, kullanıcının mevcut verilerini kaybetmemesi kritik önem taşır. Bu projede, veritabanı sürümü DB_VERSIYON değeri yükseltildiğinde çalışan özel bir veri aktarım algoritması (migrateDataAndReplaceDb) geliştirilmiştir.
// kutuphanedb.java - Veri Göçü ve Sürüm Kontrolü
private void migrateDataAndReplaceDb(File dbFile, android.content.SharedPreferences prefs) {
ArrayList<ContentValues> oldUsers = new ArrayList<>();
ArrayList<ContentValues> activeLoans = new ArrayList<>();
SQLiteDatabase oldDb = null;
try {
oldDb = SQLiteDatabase.openDatabase(dbFile.getPath(), null, SQLiteDatabase.OPEN_READONLY);
// 1. Mevcut Kayıtlı Üyeleri Yedekle (Varsayılan test hesapları hariç)
Cursor uCursor = oldDb.rawQuery("SELECT * FROM uyeler WHERE mail != ? AND mail != ?", new String[]{"admin@kutuphane.com", "uye@kutuphane.com"});
if (uCursor != null) {
int colAd = uCursor.getColumnIndex("ad_soyad");
int colMail = uCursor.getColumnIndex("mail");
int colSifre = uCursor.getColumnIndex("sifre");
int colOkul = uCursor.getColumnIndex("okul");
int colSinif = uCursor.getColumnIndex("sinif");
int colOgrNo = uCursor.getColumnIndex("ogr_no");
int colRol = uCursor.getColumnIndex("rol");
while (uCursor.moveToNext()) {
ContentValues cv = new ContentValues();
if (colAd != -1) cv.put("ad_soyad", uCursor.getString(colAd));
if (colMail != -1) cv.put("mail", uCursor.getString(colMail));
if (colSifre != -1) cv.put("sifre", uCursor.getString(colSifre));
if (colOkul != -1) cv.put("okul", uCursor.getString(colOkul));
if (colSinif != -1) cv.put("sinif", uCursor.getString(colSinif));
if (colOgrNo != -1) cv.put("ogr_no", uCursor.getString(colOgrNo));
if (colRol != -1) cv.put("rol", uCursor.getString(colRol));
oldUsers.add(cv);
}
uCursor.close();
}
// 2. Aktif Ödünç/Rezervasyon Durumlarını ve Kitap Derecelendirmelerini Yedekle
Cursor kCursor = oldDb.rawQuery("SELECT * FROM kitaplar WHERE (durum IS NOT NULL AND durum != 'rafta') OR puan_toplam > 0 OR yorum_sayisi > 0 OR (resim_yolu IS NOT NULL AND resim_yolu != '')", null);
if (kCursor != null) {
int colAd = kCursor.getColumnIndex("kitapadi");
int colDurum = kCursor.getColumnIndex("durum");
int colOduncAlan = kCursor.getColumnIndex("odunc_alan");
int colIade = kCursor.getColumnIndex("iade_tarihi");
int colPuan = kCursor.getColumnIndex("puan_toplam");
int colYorum = kCursor.getColumnIndex("yorum_sayisi");
int colResim = kCursor.getColumnIndex("resim_yolu");
while (kCursor.moveToNext()) {
ContentValues cv = new ContentValues();
if (colAd != -1) cv.put("kitapadi", kCursor.getString(colAd));
if (colDurum != -1) cv.put("durum", kCursor.getString(colDurum));
if (colOduncAlan != -1) cv.put("odunc_alan", kCursor.getString(colOduncAlan));
if (colIade != -1) cv.put("iade_tarihi", kCursor.getString(colIade));
if (colPuan != -1) cv.put("puan_toplam", kCursor.getFloat(colPuan));
if (colYorum != -1) cv.put("yorum_sayisi", kCursor.getInt(colYorum));
if (colResim != -1) cv.put("resim_yolu", kCursor.getString(colResim));
activeLoans.add(cv);
}
kCursor.close();
}
} catch (Exception e) {
Log.e("kutuphanedb", "Eski veritabanı okunurken hata oluştu: " + e.getMessage());
} finally {
if (oldDb != null && oldDb.isOpen()) oldDb.close();
}
// Eski DB dosyasını sil ve assets dizinindeki yeni şemaya sahip güncel veritabanını kopyala
if (dbFile.exists()) dbFile.delete();
copyDatabaseFromAssets(dbFile);
// 3. Yedeklenen Verileri Yeni Veritabanına Yaz
SQLiteDatabase newDb = null;
try {
newDb = SQLiteDatabase.openDatabase(dbFile.getPath(), null, SQLiteDatabase.OPEN_READWRITE);
// Üyeleri geri yükle
for (ContentValues cv : oldUsers) {
String email = cv.getAsString("mail");
Cursor c = newDb.rawQuery("SELECT id FROM uyeler WHERE mail = ?", new String[]{email});
if (c.getCount() == 0) {
newDb.insert("uyeler", null, cv);
}
c.close();
}
// Aktif ödünç/rezervasyonları ve puanları geri yükle
for (ContentValues cv : activeLoans) {
String kitapAdi = cv.getAsString("kitapadi");
ContentValues updateCv = new ContentValues();
if (cv.containsKey("durum")) updateCv.put("durum", cv.getAsString("durum"));
if (cv.containsKey("odunc_alan")) updateCv.put("odunc_alan", cv.getAsString("odunc_alan"));
if (cv.containsKey("iade_tarihi")) updateCv.put("iade_tarihi", cv.getAsString("iade_tarihi"));
if (cv.containsKey("puan_toplam")) updateCv.put("puan_toplam", cv.getAsFloat("puan_toplam"));
if (cv.containsKey("yorum_sayisi")) updateCv.put("yorum_sayisi", cv.getAsInteger("yorum_sayisi"));
if (cv.containsKey("resim_yolu")) updateCv.put("resim_yolu", cv.getAsString("resim_yolu"));
newDb.update("kitaplar", updateCv, "kitapadi = ?", new String[]{kitapAdi});
}
newDb.setVersion(DB_VERSIYON);
prefs.edit().putInt("db_version", DB_VERSIYON).apply();
} catch (Exception e) {
Log.e("kutuphanedb", "Yedeklenen veriler geri yazılırken hata: " + e.getMessage());
} finally {
if (newDb != null && newDb.isOpen()) newDb.close();
}
}
3. ROL TABANLI KİMLİK DOĞRULAMA VE ESNEK GİRİŞ MEKANİZMASI
Projenin ilk aşamasında sadece kullanıcı adı sorgulayan basit giriş yapısı, kayıt olma ekranıyla tutarsızlığa yol açmaktaydı. Yapılan geliştirmeyle kayıt formuna detaylı alanlar eklenmiş, giriş yapısı ise esnetilmiştir.
3.1 Çoklu Alan Giriş Algoritması (Flexible Auth)
Kullanıcılar artık sisteme giriş yaparken Ad Soyad, kayıtlı E-Posta adresi ya da okul tarafından verilen Öğrenci Numarasını tek bir giriş kutusuna yazarak giriş yapabilmektedirler.
// kutuphanedb.java - Esnek Kimlik Doğrulama Sorgusu
public boolean uyeKontrol(String ad, String sifre) {
SQLiteDatabase db = this.getReadableDatabase();
// ad parametresi; ad_soyad, mail veya ogr_no ile eşleşebilir
Cursor cursor = db.rawQuery(
"SELECT * FROM uyeler WHERE (ad_soyad=? OR mail=? OR ogr_no=?) AND sifre=?",
new String[]{ad, ad, ad, sifre}
);
boolean result = cursor.getCount() > 0;
cursor.close();
return result;
}
3.2 Şifre Sıfırlama ve SMTP E-posta Entegrasyonu
Kullanıcıların şifrelerini unutmaları durumunda kayıtlı e-posta adreslerine şifrelerinin gönderilmesi sağlanmıştır. Bu işlem için JavaMail API kullanılarak arka planda (Thread) SMTP üzerinden e-posta gönderimi gerçekleştirilir.
// EmailSender.java - Arka Planda E-posta Gönderme İşlemi
public static void sendPasswordEmail(Activity activity, String recipientEmail, String userPassword, EmailSendCallback callback) {
String subject = "Kütüphane Sistemi - Şifre Hatırlatma";
String messageBody = "Merhaba,\n\nKütüphane Sistemine kayıtlı şifreniz aşağıdadır:\n\nŞifreniz: "
+ userPassword + "\n\nBu e-posta otomatik olarak gönderilmiştir.";
new Thread(() -> {
boolean success = false;
Exception exception = null;
Properties props = new Properties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.host", "smtp.gmail.com");
props.put("mail.smtp.port", "587");
Session session = Session.getInstance(props, new javax.mail.Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(SENDER_EMAIL, SENDER_PASSWORD);
}
});
try {
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(SENDER_EMAIL));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(recipientEmail));
message.setSubject(subject);
message.setText(messageBody);
Transport.send(message);
success = true;
} catch (Exception e) {
exception = e;
}
final boolean finalSuccess = success;
final Exception finalException = exception;
activity.runOnUiThread(() -> {
if (finalSuccess) {
callback.onSuccess();
} else {
callback.onFailure(finalException);
}
});
}).start();
}
4. REZERVASYON VE KİTAP ÖDÜNÇ ALMA İŞ MANTIĞI
Kütüphane sisteminde kitapların durumları dinamik olarak yönetilir. Bir kitap kütüphane rafında olabileceği gibi (rafta), bir kullanıcı tarafından ödünç alınmış (odunc_verildi) ya da geçici olarak ayırtılmış (rezerve_edildi) olabilir.
4.1 İki Günlük Dinamik Rezervasyon Sistemi
Kitap rezervasyonu, ödünç almaktan farklı olarak kitabı 2 gün boyunca başka kullanıcıların erişimine kapatır. 2 günün sonunda kitap ödünç alınmazsa veya rezervasyon iptal edilirse tekrar rafa döner.
// kutuphanedb.java - Kitap Rezervasyon Metodu
public void kitapRezerveEt(String kitapAdi, String rezerveAlanUye) {
try {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues cv = new ContentValues();
cv.put("durum", "rezerve_edildi");
cv.put("odunc_alan", rezerveAlanUye);
// Bitiş tarihini günün milisaniyesine 2 günlük süre (2 * 24 * 60 * 60 * 1000) ekleyerek buluyoruz
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd/MM/yyyy", java.util.Locale.getDefault());
cv.put("iade_tarihi", sdf.format(new java.util.Date(System.currentTimeMillis() + (2L * 24 * 60 * 60 * 1000))));
db.update("kitaplar", cv, "kitapadi=?", new String[]{kitapAdi});
} catch (Exception e) {
e.printStackTrace();
}
}
4.2 Güvenlik ve Çift Rezervasyon Engelleme
Bir üye rezervasyon yaptığında, diğer üyelerin bu kitabı ödünç alması veya tekrar rezerve etmesi sistem seviyesinde kısıtlanır.
// kitapListe.java - Kitaba Tıklama Dialog Yönetimi ve Yetkilendirme
listViewKitaplar.setOnItemClickListener((parent, view, position, id) -> {
Kitap seciliKitap = adapter.getItem(position);
String kitapAdi = seciliKitap.getAd();
String durum = seciliKitap.getDurum();
String oduncAlan = seciliKitap.getOduncAlan();
AlertDialog.Builder builder = new AlertDialog.Builder(kitapListe.this);
builder.setTitle("Kitap İşlemi: " + kitapAdi);
if ("rezerve_edildi".equals(durum)) {
if (kullaniciAdi.equals(oduncAlan)) {
// Rezervasyonu yapan kişi kitabı ödünç alabilir veya iptal edebilir
builder.setMessage("Bu kitabı rezerve etmişsiniz. Ödünç almak ister misiniz?");
builder.setPositiveButton("Ödünç Al (15 Gün)", (dialog, which) -> {
db.kitapOduncVer(kitapAdi, kullaniciAdi);
listeyiDoldur();
});
builder.setNeutralButton("İptal Et", (dialog, which) -> {
db.kitapRezervasyonIptal(kitapAdi);
listeyiDoldur();
});
} else {
// Farklı bir üye tıklarsa işlem yapamaz
builder.setMessage("Bu kitap başka bir üye (" + oduncAlan + ") adına rezerve edilmiştir.");
builder.setPositiveButton("Tamam", (dialog, which) -> dialog.dismiss());
}
}
builder.show();
});
5. İLERİ DÜZEY FİLTRELEME, SIRALAMA VE TESLİM GÜNÜ ALGORİTMALARI
Uygulamanın kitap yönetim paneli, kullanıcıların aradıkları kitapları en hızlı şekilde bulabilmesi için kategori sayaçları ve dinamik sıralama algoritmalarıyla donatılmıştır.
5.1 Teslim Tarihi Kontrol Algoritması
İade tarihi geçmiş veya iade günü bugün olan kitapları tespit etmek için zaman kısımlarını (saat, dakika, saniye) sıfırlayarak sadece gün bazlı karşılaştırma yapan bir algoritma kullanılmıştır.
// kitapListe.java - Gün Bazlı Karşılaştırma Fonksiyonu
private boolean isTeslimTarihiGelen(String iadeTarihi) {
if (iadeTarihi == null || iadeTarihi.isEmpty()) return false;
try {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd/MM/yyyy", java.util.Locale.getDefault());
java.util.Date due = sdf.parse(iadeTarihi);
java.util.Date today = new java.util.Date();
// Bugünün saat/dakika/saniye kısımlarını sıfırla
java.util.Calendar calToday = java.util.Calendar.getInstance();
calToday.setTime(today);
calToday.set(java.util.Calendar.HOUR_OF_DAY, 0);
calToday.set(java.util.Calendar.MINUTE, 0);
calToday.set(java.util.Calendar.SECOND, 0);
calToday.set(java.util.Calendar.MILLISECOND, 0);
// Teslim tarihinin saat/dakika/saniye kısımlarını sıfırla
java.util.Calendar calDue = java.util.Calendar.getInstance();
calDue.setTime(due);
calDue.set(java.util.Calendar.HOUR_OF_DAY, 0);
calDue.set(java.util.Calendar.MINUTE, 0);
calDue.set(java.util.Calendar.SECOND, 0);
calDue.set(java.util.Calendar.MILLISECOND, 0);
// Bugün, teslim tarihine eşit veya sonrasındaysa true döner
return calToday.after(calDue) || calToday.equals(calDue);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
5.2 Kategorilere Göre Dinamik Liste ve Sıralama
Kitap listesinde alfabetik (A-Z, Z-A), sayfa sayısı (çoktan aza, azdan çoka) ve kütüphane puanına göre sıralama seçenekleri Collections.sort metodu kullanılarak çözülmüştür.
// kitapListe.java - Dinamik Sıralama Algoritması
if ("İsim: A - Z".equals(seciliSiralaFiltre)) {
Collections.sort(kitaplar, (o1, o2) -> o1.getAd().compareToIgnoreCase(o2.getAd()));
} else if ("Sayfa: Çoktan Aza".equals(seciliSiralaFiltre)) {
Collections.sort(kitaplar, (o1, o2) -> {
int s1 = 0, s2 = 0;
try { s1 = Integer.parseInt(o1.getSayfa()); } catch (Exception ignore) {}
try { s2 = Integer.parseInt(o2.getSayfa()); } catch (Exception ignore) {}
return Integer.compare(s2, s1);
});
} else if ("Puan: En Yüksek".equals(seciliSiralaFiltre)) {
Collections.sort(kitaplar, (o1, o2) -> Float.compare(o2.getPuan(), o1.getPuan()));
}
6. UI/UX TASARIM İLKELERİ VE XML MODERNİZASYONU
Uygulamanın görsel tasarımı, klasik Android tasarımlarından sıyrılarak modern kart (card view) ve yuvarlatılmış giriş alanları üzerine inşa edilmiştir.
6.1 Form Arka Plan Kart Tasarımı
Aşağıdaki XML kodu, formların arkasında yer alan beyaz, gölgeli ve yuvarlatılmış köşeli kart yapısını tanımlar.
<!-- bg_card.xml - Özelleştirilmiş Kart Şekli -->
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- Kart iç rengi beyaz -->
<solid android:color="#FFFFFF" />
<!-- Köşe yuvarlatma yarıçapı -->
<corners android:radius="16dp" />
<!-- Kartın sınır çizgisi rengi -->
<stroke
android:width="1dp"
android:color="#E2E8F0" />
</shape>
6.2 Giriş Alanı Tasarımı
Kullanıcının veri girdiği alanlar (EditText) için odaklanma (focus) ve normal durumları ayırt eden dinamik kenarlıklı bir yapı oluşturulmuştur.
<!-- bg_custom_input.xml - EditText Kenarlık Yapısı -->
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Giriş alanına tıklandığında (Odaklanıldığında) -->
<item android:state_focused="true">
<shape android:shape="rectangle">
<solid android:color="#F8FAFC"/>
<stroke android:width="2dp" android:color="#3B82F6"/>
<corners android:radius="10dp"/>
</shape>
</item>
<!-- Normal Durumda -->
<item>
<shape android:shape="rectangle">
<solid android:color="#FFFFFF"/>
<stroke android:width="1dp" android:color="#CBD5E1"/>
<corners android:radius="10dp"/>
</shape>
</item>
</selector>
7. SONUÇ
Bu çalışma kapsamında geliştirilen Kütüphane Üye ve Yönetim Sistemi, SQLite veritabanı yapısıyla veri tutarlılığını garanti ederken; veri göçü algoritmasıyla veri kaybının önüne geçmektedir. Çok kriterli üye giriş altyapısı, 2 günlük akıllı rezervasyon iş mantığı, teslim tarihi geçikme kontrolleri, SMTP e-posta iletişimi ve modern UI/UX kart tasarımları sayesinde okul kütüphaneleri ve yerel kütüphaneler için ölçeklenebilir, modern ve güvenilir bir mobil çözüm sunmaktadır.