Android SQLite database সিরিজের প্রথম পোস্ট থেকে আশা করি SQLite সম্পর্কে ব্যাসিক কিছু আইডিয়া পাওয়া গেছে। এই পোস্টে সরাসরি চলে যাব implementation এ। ধরে নিচ্ছি আপনি আগে Android App এ SQLite ইউজ করেন নাই। তাই ORM ইউজ না করে raw SQLite ইউজ করা দেখাবো। আপনি যদি অ্যান্ড্রয়েড অ্যাপে ডেটা স্টোর করার কোনো পদ্ধতিই আগে apply না করে থাকেন তাহলে সাজেশন হচ্ছে আগে SharedPreferences এর উপর আমার ব্লগ পোস্টটি পড়ে আসেন। এতে ডেটা স্টোর করা সম্পর্কে কিছু আইডিয়া হবে। ডেটাবেজে ডেটা স্টোর করার সিসটেমগুলো হয়ত এতে বুঝতে সুবিধা হবে।
SQLite database implement করার জন্য প্রাথমিক ভাবে আমরা দুইটি ক্লাস লিখব। একটা ক্লাসে থাকবে ডেটাবেজের টেবিলগুলো বানানোর কাজ। আরেকটা ক্লাসে থাকবে টেবিলগুলোতে ডেটা রিড-রাইট করার কাজ। চাইলে সব কিছু একটা ক্লাসের মধ্যেই করা যায়। কিন্তু কোড কিছুটা ক্লিন আর ফ্রেশ রাখার জন্য আলাদা আলাদা ফাইলের ব্যবস্থা।
অ্যান্ড্রয়েড অ্যাপ ইন্সটল হবার সঙ্গে সঙ্গেই কিন্তু ডেটাবেজ তৈরি হয়ে যায় না। বরং যখন ডেটাবেজের কোনো ডেটা রিড-রাইট করার দরকার হয় তখনই database create হয়। যদি ইউজার কোনো একটা নতুন এন্ট্রি করতে চান তখন চেক করা হয় যে ডেটাবেজ অলরেডি exist কিনা? যদি ডেটাবেজ তৈরি না থাকে তাহলে ক্রিয়েট করা হয়। আর পরবর্তী সকল কাজের সময় চেক করে পাওয়া যায় যা ইতমধ্যে অমুক নামের একটা ডেটাবেজ ঐ ডিভাইসে রয়েছে। তখন আর ডেটাবেজ ক্রিয়েট হয় না। Existing database এর একটা instance রিটার্ন করা হয়।
Problem Description
আমরা চাই, একটা Student information App বানাতে। যেন যে কোনো স্কুল, কলেজ বা হোম টিউটরগণ এই অ্যাপের সাহায্যে তার স্টুডেন্টদের ইনফরমেশন সেভ করে রাখতে পারেন। চাইলে প্রয়োজন মত আপডেট-ডিলেটও করতে পারেন।
ইউজার প্রথম বারের মত আমাদের অ্যাপ ওপেন করলে Home Activity দেখতে পাবে। হোমে প্রথমত একটা FAB (Floating Action Button) ছাড়া আর কিছু থাকবে না। Activity’র মাঝে লেখা দেখা যাবে যে এখন পর্যন্ত কোনো স্টুডেন্টের এন্ট্রি এই অ্যাপে নাই। FAB-এ ক্লিক করলে একটা DialogFragment ওপেন হবে। সেখানে চারটা ইনপুট ফিল্ড থাকবে। Name, Reg No, Email ও Phone. প্রতিটি স্টুডেন্টের রেজিস্ট্রেশন নাম্বার unique হতে হবে। একই নাম্বার দিয়ে একাধিক স্টুডেন্টের এন্ট্রি দিলে error message দেখাবে। সবগুলো ইনপুট ফিল্ডে ডেটা দিয়ে সেভ করলে ঐ Activity-তে সঙ্গে সঙ্গে একটা লিস্ট আইটেম হিসাবে ঐ স্টুডেন্টের ইনফরমেশন যোগ হয়ে যাবে। লিস্ট আইটেমে ঐ স্টুডেন্টের ইনফরমেশন আপডেট ও ডিলেটের অপশন থাকবে। Edit button এ ক্লিক করলে DialogFragment এ ঐ স্টুডেন্টের ইনফরমেশনগুলো শো করবে। ভ্যালিড ডেটা দিয়ে সেভ দিলে ডেটাবেজ আপডেট হবে এবং একই সাথে লিস্টটিও আপডেট হবে।
ধরে নিচ্ছি ডেটা ইনপুট নেয়া, বাটন ক্লিক ইভেন্ট, DialogFragment এগুলো সম্পর্কে আপনার ধারণা আছে। এগুলো নিয়ে আলোচনা না করে তাই আমি সরাসরি চলে যাব ডেটাবেজ রিলেটেড কথাবার্তায়। যদি এই স্যাম্পল অ্যাপটা বানাতে কোথাও আটকে যান সেক্ষেত্রে পোস্টের শেষে থাকা GitHub লিংক থেকে full project নামিয়ে রান করে দেখতে পারেন। প্রতিটা স্টুডেন্টের এন্ট্রি হবার পর সাথে সাথেই লিস্ট আপডেট হচ্ছে। ডিলেট করলে ডেটাবেজ থেকে ডিলেট হচ্ছে একই সাথে লিস্ট থেকেও ডিলেট হচ্ছে। স্টুডেন্টের কোনো ডেটা আপডেট করলে লিস্টেও আপডেট হচ্ছে। এর জন্য Java interface ব্যবহার করেছি। ইন্টারফেসের মাধ্যমে RecyclerViewAdapter বা Home Activity এর কাছে খবর পাঠানো হচ্ছে যে লিস্ট আপডেট করা জরুরি।
Create Database and Table using SQLiteOpenHelper
প্রবলেম ডেস্ক্রিপশন থেকে বুঝা গেল আমরা Student ক্লাসের অবজেক্ট নিয়ে কাজ করব। প্রতিটা স্টুডেন্টের নাম, রেজিস্ট্রেশন নম্বর, ইমেইল আর ফোন নাম্বার আমরা ইনপুট নিব। সেগুলো ডেটাবেজে স্টোর করব। দরকারের সময় সেই ডেটার উপর বিভিন্ন অপারেশন চালাব। এজন্য শুরুতেই আমাদের একটা ডেটাবেজ বানাতে হবে। আপনি নিশ্চয়ই Android Studio-তে প্রোজেক্ট খুলে বিভিন্ন Activity তৈরি করতে পারেন। তাই আমি আর সেদিকে যাচ্ছি না। সরাসরি চলে যাব কিভাবে ডেটাবেজ তৈরি করতে হয় সেই কথায়।
এই সিরিজের প্রথম পর্বে জেনেছেন SQLite database ডেভেলপ করা হয়েছে C programming language দিয়ে। তাহলে আমরা অ্যান্ড্রয়েড অ্যাপে এটা ইউজ করার জন্যেও কি সি প্রোগ্রামিং জানা থাকা লাগবে? অবশ্যই না! একদম core এ থাকা ডেটাবেজের সি কোডের সাথে communicate করার জন্য Android SDK তে রয়েছে বেশ কিছু class. আমরা এই ক্লাসগুলোর বিভিন্ন অবজেক্ট বানিয়ে এগুলোর মেথডগুলো ইউজ করব। একদম ভিতরে কী হচ্ছে না হচ্ছে আমাদের জানার দরকার হচ্ছে না। মাঝে একটা abstraction layer রয়েছে। আমরা এই লেয়ারের সাথে জাভা কোড দিয়ে কাজ করব। এই লেয়ারটা ভিতরের সি কোডের সাথে কাজ করবে।
SQLite ডেটাবেজ তৈরির জন্য আমাদেরকে সাহায্য করবে SQLiteOpenHelper abstract ক্লাস। এটা আমাদের নিজেদের লিখা কোনো ক্লাস নয়। এটা Android SDK এর ক্লাস। শুরুতে আমরা SQLiteOpenHelper কে inherit করে একটা ক্লাস বানাব। আমাদের ক্লাসের নাম দিলাম DatabaseHelper. Inherit করলে দেখা যাবে error দিচ্ছে। Error এর লাইনে কার্সর রেখে Alt+Enter চাপলে কিছু সাজেশন আসবে। সেখানে কিছু method override করার কথা বলা থাকবে। মেথডগুলো হচ্ছে onCreate() আর onUpgrade().
অ্যাপ ইন্সটল হবার সাথে সাথেই ডেটাবেজ তৈরি হবে না। বরং যখন আমাদের অ্যাপ থেকে প্রথমবারের মত ডেটাবেজে কোনো ডেটা স্টোর করা হবে বা ডেটা সার্চ করা হবে তখন ডেটাবেজ তৈরি হবে। উপরের GIF screenshot থেকে এটা পরিষ্কার বুঝা যাচ্ছে যে অ্যাপ ওপেন করার পর প্রথমে ডেটাবেজে সার্চ করা হচ্ছে যে কোনো স্টুডেন্ট আছে কিনা। স্টুডেন্ট না পাওয়া যাওয়ায় মেসেজ দেখানো হচ্ছে যে, “Student not found”. এই সার্চিং অপারেশন চালানোর আগে একটা ডেটাবেজ দরকার। তাই এই অপারেশন এক্সিকিউট হবার আগ মুহূর্তেই আমাদের ডেটাবেজ তৈরি হয়েছে। আর এটা তৈরি করার জন্য DatabaseHelper ক্লাসের কনস্ট্রাকটর কল করার প্রয়োজন হবে। সেই constructor এর ভিতরে কল করা হবে super() মেথড।
DatabaseHelper(Context context) { super(context, "student-db", null, 1); }
এই সুপার মেথড কল দিয়ে ডেটাবেজ তৈরি করার জন্য ২ টা জিনিস provide করতে হবে। একটা ডেটাবেজের নাম, আরেকটি হচ্ছে ডেটাবেজের ভার্সন নাম্বার। প্রথমটি একটা String আর দ্বিতীয়টি একটা integer value. আমাদের ডেটাবেজের নাম দিতে চাই student-db, আর একদম শুরুর অবস্থায় ডেটাবেজের ভার্সন দিলাম 1. অ্যাপ আপডেট করলেই ডেটাবেজের এই ভার্সন কিন্তু আপডেট করতে হবে না। অ্যাপ পাবলিশড করে দেয়ার পর যদি ঐ অ্যাপের ডেটাবেজের স্ট্রাকচারে কোনো পরিবর্তন দরকার হয় তাহলে এই ডেটাবেজ ভার্সনের নাম্বার আগের চেয়ে বড় হবে।
ডেটাবেজ তো ক্রিয়েট হয়ে গেল। কিন্তু কিছু কি আছে ডেটাবেজে? ডেটাবেজে তো একটা student table থাকা উচিত। সেটা তো তৈরি করা হয় নি! স্টুডেন্ট টেবিলটা একটু কল্পনা করি। আশা করি কল্পনায় নিচের টেবিলের মত একটা টেবিল দেখা যাবে।
আমাদের ঠিক কখন এই টেবিলটা বানানো দরকার? যখন ডেটাবেজ ক্রিয়েট হল এর পরপরই! তার মানে আমরা যদি কোনো ভাবে জানতে পারি কখন ডেটাবেজ ক্রিয়েট হয়েছে বা উল্টিয়ে যদি বলি ডেটাবেজ ক্রিয়েট হলে কেউ যদি আমাদেরকে খবর দেয় তাহলে তখনই টেবিল ক্রিয়েট করব। ডেটাবেজ ক্রিয়েট হবার পরপরই আমাদের override করা onCreate() মেথডটি কল হবে। তাই যাবতীয় টেবিল তৈরির কাজগুলো onCreate() মেথডের ভিতর করতে হবে। আপনি যদি আগে MySQL এর সাথে পরিচিত হয়ে থাকেন তাহলে টুকটাক SQL query এর সাথে পরিচিত হবার কথা। ডেটাবেজে টেবিল ক্রিয়েট করার SQL টা মনে আছে? উপরে থাকা টেবিলের ছবির মত একটা টেবিল বানাবার জন্য নিচের query টি execute করতে হবে।
CREATE TABLE student(_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, registration_no INTEGER NOT NULL UNIQUE, phone TEXT, email TEXT )
এখানে student নামের একটা টেবিল বানানো হয়েছে। যার primary key হচ্ছে _id. আইডি হবে auto increment. অর্থাৎ আমরা প্রতি স্টুডেন্টের জন্য আইডি ডিফাইন করে দিব না। আইডিটা SQLite database-ই আমাদেরকে জেনারেট করে দিবে। কোনো একটা টেবিলের প্রাইমারি কী সব সময় _id রাখা উচিত। এটা mandatory না কিন্তু convention. এতে ডেটাবেজের অপারেশনগুলো তুলনামূলক faster হয়ে থাকে। টেবিলের রেজিস্ট্রেশন নাম্বারটা হবে integer আর যোগ করা হয়েছে unique constraint. টেবিলের সবগুলো কলামের type বলে দেয়া হয়েছে। কিন্তু SQLite ডিজাইন করা হয়েছে weakly typed হিসাবে। অর্থাৎ name কলামে যদি টেক্সট না দিয়ে নাম্বার আর রেজিস্ট্রেশন কলামে integer না দিয়ে যদি টেক্সট ইনসার্ট করা হয় তবুও এটা কোনো error শো করবে না। যা ইনপুট দেয়া হবে সেটাই সে রেখে দিবে। উপরের query টা onCreate() method এর ভিতরে এক্সিকিউট করা হচ্ছে এভাবেঃ
@Override public void onCreate(SQLiteDatabase db) { String CREATE_STUDENT_TABLE = "CREATE TABLE " + Config.TABLE_STUDENT + "(" + Config.COLUMN_STUDENT_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + Config.COLUMN_STUDENT_NAME + " TEXT NOT NULL, " + Config.COLUMN_STUDENT_REGISTRATION + " INTEGER NOT NULL UNIQUE, " + Config.COLUMN_STUDENT_PHONE + " TEXT, " //nullable + Config.COLUMN_STUDENT_EMAIL + " TEXT " //nullable + ")"; db.execSQL(CREATE_STUDENT_TABLE); }
টেবিল ও কলামের নামগুলো hardcoded string হিসাবে না লিখে Config নামের একটা ক্লাসের স্ট্যাটিক ডেটা মেম্বার হিসাবে রাখা হয়েছে। Config.java টা এরকমঃ
public class Config { public static final String DATABASE_NAME = "student-db"; //column names of student table public static final String TABLE_STUDENT = "student"; public static final String COLUMN_STUDENT_ID = "_id"; public static final String COLUMN_STUDENT_NAME = "name"; public static final String COLUMN_STUDENT_REGISTRATION = "registration_no"; public static final String COLUMN_STUDENT_PHONE = "phone"; public static final String COLUMN_STUDENT_EMAIL = "email"; //others for general purpose key-value pair data public static final String TITLE = "title"; public static final String CREATE_STUDENT = "create_student"; public static final String UPDATE_STUDENT = "update_student"; }
DatabaseHelper ক্লাসে override হওয়া দ্বিতীয় মেথডটি হচ্ছে onUpgrade(). আমাদের ডেটাবেজের স্ট্রাকচার চেঞ্জ হলে কী করতে হবে সেই সকল ইন্সট্রাকশন এখানে লিখতে হবে। আপাতত আমরা কোনো জটিল লজিকে যাব না। কেবল শুরু করছি তাই আমরা বলব যে যদি অমুক অমুক টেবিল ডেটাবেজে থাকে, সেগুলোকে ডিলেট করো। এরপর আবার নতুন করে onCreate() মেথড কল করে নতুন স্ট্রাকচার অনুযায়ী টেবিল বানাও। আপাতত এটুকু জানলেই চলবে। সামনে শুধু এটার উপরই কয়েকটা ব্লগ পোস্ট লেখা যাবে।
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // Drop older table if existed db.execSQL("DROP TABLE IF EXISTS " + Config.TABLE_STUDENT); // Create tables again onCreate(db); }
DatabaseHelper.java ক্লাসের সম্পূর্ণ সোর্সকোড নিচে দেয়া হলোঃ
public class DatabaseHelper extends SQLiteOpenHelper { private static DatabaseHelper databaseHelper; // All Static variables private static final int DATABASE_VERSION = 1; // Database Name private static final String DATABASE_NAME = Config.DATABASE_NAME; //Private Constructor private DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); Logger.addLogAdapter(new AndroidLogAdapter()); } public static synchronized DatabaseHelper getInstance(Context context){ if(databaseHelper==null){ databaseHelper = new DatabaseHelper(context); } return databaseHelper; } @Override public void onCreate(SQLiteDatabase db) { String CREATE_STUDENT_TABLE = "CREATE TABLE " + Config.TABLE_STUDENT + "(" + Config.COLUMN_STUDENT_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + Config.COLUMN_STUDENT_NAME + " TEXT NOT NULL, " + Config.COLUMN_STUDENT_REGISTRATION + " INTEGER NOT NULL UNIQUE, " + Config.COLUMN_STUDENT_PHONE + " TEXT, " //nullable + Config.COLUMN_STUDENT_EMAIL + " TEXT " //nullable + ")"; db.execSQL(CREATE_STUDENT_TABLE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // Drop older table if existed db.execSQL("DROP TABLE IF EXISTS " + Config.TABLE_STUDENT); // Create tables again onCreate(db); } }
লক্ষ্য করে দেখুন, DatabaseHelper ক্লাসের constructor টি কিন্তু private! তাহলে অ্যাপের যে কোনো ক্লাস থেকে এই DatabaseHelper ক্লাসের অবজেক্ট কিভাবে বানানো যাবে? এখন কনস্ট্রাক্টরের পরের getInstance() মেথডটা দেখেন। এটা public static. তার মানে DatabaseHelper ক্লাসের অবজেক্ট বানানো ছাড়াই এই মেথডকে অ্যাপের যে কোনো জায়গা থেকে কল করা যাবে। আর synchronized keyword-টা? এটা নিশ্চিত করে যে একাধিক জায়গা থেকে একই মেথড at a time call হলেও এই মেথড একটার পর একটা execute হবে। অর্থাৎ একই সময়ে Activity আর কোনো একটা ব্যাকগ্রাউন্ড সার্ভিস যদি এই মেথডকে কল করে তাহলে এটা parallelly এক্সিকিউট হবে না। বরং একটা এক্সিকিউট হবে, এরপর আরেকটা এক্সিকিউট হবে। এই মেথডের বডিতে দেখা যাচ্ছে এই ক্লাসের ভিতরে থাকা DatabaseHelper এর একটা private object null কিনা সেটা চেক করছে। এটা null থাকলে DatabaseHelper এর private constructor কে কল করে সেটা initialization হচ্ছে। আর যদি null না হয় তবে existing object-কেই return করে দেয়া হচ্ছে। ফলে বারবার প্রাইভেট কনস্ট্রাক্টর কল হয়ে প্রতিবার নতুন নতুন ডেটাবেজ তৈরির আশংকা থাকলো না। একটা অ্যাপে DatabaseHelper ক্লাসের একটা মাত্র অবজেক্টই তৈরি হবে। সেটাই যখন দরকার reuse হবে। এই প্রকৃয়াটাকে কেতাবী ভাষায় বলা হয় Singleton! তো অ্যাপের যেখানেই DatabaseHelper এর instance দরকার হবে getInstance() মেথডে কল দিলেই পাওয়া যাবে।
এই পর্বে শুধু ডেটাবেজ তৈরি পর্যন্তই দেখানো হলো। পরের পর্বে দেখাবো ডেটাবেজ থেকে কিভাবে কুয়েরি করে ডেটা রিড করতে হয় আর কিভাবে ডেটাবেজে ডেটা রাইট করতে হয়। পরের পর্বটি পড়তে পারবেন এখান থেকে।
কষ্ট করে এত লম্বা পোস্ট পড়ার জন্য ধন্যবাদ। কোথাও কোনো ভুলত্রুটি চোখে পড়লে কমেন্ট করে জানাবেন প্লিজ। পুরো প্রোজেক্টের সোর্সকোড পাওয়া যাবে আমার গিটহাব রিপোজিটরিতে।