SQLite database সিরিজের এটা পঞ্চম পর্ব। তিনটা টেবিলের মধ্যে FOREIGN KEY এর মাধ্যমে রিলেশন করা ও টেবিলগুলোতে ডেটা CREATE, READ, UPDATE ও DELETE (CRUD Operation) দেখানো হবে। ধরে নিচ্ছি আপনি SQLite database ব্যবহার করে দুইটা টেবিলে CRUD operation চালাতে পারেন। যদি তা না হয় তাহলে এই পোস্টটা আপনার খুব একটা কাজে আসবে না। আপনার যদি SQLite database সম্পর্কে ব্যাসিক ধারণা না থেকে থাকে তাহলে অবশ্যই এই সিরিজের প্রথম পোস্টটি পড়ে আসুন। দ্বিতীয় পোস্টে দেখানো হয়েছিল কিভাবে যে কোনো অ্যাপের জন্য ডেটাবেজ বানানো যায়। ডেটাবেজ বানানোর পর টেবিল বানানো সম্পর্কেও সেখানে আলোকপাত করা হয়েছে। তৃতীয় পোস্টে দেখানো হয়েছিল single table এর একটা ডেটাবেজে কিভাবে CRUD operation চালানো যায়। এরপর চতুর্থ পোস্টে দেখানো হয়েছিল দুইটা টেবিলের মধ্যে কিভাবে রিলেশন তৈরি করে কাজ করা যায়।
এই চারটা পোস্ট পড়া হলে বা কনসেপ্ট ক্লিয়ার থাকলে এরপর এই পর্বটি পড়া শুরু করতে পারেন। আশা করি তখন এটা বুঝতে সহজ হবে।
Problem Description
একজন টিচারের জন্য বা একটা ডিপার্টমেন্টের জন্য একটা অ্যাপ বানাতে হবে যেখানে স্টুডেন্টের নামের এন্ট্রি হবে। এই ডিপার্টমেন্টে কী কী সাবজেক্ট পড়ানো হয় সেগুলো আলাদা ভাবে এন্ট্রি হবে। এরপর কোন স্টুডেন্ট কোন কোন সাবজেক্ট নিয়েছে সেই ইনফরমেশনও ডেটাবেজে রাখা হবে।
এই একই কাজ গত পর্বে করা হয়েছিল, তবে তা ছিল একদম naive way তে। সেখানে আমরা প্রত্যেক স্টুডেন্টের জন্য একই সাবজেক্ট বারবার ইনপুট দিয়েছিলাম। মানে প্রথম স্টুডেন্ট যদি সি, ডেটা স্ট্রাকচার এই দুইটা সাবজেক্ট নেয় তখন প্রথম স্টুডেন্টের জন্য এই দুইটা সাবজেক্টের ডেটা ইনপুট দেয়া হয়েছিল। দ্বিতীয় স্টুডেন্ট এসেও যদি একই সাবজেক্ট নিতে চায় ঐ স্টুডেন্টের জন্যও আবার সি ও ডেটা স্ট্রাকচার সাবজেক্ট দুটির যাবতীয় ইনফরমেশন ইনপুট দিতে হত। এতে ঝামেলা হচ্ছে ডেটার রিপিটেশন। প্রথম স্টুডেন্টের ক্ষেত্রে সাবজেক্ট এন্ট্রি দিতে যেই পরিমান ডেটা স্টোর করা হচ্ছে দ্বিতীয় স্টুডেন্টের ক্ষেত্রেও একই ডেটা স্টোর করতে হচ্ছে। একই সাথে এভাবে কাজ করলে ইউজারও বিরক্ত হবেন। কারণ প্রত্যেক স্টুডেন্টের জন্য একই তথ্য বারবার ইনপুট দিতে হচ্ছে।
তাই আমাদেরকে এখানে এমন একটা অ্যাপ দাঁড় করাতে হবে যাতে প্রথমে স্টুডেন্ট এন্ট্রি দেয়া হবে। এরপর কী কী সাবজেক্ট অ্যাভেইলেবল সেগুলো এন্ট্রি দেয়া হবে। এরপর প্রতি স্টুডেন্টের জন্য সাবজেক্টের লিস্ট শো করবে। সেখান থেকে টিক চিহ্ন (CheckBox) দিয়ে দেয়া হবে যে, একজন স্টুডেন্ট কোন কোন সাবজেক্ট নিয়েছে। সব স্টুডেন্টের জন্য আলাদা আলাদা করে সাবজেক্টের নামধাম ইনপুট দেয়া লাগবে না।
Database design
কোড শুরু করার আগে আমাদের দরকার রিকোয়ার্মেন্ট অ্যানালাইসিস করে মনে মনে চিন্তা করা এবং খাতায় কাটাকুটি করা। এই প্রজেক্টটা দাঁড় করানোর জন্য আমি প্রথমে কাগজে ডেটাবেজের ডিজাইনটা কেমন হতে পারে সেটা নিয়ে আঁকিবুকি করেছি। এরপর অ্যাপের UI design কেমন হতে পারে সেটা কাগজে এঁকেছি। কয়েক দফা আঁকাঅাঁকির পরে উপরের GIF ইমেজের মত ফ্লো ফাইনাল করেছি। অবশ্য কিছু জিনিস অ্যাপ ডেভেলপমেন্টের সময়ও চেঞ্জ হয়েছে। যাই হোক, নিচে ডেটাবেজ স্কিমাটা তুলে ধরছি। আগের পর্বের স্কিমার সাথে মিলিয়ে দেখলে পার্থক্যটা হয়ত পরিস্কার হবে।
এখানে স্টুডেন্ট টেবিল ও সাবজেক্ট টেবিল পুরোপুরি সেপারেট। এদের মধ্যে সরাসরি কোনো রিলেশন নাই। স্টুডেন্ট টেবিলের মধ্যে সাবজেক্টের কোনো ইনফরমেশন রাখা হয় নাই। একই ভাবে সাবজেক্ট টেবিলের মধ্যেও স্টুডেন্টের কোনো ইনফরমেশন নাই। কিন্তু এদের মধ্যে আমাদেরকে রিলেশন ক্রিয়েট করতে হবে। এজন্য student-subject নামের একটা pivot table বানানো হয়েছে। এই টেবিলে আমরা স্টুডেন্টের আইডি আর সাবজেক্টের আইডি রাখব। এখান থেকে সহজে বের করে নেয়া যাবে কোন স্টুডেন্ট কোন কোন সাবজেক্ট নিয়েছে। যদি 101 আইডির স্টুডেন্টের সাবজেক্টগুলো বের করার দরকার হয় তখন এই পিভট টেবিলে খুঁজব 101 এর জন্য কী কী সাবজেক্ট আইডি রয়েছে। আর এই আইডিগুলো দিয়ে সাবজেক্ট টেবিলে কুয়েরি চালাবো। একই ভাবে কোনো একটা সাবজেক্ট কোন কোন স্টুডেন্ট নিয়েছে সেটাও এই টেবিলে কুয়েরি চালিয়ে বের করা যাবে।
টেবিলগুলো ক্রিয়েট করে ডেটা ইনসার্ট করা হলে ডেটাবেজের চেহারা কেমন হবে সেটা নিচের ছবিতে দেখানো হলোঃ
সহজে টেবিলগুলোর মধ্যে রিলেশন বুঝানোর জন্য উপরের ছবিতে পিভট টেবিলকে মাঝে রাখা হয়েছে। স্টুডেন্ট টেবিলের আইডি ও সাবজেক্ট টেবিলের আইডি রাখা হয়েছে পিভট টেবিলে। পিভট টেবিলে দেখতে পাচ্ছি 101 আইডির স্টুডেন্ট 2 ও 4 নাম্বার আইডির সাবজেক্ট নিয়েছে। এই আইডির সাবজেক্টগুলো কী কী, সেটা পাওয়া যাবে সাবজেক্ট টেবিলে। 2 ও 4 নাম্বার আইডির সাবজেক্টগুলো হচ্ছে যথাক্রমে Object Oriented Programming ও Data Structures Lab. আশা করি এইটুকু বুঝতে কোনো সমস্যা নাই। সমস্যা থাকলে আরেকবার পড়ে দেখা যেতে পারে। অন্যথায় পোস্ট পুরোটা পড়ে এরপর কমেন্ট করে প্রশ্ন করতে পারেন।
ডেটাবেজের বিভিন্ন constraint ও logic ডেটাবেজ স্কিমায় দেখানো সম্ভব হয় না। তাই বিস্তারিত লজিকগুলো নিচে পয়েন্ট আকারে উল্লেখ করছিঃ
- Primary key of student table is _id
- registration_no field of student table must be unique
- Primary key of subject table is _id
- code field of subject table is unique
- No need any ID in pivot table
- Pivot table contains student_id and subject_id
- student_id comes from student table
- subject_id comes from subject table
- There is a unique constraint in pivot table by student_id and subject_id. Pivot tables doesn’t allow any entry with same pair of student_id and subject_id (Ex: student id: 101 and subject id: 2 is allowed for single time. because student 101 cannot take subject 2 multiple times)
- When a student is removed from student table, all entry of that student must be removed from pivot table
- When a subject is removed from subject table, all entry of that subject must be removed from pivot table
- When _id of a student will be updated in student table, student_id field of pivot table must update for associated student
- When _id of a subject will be updated in subject table, subject_id field of pivot table must update for associated subject
- If all student is removed from student table, then all entry of pivot table will be removed
- If all subject is removed from subject table, then all entry of pivot table will be removed
Database Creation by Java Code
আগের পর্বের মত এই পর্বেও DetabaseHelper ক্লাস বানিয়ে তার onCreate() মেথডের ভিতরে ডেটাবেজ স্ট্রাকচার ডিফাইন করে দেয়া হয়েছে। ডেটাবেজ ও তার টেবিল স্ট্রাকচারের কোডটুকু নিয়ে দেয়া হচ্ছেঃ
@Override public void onCreate(SQLiteDatabase sqLiteDatabase) { String CREATE_STUDENT_TABLE = "CREATE TABLE " + TABLE_STUDENT + "(" + STUDENT_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + STUDENT_NAME + " TEXT NOT NULL, " + STUDENT_REGISTRATION_NUM + " INTEGER NOT NULL UNIQUE, " + STUDENT_PHONE + " TEXT, " //nullable + STUDENT_EMAIL + " TEXT " //nullable + ")"; String CREATE_SUBJECT_TABLE = "CREATE TABLE " + TABLE_SUBJECT + "(" + SUBJECT_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + SUBJECT_NAME + " TEXT NOT NULL, " + SUBJECT_CODE + " INTEGER NOT NULL UNIQUE, " + SUBJECT_CREDIT + " REAL" //nullable + ")"; String CREATE_TAKEN_SUBJECT_TABLE = "CREATE TABLE " + TABLE_STUDENT_SUBJECT + "(" + STUDENT_ID_FK + " INTEGER NOT NULL, " + SUBJECT_ID_FK + " INTEGER NOT NULL, " + "FOREIGN KEY (" + STUDENT_ID_FK + ") REFERENCES " + TABLE_STUDENT + "(" + STUDENT_ID + ") ON UPDATE CASCADE ON DELETE CASCADE, " + "FOREIGN KEY (" + SUBJECT_ID_FK + ") REFERENCES " + TABLE_SUBJECT + "(" + SUBJECT_ID + ") ON UPDATE CASCADE ON DELETE CASCADE, " + "CONSTRAINT " + STUDENT_SUB_CONSTRAINT + " UNIQUE (" + STUDENT_ID_FK + "," + SUBJECT_ID_FK + ")" + ")"; sqLiteDatabase.execSQL(CREATE_STUDENT_TABLE); sqLiteDatabase.execSQL(CREATE_SUBJECT_TABLE); sqLiteDatabase.execSQL(CREATE_TAKEN_SUBJECT_TABLE); }
গত পর্বে এই কোডগুলোর ব্যাখ্যা করা হয়েছিল। তাই এখানে আবার একই কথা রিপিট করলাম না। এখান থেকে DatabaseHelper ক্লাসের পুরো কোড দেখতে পারবেন। খেয়াল করলে দেখবেন এখানে এই ক্লাসের অবজেক্ট ক্রিয়েট করার জন্য কোনো public constructor নাই। Object বানানোর জন্য নিচের পাবলিক মেথডটা শুধু দেয়া আছেঃ
public static DatabaseHelper getInstance() { if (databaseHelper == null) { synchronized (DatabaseHelper.class){ //thread safe singleton if (databaseHelper == null) databaseHelper = new DatabaseHelper(); } } return databaseHelper; }
যখনই DatabaseHelper ক্লাসের কোনো একটা অবজেক্ট দরকার হবে এই মেথড কল করতে হবে। আমাদের উদ্দেশ্য হচ্ছে পুরো অ্যাপে এই ক্লাসের একটা অবজেক্টই exist করবে। যদি অলরেডি অবজেক্ট বানানো থাকে তাহলে ঐ অবজেক্টটাই এই মেথড রিটার্ন করবে। আর যদি এই অবজেক্টটা null হয় তবে new DatabaseHelper() কল করে নতুন অবজেক্ট বানানো হয়েছে(এটা নাল হবে প্রথমবার কল হবার সময়। পরবর্তীতে এটা আর নাল হবে না)। এই constructor-টা প্রাইভেট। তাই এই ক্লাসের ভিতর থেকেই শুধুমাত্র কল করা যাবে। অ্যাক্টিভিটি বা অন্যান্য কোথাও থেকে সরাসরি constructor কল করে DatabaseHelper ক্লাসের অবজেক্ট ক্রিয়েট করা যাবে না।
একই সাথে আমাদের ডেটাবেজ অবজেক্ট ক্রিয়েট করার কাজটা হতে হবে thread safe. অর্থাৎ দুইটা থ্রেড থেকে বা দুই জায়গা থেকে একই সময়ে যদি এই মেথড কল করা হয় তাহলে যেন ডেটাবেজের দুইটা অবজেক্ট ক্রিয়েট না হয়। এজন্য মেথডের ভিতর নাল চেক করার পর synchronized keyword ইউজ করা হয়েছে। অনেক সময় বিভিন্ন জায়গায় দেখতে পারেন মেথডের নামের শুরুতে synchronized ইউজ করা হয়। সেটাতেও কাজ হবে কিন্তু পারফর্মেন্স খারাপ হবে। কারণ যতবারই এই মেথড কল করা হবে বা বিভিন্ন থ্রেড থেকে একই সময়ে অনেকবার এই মেথড কল হলে মেথড কলগুলো queue-তে পড়ে যাবে। একটা মেথড কল পুরোপুরি শেষ হবার আগ পর্যন্ত অন্য কোনো কলের এক্সিকিউশন শুরু হবে না। যেহেতু মাল্টিপল অবজেক্ট ক্রিয়েট করা আটকাতে হবে তাই databaseHelper অবজেক্টটা নাল হলেই এরপর synchronized ব্যবহার করে অবজেক্ট ক্রিয়েট করা হয়েছে। অন্যথায় যদি অলরেডি একটা অবজেক্ট বানানো থাকে তাহলে আর সিনক্রোনাইজেশনের দরকারই নাই। বানানো অবজেক্টই চোখ বন্ধ করে রিটার্ন করে দেয়া হবে। অ্যান্ড্রয়েডের জব ইন্টারভিউয়ের ক্ষেত্রে thread safe singleton এর বিষয়ে জানতে চাওয়া হয়। সেটা ডেটাবেজের অবজেক্ট ক্রিয়েট করা বা SharedPreference এর অবজেক্ট ক্রিয়েট করার ব্যাপারেও জানতে চাওয়া হতে পারে। অনেক সময় বলা হয় ডেটাবেজ বা শেয়ার্ডপ্রেফারেন্সের অবজেক্ট ক্রিয়েট করার কোডটা কাগজে লিখতে। এরপর আপনার কোড যদি থ্রেড সেফ না হয় তখন হয়ত জানতে চাইবে বা synchronized keyword এর কাজ সম্পর্কে জানতে চাইবে।
CRUD Operation
ডেটাবেজে কুয়েরি চালানোর কোডগুলোর ক্ষেত্রে আগের পর্বের সাথে এই পর্বে বেশ পার্থক্য রয়েছে। এই পর্বে চেষ্টা করেছি কোড কোয়ালিটি আরেকটু better করার জন্য। Activity এর ভিতর ডেটাবেজের কুয়েরি করা বা কোনো concrete implementation না করে ডেটাবেজের পুরো কাজটা আলাদা করা হয়েছে। অ্যাক্টিভিটিগুলো interface এর মাধ্যমে ডেটাবেজ কুয়েরির রেজাল্ট পাবে। এটাকে ডেটাবেজ লেয়ার বলতে পারি। Caller class-গুলো জানবে না মেথডগুলোর ভিতরে কী কাজ হয়েছে। মেথড কল করে জাস্ট কলব্যাক মেথডের জন্য অপেক্ষা করবে। আমি যেহেতু এখনো শিখছি হয়ত এটাও সঠিক ওয়ে না। আরো ভাল ওয়ে থাকতে পারে। আপনার যে কোনো পরামর্শ বা মতামত একান্ত কাম্য। যাই হোক, নিচে CRUD operation এর জন্য যেই মেথডগুলো দরকার সেগুলোর সিগনেচার তুলে ধরা হলো। ধরেন আপনি ডেটাবেজের কাজ করবেন বা ডেটাবেজের কাজ ভাল পারেন। আরেকজন ডেভেলপার শুধু আপনার কোডগুলোর মেথড কল করে কাজ করবে। তখন আপনি নিচের ইন্টারফেসগুলো দিয়ে বলবেন এগুলো কল করলে ডেটা পাবে। এগুলার পিছনে কী কাজ হচ্ছে তার জানার দরকারও নাই। আপনি এই ইন্টারফেসের ইমপ্লিমেন্টেশনগুলো লিখবেন, সে শুধু মেথড কল করবে। এটা OOP এর abstraction এর একটা চমৎকার প্রয়োগ।
public class QueryContract { public interface StudentQuery { void createStudent(Student student, QueryResponse<Boolean> response); void readStudent(int studentId, QueryResponse<Student> response); void readAllStudent(QueryResponse<List<Student>> response); void updateStudent(Student student, QueryResponse<Boolean> response); void deleteStudent(int studentId, QueryResponse<Boolean> response); } public interface SubjectQuery { void createSubject(Subject subject, QueryResponse<Boolean> response); void readAllSubject(QueryResponse<List<Subject>> response); void updateSubject(Subject subject, QueryResponse<Boolean> response); void deleteSubject(int subjectId, QueryResponse<Boolean> response); } public interface TakenSubjectQuery { void createTakenSubject(int studentId, int subjectId, QueryResponse<Boolean> response); void readAllTakenSubjectByStudentId(int studentId, QueryResponse<List<Subject>> response); void readAllSubjectWithTakenStatus(int studentId, QueryResponse<List<TakenSubject>> response); void deleteTakenSubject(int studentId, int subjectId, QueryResponse<Boolean> response); } public interface TableRowCountQuery { void getTableRowCount(QueryResponse<TableRowCount> response); } }
চাইলে একটা ইন্টারফেসের মধ্যে সবগুলো মেথড সিগনেচার দেয়া যেত। কিন্তু অনেকগুলো মেথড একসাথে থাকলে মেইনটেইন করতে কষ্ট। আবার যেই ক্লাস এই ইন্টারফেসকে implement করবে সেই ক্লাসটাও অনেক বড় হয়ে যেত। তাই এগুলোকে আলাদা আলাদা রাখা হয়েছে। আপনি চাইলে একসাথে রেখে টেস্ট করে দেখতে পারেন।
দেখা যাচ্ছে প্রতিটা method signature এর শেষ parameter এ QueryResponse নামের একটা generic class এর অবজেক্ট পাঠানো হয়েছে। এটাও আসলে একটা ইন্টারফেস। এটার মাধ্যমে caller এর কাছে তার desired data পাঠানো হবে। ইন্টারফেসটা হচ্ছেঃ
public interface QueryResponse<T> { void onSuccess(T data); void onFailure(String message); }
অর্থাৎ Activity থেকে যখন কোনো ডেটা রিড/রাইট করতে চেয়ে মেথড কল করা হবে সেই মেথডের শেষ প্যারামিটারে QueryResponse এর একটা ইমপ্লিমেন্টেশন পাঠাতে হবে। সেখানে onSuccess() ও onFailure() মেথড দুটি override হবে। নিচে স্টুডেন্ট কুয়েরির ইমপ্লিমেন্টেশনটা দেখানো হলঃ
public class StudentQueryImplementation implements QueryContract.StudentQuery { private DatabaseHelper databaseHelper = DatabaseHelper.getInstance(); @Override public void createStudent(Student student, QueryResponse<Boolean> response) { SQLiteDatabase sqLiteDatabase = databaseHelper.getWritableDatabase(); ContentValues contentValues = getContentValuesForStudent(student); try { long id = sqLiteDatabase.insertOrThrow(TABLE_STUDENT, null, contentValues); if(id>0) { response.onSuccess(true); student.setId((int) id); } else response.onFailure("Failed to create student. Unknown Reason!"); } catch (SQLiteException e){ response.onFailure(e.getMessage()); } finally { sqLiteDatabase.close(); } } @Override public void readStudent(int studentId, QueryResponse<Student> response) { SQLiteDatabase sqLiteDatabase = databaseHelper.getReadableDatabase(); Cursor cursor = null; try { cursor = sqLiteDatabase.query(TABLE_STUDENT, null, STUDENT_ID + " =? ", new String[]{String.valueOf(studentId)}, null, null, null); if(cursor!=null && cursor.moveToFirst()) { Student student = getStudentFromCursor(cursor); response.onSuccess(student); } else response.onFailure("Student not found with this ID in database"); } catch (Exception e){ response.onFailure(e.getMessage()); } finally { sqLiteDatabase.close(); if(cursor!=null) cursor.close(); } } @Override public void readAllStudent(QueryResponse<List<Student>> response) { SQLiteDatabase sqLiteDatabase = databaseHelper.getReadableDatabase(); List<Student> studentList = new ArrayList<>(); Cursor cursor = null; try { cursor = sqLiteDatabase.query(TABLE_STUDENT, null, null, null, null, null, null); if(cursor!=null && cursor.moveToFirst()){ do { Student student = getStudentFromCursor(cursor); studentList.add(student); } while (cursor.moveToNext()); response.onSuccess(studentList); } else response.onFailure("There are no student in database"); } catch (Exception e){ response.onFailure(e.getMessage()); } finally { sqLiteDatabase.close(); if(cursor!=null) cursor.close(); } } @Override public void updateStudent(Student student, QueryResponse<Boolean> response) { SQLiteDatabase sqLiteDatabase = databaseHelper.getWritableDatabase(); ContentValues contentValues = getContentValuesForStudent(student); try { long rowCount = sqLiteDatabase.update(TABLE_STUDENT, contentValues, STUDENT_ID + " =? ", new String[]{String.valueOf(student.getId())}); if(rowCount>0) response.onSuccess(true); else response.onFailure("No data is updated at all"); } catch (Exception e){ response.onFailure(e.getMessage()); } finally { sqLiteDatabase.close(); } } @Override public void deleteStudent(int studentId, QueryResponse<Boolean> response) { SQLiteDatabase sqLiteDatabase = databaseHelper.getWritableDatabase(); try { long rowCount = sqLiteDatabase.delete(TABLE_STUDENT, STUDENT_ID + " =? ", new String[]{String.valueOf(studentId)}); if(rowCount>0) response.onSuccess(true); else response.onFailure("Failed to delete student. Unknown reason"); } catch (Exception e){ response.onFailure(e.getMessage()); } finally { sqLiteDatabase.close(); } } private ContentValues getContentValuesForStudent(Student student){ ContentValues contentValues = new ContentValues(); contentValues.put(STUDENT_NAME, student.getName()); contentValues.put(STUDENT_REGISTRATION_NUM, student.getRegistrationNumber()); contentValues.put(STUDENT_PHONE, student.getPhone()); contentValues.put(STUDENT_EMAIL, student.getEmail()); return contentValues; } private Student getStudentFromCursor(Cursor cursor){ int id = cursor.getInt(cursor.getColumnIndex(STUDENT_ID)); String name = cursor.getString(cursor.getColumnIndex(STUDENT_NAME)); int regNum = cursor.getInt(cursor.getColumnIndex(STUDENT_REGISTRATION_NUM)); String phone = cursor.getString(cursor.getColumnIndex(STUDENT_PHONE)); String email = cursor.getString(cursor.getColumnIndex(STUDENT_EMAIL)); return new Student(id, name, regNum, phone, email); } }
এখানে StudentQueryImplementation ক্লাসটি QueryContract.StudentQuery ইন্টারফেসকে implement করেছে। ফলে QueryContract.StudentQuery ইন্টারফেসের সবগুলো মেথড এখানে override করে মেথডের বডিগুলো লিখে দিতে হচ্ছে। এটার ভিতরের কাজগুলো সব আগের পর্বের মতই। একই কাজ একটু অন্য ভাবে দেখানো হয়েছে মাত্র। তাই বিস্তারিত আলোচনা এড়িয়ে গেলাম।
Android SQLite JOIN Query
অ্যাপের একটা রিকোয়ার্মেন্ট হচ্ছে একজন স্টুডেন্টকে সাবজেক্ট অ্যাসাইন করা যাবে। সেজন্য যে কোন স্টুডেন্টের জন্য নিচের মত একটা UI ডেভেলপ করতে হবে।
আমাদের ডেটাবেজে উপরের ৫টা সাবজেক্ট এন্ট্রি দেয়া আছে। নির্দিষ্ট কোনো একজন স্টুডেন্টের জন্য কোনো সাবজেক্ট অ্যাসাইন করতে চাইলে এরকম লিস্ট শো করবে। এটা বুঝাচ্ছে যে সিলেক্টেড তিনটা সাবজেক্ট অলরেডি অ্যাসাইন করা হয়েছে। এখান থেকে subject assign বা deassign করা যাবে। মনে মনে একটু চিন্তা করেন তো! এই লিস্টটা জেনারেট করার জন্য ডেটাবেজে কেমন কুয়েরি চালানো লাগতে পারে?
প্রথম যেই অ্যাপ্রোচ আমার মাথায় এসেছিল সেটা হচ্ছে, যেহেতু সবগুলো সাবজেক্টকে নিয়ে আসতে হচ্ছে তাহলে সাবজেক্ট টেবিলে কুয়েরি চালিয়ে সব সাবজেক্ট এনে একটা সাবজেক্টের লিস্ট বানাবো। এরপর পিভট টেবিলে স্টুডেন্ট আইডি দিয়ে কুয়েরি চালিয়ে সাবজেক্ট আইডিগুলো বের করব, এই স্টুডেন্ট যেই সাবজেক্টগুলো নিয়েছে সেগুলো। এরপর সাবজেক্ট লিস্ট শো করার সময় চেক করব সাবজেক্টের আইডিটা দ্বিতীয় এই লিস্টে আছে কিনা। যদি থাকে তাহলে চেকবক্সটা চেক করে দিব, অন্যথায় আনচেক রাখব।
এই অ্যাপ্রোচটা স্ট্রেইট ফরওয়ার্ড বা সোজাসাপ্টা হলেও বেশ খাটাখাটনি করতে হবে। আরো সহজে কাজটা করার জন্য JOIN QUERY এর সাহায্য নেয়া যেতে পারে। আগের পর্বে আমাদের JOIN Query চালাতে হয় নাই। এক্ষেত্রে পিভট টেবিলের সাথে student_subject টেবিলের জয়েন করে ডেটা বের করা হয়েছে।
কোনো একজন স্টুডেন্ট কী কী সাবজেক্ট নিয়েছে আর কী কী নেয় নি সেটা লিস্ট আকারে চেকবক্সের মাধ্যমে দেখানোর জন্য নিচের মেথডটা কাজ করবেঃ
@Override public void readAllSubjectWithTakenStatus(int studentId, QueryResponse<List<TakenSubject>> response) { SQLiteDatabase sqLiteDatabase = databaseHelper.getReadableDatabase(); String QUERY = "SELECT s._id, s.name, s.code, s.credit, ss.student_id " + "FROM subject as s LEFT JOIN student_subject as ss ON s._id = ss.subject_id " + "AND ss.student_id = " + studentId; Cursor cursor = null; try { List<TakenSubject> takenSubjectList = new ArrayList<>(); cursor = sqLiteDatabase.rawQuery(QUERY, null); if(cursor.moveToFirst()){ do { int id = cursor.getInt(cursor.getColumnIndex(SUBJECT_ID)); String subjectName = cursor.getString(cursor.getColumnIndex(SUBJECT_NAME)); int subjectCode = cursor.getInt(cursor.getColumnIndex(SUBJECT_CODE)); double subjectCredit = cursor.getDouble(cursor.getColumnIndex(SUBJECT_CREDIT)); boolean isTaken = false; if(cursor.getInt(cursor.getColumnIndex(STUDENT_ID_FK)) > 0) { isTaken = true; } TakenSubject takenSubject = new TakenSubject(id, subjectName, subjectCode, subjectCredit, isTaken); takenSubjectList.add(takenSubject); } while (cursor.moveToNext()); response.onSuccess(takenSubjectList); } else response.onFailure("There are no subject assigned to this student"); } catch (Exception e){ response.onFailure(e.getMessage()); } finally { sqLiteDatabase.close(); if (cursor!=null) cursor.close(); } }
এখানে কুয়েরি করার মূল স্ট্রিংটা হচ্ছেঃ
String QUERY = “SELECT s._id, s.name, s.code, s.credit, ss.student_id FROM subject as s LEFT JOIN student_subject as ss ON s._id = ss.subject_id AND ss.student_id = ” + studentId;
সিলেক্ট কুয়েরির ভিতরে সাবজেক্ট টেবিলের সবগুলো কলাম সিলেক্ট করা হয়েছে। আরেকটা বাড়তি কলাম সিলেক্ট করা হয়েছে। সেটা হচ্ছে student_subject টেবিলের student_id কলাম। আচ্ছা এই স্টুডেন্ট আইডি দিয়ে আমরা কী করব? আমরা তো কোনো একটা স্টুডেন্টের কী কী সাবজেক্ট আছে আর কোন সাবজেক্টগুলো নাই সেটা জানতে চাচ্ছি। এই স্টুডেন্টের আইডির মাধ্যমে আমরা বুঝে নিব কোন সাবজেক্টটা এই স্টুডেন্ট নিয়েছে আর কোনটা নেয় নি। সবগুলো সাবজেক্ট লিস্ট আকারে আসবে আর সাবজেক্টের সব ডেটার পাশে (সর্বডানে) পাশে স্টুডেন্টের আইডি থাকবে যদি ঐ সাবজেক্টটা এই স্টুডেন্ট নিয়ে থাকে। নিচের ছবি দেখলে ক্লিয়ার হবেঃ
101 আইডির স্টুডেন্টের জন্য শুরুতে উল্লেখ করা ডেটাবেজে যদি উপরের জয়েন কুয়েরিটা চালানো হয় তাহলে এমন একটা টেবিল পাওয়া যাবে। পিভট টেবিলটা আরেকবার চেক করে দেখতে পারেন। 101 আইডির স্টুডেন্ট শুধু 2 ও 4 আইডি বিশিষ্ট সাবজেক্ট দুটি নিয়েছে। তাই সাবজেক্ট টেবিলের 2 ও 4 আইডির পাশে স্টুডেন্ট আইডিটা বসে গেছে। বাকি দুইটা সাবজেক্ট এই স্টুডেন্ট নেয় নি তাই স্টুডেন্টের আইডি NULL বসে গেছে। আমরা এই NULL ভ্যালু দিয়ে চেক করে সিদ্ধান্ত নিব। নিচের চেকটা পাবেন উপরের এই মেথডের ভিতরঃ
if(cursor.getInt(cursor.getColumnIndex(STUDENT_ID_FK)) > 0) { isTaken = true; }
একটা প্রশ্ন মাথায় আসতে পারে, LEFT JOIN কেন করলাম? কেন RIGHT JOIN করলাম না? কুয়েরিটা একটু খেয়াল করলে দেখবেন জয়েন করার সময় Left side এ সাবজেক্ট টেবিল রেখেছি আর right side এ রেখেছি student_subject টেবিল। কোন পাশের ডেটাগুলো must লাগবেই? আর কোন পাশের ডেটা nullable? ঠিক ধরেছেন! বাম পাশের সাবজেক্টের সব ইনফরমেশন অবশ্যই রিটার্ন করতে হবে (কারণ লিস্টে সবগুলো সাবজেক্টই শো করতে হবে)। আর ডান পাশের স্টুডেন্ট আইডি নাল হলেও চলবে (নাল হলে বুঝে নিব যে এই সাবজেক্টটা এই স্টুডেন্ট নেয় নি)। যেহেতু বাম পাশের ডেটাগুলো নাল হওয়া যাবে না তাই LEFT JOIN করা হয়েছে। অর্থাৎ যেই পাশের ডেটাগুলো null-able না সেই পাশের জয়েন করতে হবে। এটা আসলে relational database এর কনসেপ্ট। তাই ক্লিয়ার না হলে রিলেশনাল ডেটাবেজের জয়েন কুয়েরির উপর একটু পড়াশোনা করা যেতে পারে।
কোনো একটা স্টুডেন্টের আইডি দিয়ে ঐ স্টুডেন্ট কী কী সাবজেক্ট নিয়েছে শুধুমাত্র সেই সাবজেক্টগুলো বের করার জন্য এই মেথডটি লিখা আছে TakenSubjectQueryImplementation ক্লাসেঃ
@Override public void readAllTakenSubjectByStudentId(int studentId, QueryResponse<List<Subject>> response) { SQLiteDatabase sqLiteDatabase = databaseHelper.getReadableDatabase(); String QUERY = "SELECT * FROM subject as s JOIN student_subject as ss ON s._id = ss.subject_id WHERE ss.student_id = " + studentId; Cursor cursor = null; try { List<Subject> subjectList = new ArrayList<>(); cursor = sqLiteDatabase.rawQuery(QUERY, null); if(cursor.moveToFirst()){ do { int id = cursor.getInt(cursor.getColumnIndex(SUBJECT_ID)); String subjectName = cursor.getString(cursor.getColumnIndex(SUBJECT_NAME)); int subjectCode = cursor.getInt(cursor.getColumnIndex(SUBJECT_CODE)); double subjectCredit = cursor.getDouble(cursor.getColumnIndex(SUBJECT_CREDIT)); Subject subject = new Subject(id, subjectName, subjectCode, subjectCredit); subjectList.add(subject); } while (cursor.moveToNext()); response.onSuccess(subjectList); } else response.onFailure("There are no subject assigned to this student"); } catch (Exception e){ response.onFailure(e.getMessage()); } finally { sqLiteDatabase.close(); if (cursor!=null) cursor.close(); } }
মূল কুয়েরি স্ট্রিংটা হচ্ছেঃ
“SELECT * FROM subject as s JOIN student_subject as ss ON s._id = ss.subject_id WHERE ss.student_id = ” + studentId;
এখানে স্টুডেন্ট ও সাবজেক্ট টেবিলের মধ্যে জয়েন করা হয়েছে। জয়েন হবে উভয় টেবিলের সাবজেক্ট আইডির উপর ভিত্তি করে। আর WHERE clause দিয়ে ফিল্টার করা হচ্ছে স্পেসিফিক কোনো একটা স্টুডেন্টের সাবজেক্টগুলো।
সবশেষে caller class থেকে একটা মেথড কল করে দেখানো হচ্ছে। ডেটাবেজে একজন নতুন স্টুডেন্ট ক্রিয়েট করার জন্য ডায়লগ ফ্রেগমেন্টে নিচের কোড করা হয়েছে। ওভাররাইড করা মেথডের ভিতর থেকে অ্যাক্টিভিটির UI update এর জন্য onStudentListUpdate() মেথড কল করা হয়েছে। যা কলব্যাক মেথড হিসাবে অ্যাক্টিভিতে থাকা overridden method-কে এক্সিকিউট করবে।
QueryContract.StudentQuery studentQuery = new StudentQueryImplementation(); studentQuery.createStudent(student, new QueryResponse<Boolean>() { @Override public void onSuccess(Boolean data) { getDialog().dismiss(); studentCrudListener.onStudentListUpdate(data); Toast.makeText(getContext(), "Student created successfully", Toast.LENGTH_LONG).show(); } @Override public void onFailure(String message) { studentCrudListener.onStudentListUpdate(false); Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show(); } });
যেহেতু শুধু SQLite নিয়ে আলোচনা এই পোস্টের উদ্দেশ্য, তাই পোস্ট আরো বড় হয়ে যাবার ভয়ে পুরো অ্যাপে কিভাবে কী করা হয়েছে সেটা স্কিপ করে গেছি। কিভাবে ডায়লগ ফ্রেগমেন্ট কাজ করছে বা UI কিভাবে ডিজাইন করেছি সেগুলো আলোচনা করা হল না। আমার বিশ্বাস পুরো প্রোজেক্ট গিটহাব থেকে নামিয়ে রান করলে আর কোডগুলো একটু সময় নিয়ে মিলালে বুঝতে কঠিন হবে না। প্যাকেজের নাম আর ক্লাসের নামগুলো রিলেভেন্ট রাখার চেষ্টা করেছি। পুরো প্রোজেক্টের সোর্সকোড পাওয়া যাবে আমার গিটহাব রিপোজিটরিতে।
অনেক লম্বা পোস্ট হয়ে গেছে। কোথাও অস্পষ্টতা থাকলে বা ভুল চোখে পড়লে নির্দ্বিধায় কমেন্টে জানানোর অনুরোধ রইলো। আমি অ্যান্ড্রয়েড কেবল শিখছি। অনেক ভুলভ্রান্তি থাকতে পারে। ধরিয়ে দিলে কৃতজ্ঞ থাকব। এত সময় নিয়ে পড়ার জন্য আন্তরিক ধন্যবাদ।
vaiya,which design pattern is used on this project?
কোনো প্রতিষ্ঠিত ডিজাইন প্যাটার্ন এখানে ইউজ করা হয় নাই। খানিকটা View-Model বলা যেতে পারে। সামনে MVP ইউজ করে ডেটাবেজ ও নেটওয়ার্ক কলের টিউটোরিয়াল লিখার ইচ্ছা আছে।
Vaia apnar sathe ektu kotha cilo … Apnr fb te ki msg dibo ??
Brother Source Code Download kora jacche na. R kono question er jonno ki apnake facebook a knock deya jabe?
এখান থেকে নামানোর চেষ্টা করতে পারেন।
যে কোনো কিছু জানার জন্য গুগলে সার্চ দিন।