আপনার অ্যাপের ডেটা স্টোর করার জন্য Room পারসিস্টেন্স লাইব্রেরি ব্যবহার করলে, আপনি ডেটা অ্যাক্সেস অবজেক্ট বা DAO নির্ধারণ করার মাধ্যমে স্টোর করা ডেটার সাথে ইন্টার্যাক্ট করেন। প্রতিটি DAO -তে এমন ফাংশন থাকে যা আপনার অ্যাপের ডেটাবেসে অ্যাবস্ট্রাক্ট অ্যাক্সেস প্রদান করে। কম্পাইল টাইমে, আপনার ডিফাইন করা DAO-এর ইমপ্লিমেন্টেশন Room অটোমেটিক তৈরি করে।
কোয়েরি বিল্ডার বা সরাসরি কোয়েরির পরিবর্তে আপনার অ্যাপের ডেটাবেস অ্যাক্সেস করতে DAO ব্যবহার করে, আপনি উদ্বেগ আলাদা করা, একটি গুরুত্বপূর্ণ আর্কিটেকচারাল নীতি বজায় রাখতে পারবেন। এছাড়াও, আপনি যখন আপনার অ্যাপ পরীক্ষা করেন, তখন DAO আপনাকে মক ডেটাবেস অ্যাক্সেস করতে দেয়।
DAO-এর গঠনতন্ত্র
আপনি প্রতিটি DAO-কে ইন্টারফেস বা অ্যাবস্ট্র্যাক্ট ক্লাস হিসেবে সংজ্ঞায়িত করতে পারেন। সাধারণ
ব্যবহারের ক্ষেত্রে, আপনি সাধারণত একটি ইন্টারফেস ব্যবহার করেন। যাই হোক না কেন, আপনাকে সবসময় @Dao দিয়ে DAO
অ্যানোটেট করতে হবে। DAO-এর কোনও প্রপার্টি নেই, তবে আপনার অ্যাপের
ডেটাবেসের সাথে ইন্টার্যাক্ট করার জন্য এক বা একাধিক ফাংশন
সেগুলি নির্ধারণ করে।
নিচে দেওয়া কোডটি হল DAO-এর একটি উদাহরণ যা
Room ডেটাবেসে User অবজেক্ট যোগ করা, মুছে দেওয়া ও বেছে নেওয়ার ফাংশন নির্ধারণ করে:
@Dao interface UserDao { @Insert suspend fun insertAll(vararg users: User) @Delete suspend fun delete(user: User) @Query("SELECT * FROM user") suspend fun getAll(): List<User> }
ডেটাবেস ইন্টার্যাকশনকে সংজ্ঞায়িত করে এমন দুই ধরনের DAO ফাংশন আছে:
- কনভেনিয়েন্স ফাংশন যা আপনাকে কোনও SQL কোড না লিখে আপনার ডেটাবেসে সারি যোগ করতে, আপডেট করতে এবং মুছে ফেলতে দেয়।
- কোয়েরি ফাংশন যা আপনাকে ডেটাবেসের সাথে ইন্টার্যাক্ট করার জন্য নিজের SQL কোয়েরি লিখতে দেয়।
আপনার অ্যাপের প্রয়োজনীয় ডেটাবেস ইন্টার্যাকশন নির্ধারণ করতে, কীভাবে দুই ধরনের DAO ফাংশন ব্যবহার করতে হয় তা নিম্নলিখিত বিভাগগুলি থেকে জানতে পারবেন।
কনভেনিয়েন্স ফাংশন
SQL স্টেটমেন্ট লেখার প্রয়োজন ছাড়াই, ইনসার্শন, আপডেট ও ডিলিশন পারফর্ম করে এমন ফাংশন সংজ্ঞায়িত করার জন্য রুম কনভেনিয়েন্স অ্যানোটেশন প্রদান করে।
আপনি যদি আরও জটিল ইনসার্শন, আপডেট বা মুছে ফেলা সংক্রান্ত কাজ করতে চান অথবা যদি ডেটাবেসে ডেটা কোয়েরি করার প্রয়োজন হয়, তাহলে তার পরিবর্তে কোয়েরি ফাংশন ব্যবহার করুন।
ইনসার্ট
@Insert অ্যানোটেশন আপনাকে এমন ফাংশন নির্ধারণ করতে দেয় যা ডেটাবেসের উপযুক্ত টেবিলে
সেগুলির প্যারামিটার ইনসার্ট করে। নিচে দেওয়া কোডে
বৈধ @Insert ফাংশনের উদাহরণ দেখানো হয়েছে, যা ডেটাবেসে এক বা একাধিক User অবজেক্ট
ইনসার্ট করে:
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUsers(vararg users: User) @Insert suspend fun insertBothUsers(user1: User, user2: User) @Insert suspend fun insertUsersAndFriends(user: User, friends: List<User>) }
@Insert ফাংশনের প্রতিটি প্যারামিটারকে অবশ্যই @Entity দিয়ে অ্যানোটেশন করা Room
ডেটা এন্টিটি ক্লাস অথবা ডেটা এন্টিটি
ক্লাস ইনস্ট্যান্সের সংগ্রহ হতে হবে। @Insert ফাংশন কল করা হলে, Room পাস করা প্রতিটি
এনটিটি ইনস্ট্যান্সকে সংশ্লিষ্ট ডেটাবেস টেবিলে ইনসার্ট করে।
@Insert ফাংশন একটি প্যারামিটার পেলে, এটি একটি Long
ভ্যালু রিটার্ন করতে পারে, যা হল যোগ করা আইটেমের জন্য নতুন rowId। প্যারামিটারটি কোনও
অ্যারে বা সংগ্রহ হলে, এর পরিবর্তে Long
ভ্যালুর একটি অ্যারে বা সংগ্রহ রিটার্ন করা উচিত, যেখানে প্রতিটি ভ্যালু হল একটি করে যোগ করা আইটেমের rowId।
rowId ভ্যালু ফেরত দেওয়া সম্পর্কে আরও জানতে, @Insert অ্যানোটেশন ও rowid টেবিলের জন্য SQLite ডকুমেন্টেশন
সংক্রান্ত রেফারেন্স দেখুন।
আপডেট করুন
@Update অ্যানোটেশন আপনাকে এমন ফাংশন নির্ধারণ করতে দেয় যা ডেটাবেস টেবিলের নির্দিষ্ট
সারি আপডেট করে। @Insert ফাংশনের মতো, @Update ফাংশন প্যারামিটার হিসেবে
ডেটা এন্টিটি ইনস্ট্যান্স গ্রহণ করে। নিচের কোডে এমন একটি
@Update ফাংশনের উদাহরণ দেখানো হয়েছে যা
ডেটাবেসে এক বা একাধিক User অবজেক্ট আপডেট করার চেষ্টা করে:
@Dao interface UserDao { @Update suspend fun updateUsers(vararg users: User) }
আর্গুমেন্টে থাকা এন্টিটি ইনস্ট্যান্সকে ডেটাবেসের সারিগুলির সাথে ম্যাচ করাতে Room প্রাইমারি কী ব্যবহার করে। একই প্রাইমারি কী সহ কোনও সারি না থাকলে, Room কোনও পরিবর্তন করে না।
@Update ফাংশন ঐচ্ছিকভাবে Int ভ্যালু রিটার্ন করতে পারে যা সফলভাবে আপডেট করা
সারির সংখ্যা নির্দেশ করে।
মুছুন
@Delete অ্যানোটেশন আপনাকে
এমন ফাংশন নির্ধারণ করতে দেয় যা ডেটাবেস টেবিল থেকে নির্দিষ্ট সারি মুছে দেয়। Like
@Insert ফাংশনের মতো, @Delete ফাংশনগুলি ডেটা এন্টিটি ইনস্ট্যান্সকে
প্যারামিটার হিসেবে গ্রহণ করে। নিচের কোডে @Delete ফাংশনের একটি উদাহরণ দেখানো হয়েছে যা
ডেটাবেস থেকে এক বা একাধিক User অবজেক্ট মুছে ফেলার চেষ্টা করে:
@Dao interface UserDao { @Delete suspend fun deleteUsers(vararg users: User) }
আর্গুমেন্টে থাকা এন্টিটি ইনস্ট্যান্সকে ডেটাবেসের সারিগুলির সাথে ম্যাচ করাতে Room প্রাইমারি কী ব্যবহার করে। একই প্রাইমারি কী সহ কোনও সারি না থাকলে, Room কোনও পরিবর্তন করে না।
@Delete ফাংশন ঐচ্ছিকভাবে Int ভ্যালু রিটার্ন করতে পারে যা সফলভাবে মুছে ফেলা
সারিগুলির সংখ্যা নির্দেশ করে।
আপসার্ট
@Upsert অ্যানোটেশন আপনাকে
এমন ফাংশন নির্ধারণ করতে দেয় যা কোনও ম্যাচিং সারি না থাকলে এন্টিটি ইনস্ট্যান্স যোগ করে অথবা
একই প্রাইমারি কী সহ কোনও সারি আগে থেকেই থাকলে সেগুলি আপডেট করে।
@Insert ও @Update ফাংশনের মতো, @Upsert ফাংশন প্যারামিটার হিসেবে ডেটা এন্টিটি
ইনস্ট্যান্স গ্রহণ করে। নিচের কোডটি এমন একটি @Upsert
ফাংশনের উদাহরণ দেখায় যা ডেটাবেসে এক বা একাধিক User অবজেক্ট আপসার্ট করার চেষ্টা করে:
@Dao interface UserDao { @Upsert suspend fun upsertUsers(vararg users: User) }
@Upsert ফাংশন একটি প্যারামিটার পেলে, এটি একটি Long
ভ্যালু রিটার্ন করতে পারে। এর ফলে নতুন সারি যোগ করা হলে, এটি নতুন যোগ করা সারির rowId
রিটার্ন করে। এর ফলে আগে থেকে থাকা কোনও সারি আপডেট করা হলে, এটি
-1 রিটার্ন করে। প্যারামিটারটি অ্যারে বা কালেকশন হলে, এর পরিবর্তে এটি Long ভ্যালুর অ্যারে বা
কালেকশন রিটার্ন করা উচিত।
কোয়েরি ফাংশন
@Query অ্যানোটেশন আপনাকে
SQL স্টেটমেন্ট লিখতে এবং DAO ফাংশন হিসেবে সেগুলি এক্সপোজ করতে দেয়। আপনার অ্যাপের ডেটাবেস থেকে ডেটা কোয়েরি করতে অথবা আরও জটিল
ইনসার্শন, আপডেট ও মুছে দেওয়ার মতো কাজ করার প্রয়োজন হলে, এইসব কোয়েরি ফাংশন ব্যবহার করুন।
কম্পাইল করার সময় Room SQL কোয়েরি যাচাই করে। এর অর্থ হল, আপনার কোয়েরিতে কোনও সমস্যা থাকলে, রানটাইম ব্যর্থতার পরিবর্তে কম্পাইলেশন সংক্রান্ত সমস্যা হয়।
সহজ কোয়েরি
নিম্নলিখিত কোড একটি ফাংশন নির্ধারণ করে যা ডেটাবেসের সব
User অবজেক্ট রিটার্ন করার জন্য SELECT কোয়েরি ব্যবহার করে:
@Query("SELECT * FROM user") suspend fun loadAllUsers(): List<User>
নিম্নলিখিত বিভাগে, সাধারণ ব্যবহারের ক্ষেত্রে এই উদাহরণ কীভাবে পরিবর্তন করতে হয় তা দেখানো হয়েছে ।
টেবিলের কলামের সাবসেট রিটার্ন করা
বেশিরভাগ ক্ষেত্রে, আপনি যে টেবিল কোয়েরি করছেন, আপনাকে শুধু সেই টেবিলের কলামের সাবসেট রিটার্ন করতে হবে। যেমন, আপনার UI কোনও ব্যবহারকারীর সম্পর্কে প্রতিটি বিবরণের পরিবর্তে শুধুমাত্র প্রথম ও শেষ নাম দেখাতে পারে। রিসোর্স সেভ করতে এবং আপনার কোয়েরি এক্সিকিউট করার প্রসেসকে আরও সহজ করতে, শুধুমাত্র সেইসব প্রপার্টি কোয়েরি করুন যেগুলি আপনার প্রয়োজন।
আপনি যতক্ষণ না পর্যন্ত ফলাফলের কলামের সেটকে ফেরত আসা অবজেক্টের সাথে ম্যাপ করতে পারছেন, ততক্ষণ পর্যন্ত Room আপনাকে যেকোনও কোয়েরি থেকে ডেটা অবজেক্ট ফেরত দিতে দেয়। যেমন, ব্যবহারকারীর নাম ও পদবি ধরে রাখার জন্য আপনি নিম্নলিখিত অবজেক্টটি নির্ধারণ করতে পারেন:
data class NameTuple( @ColumnInfo(name = "first_name") val firstName: String, @ColumnInfo(name = "last_name") val lastName: String )
তারপরে, আপনার কোয়েরি ফাংশন থেকে সেই ডেটা অবজেক্ট রিটার্ন করতে পারবেন:
@Query("SELECT first_name, last_name FROM user") suspend fun loadFullName(): List<NameTuple>
কোয়েরি first_name ও last_name কলামের ভ্যালু রিটার্ন করে বলে,
Room NameTuple ক্লাসের প্রপার্টির সাথে এইসব ভ্যালু ম্যাপ করে। কোয়েরি
যদি এমন কলাম রিটার্ন করে যা রিটার্ন করা অবজেক্টের
প্রপার্টির সাথে ম্যাপ করে না, তাহলে Room একটি সতর্কতা দেখায়।
যদিও আগের উদাহরণে কলামের সাবসেট
রিট্রিভ করার জন্য কাস্টম ডেটা ক্লাস ব্যবহার করা হয়েছে, তবে কোনও কোয়েরি ঠিক দুটি বা তিনটি কলাম রিটার্ন করলে, Room kotlin.Pair ও kotlin.Triple রিটার্ন করার ক্ষেত্রেও
সুবিধা দেয়। এইসব
ধরনের ডেটা ব্যবহার করার সময়, কলামগুলিকে কোয়েরি
স্টেটমেন্টে যেভাবে ডিফাইন করা হয়েছে সেই ক্রম অনুযায়ী ম্যাপ করা হয়, তাই SELECT স্টেটমেন্টের কলামের ক্রমকে Pair বা Triple-এর
ধরনের ক্রমের সাথে মিলতে হবে।
কোয়েরিতে সাধারণ প্যারামিটার পাস করা
বেশিরভাগ ক্ষেত্রে, আপনার DAO ফাংশনকে প্যারামিটার গ্রহণ করতে হবে যাতে সেগুলি ফিল্টার করার অপারেশন পারফর্ম করতে পারে। আপনার কোয়েরিতে Room, ফাংশন প্যারামিটারকে bind প্যারামিটার হিসেবে ব্যবহার করার সুবিধা দেয়।
যেমন, নিম্নলিখিত কোডটি এমন একটি ফাংশন নির্ধারণ করে যা নির্দিষ্ট বয়সের উপরের সব ব্যবহারকারীকে রিটার্ন করে:
@Query("SELECT * FROM user WHERE age > :minAge") suspend fun loadAllUsersOlderThan(minAge: Int): Array<User>
এছাড়াও, আপনি কোয়েরিতে একাধিক প্যারামিটার পাস করতে পারেন অথবা একই প্যারামিটার একাধিকবার রেফার করতে পারেন, যেমনটি নিম্নলিখিত কোডে দেখানো হয়েছে:
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge") suspend fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User> @Query( """ SELECT * FROM user WHERE first_name LIKE :search OR last_name LIKE :search """ ) suspend fun findUserWithName(search: String): List<User>
কোয়েরিতে প্যারামিটারের সংগ্রহ পাস করা
আপনার DAO ফাংশনের কিছু ক্ষেত্রে, রানটাইম পর্যন্ত অজানা সংখ্যক ভেরিয়েবল প্যারামিটার পাস করতে হতে পারে। কোনও প্যারামিটার যদি কোনও সংগ্রহকে উপস্থাপন করে, তাহলে ভ্যালুর সংখ্যার উপর ভিত্তি করে রানটাইমে এটি অটোমেটিক এক্সপ্যান্ড হয়ে যায়।
যেমন, নিম্নলিখিত কোড এমন একটি ফাংশন নির্ধারণ করে যা অঞ্চলের একটি সাবসেট থেকে সমস্ত ব্যবহারকারীর সম্পর্কে তথ্য প্রদান করে:
@Query("SELECT * FROM user WHERE region IN (:regions)") suspend fun loadUsersFromRegions(regions: List<String>): List<User>
একাধিক টেবিল কোয়েরি করা
আপনার কিছু কোয়েরির ফলাফল গণনা করার জন্য একাধিক টেবিল অ্যাক্সেস করার প্রয়োজন হতে পারে।
একাধিক টেবিলের রেফারেন্স দিতে, আপনার SQL কোয়েরিতে JOIN ক্লজ ব্যবহার করতে পারেন।
নিচের কোডটি এমন একটি ফাংশনকে সংজ্ঞায়িত করে যা তিনটি টেবিলকে একসাথে যোগ করে সেইসব বইয়ের তথ্য রিটার্ন করে যেগুলি বর্তমানে কোনও নির্দিষ্ট ব্যবহারকারীকে লোন দেওয়া হয়েছে:
@Query( """ SELECT * FROM book INNER JOIN loan ON loan.book_id = book.id INNER JOIN user ON user.id = loan.user_id WHERE user.name LIKE :userName """ ) suspend fun findBooksBorrowedByName(userName: String): List<Book>
এছাড়াও, একাধিক জয়েন করা টেবিল থেকে কলামের সাবসেট রিটার্ন করার জন্য আপনি ডেটা অবজেক্ট ডিফাইন করতে পারেন। আরও তথ্যের জন্য, টেবিলের কলামের সাবসেট রিটার্ন করা দেখুন। নিচের কোডটি একটি ফাংশন সহ DAO-কে সংজ্ঞায়িত করে যা ব্যবহারকারীদের নাম এবং তাদের ধার করা বইয়ের নাম রিটার্ন করে:
interface UserBookDao { @Query( """ SELECT user.name AS userName, book.name AS bookName FROM user, book WHERE user.id = book.user_id """ ) fun loadUserAndBookNames(): Flow<List<UserBook>> } data class UserBook(val userName: String, val bookName: String)
মাল্টিম্যাপ ফেরত দেওয়া
এছাড়াও, জয়েন করার অপারেশনের জন্য, আপনি অতিরিক্ত ডেটা ক্লাসকে সংজ্ঞায়িত না করেই একাধিক টেবিল থেকে কলাম কোয়েরি করতে পারেন। এর জন্য আপনাকে এমন কোয়েরি ফাংশন লিখতে হবে যা মাল্টিম্যাপ রিটার্ন করে।
একাধিক টেবিল কোয়েরি করা থেকে উদাহরণটি বিবেচনা করুন। User এবং
Book ইনস্ট্যান্সের পেয়ারিং হোল্ড করে এমন কাস্টম ডেটা ক্লাসের
ইনস্ট্যান্সের তালিকা রিটার্ন করার পরিবর্তে, আপনি সরাসরি কোয়েরি ফাংশন থেকে
User এবং Book-এর ম্যাপিং রিটার্ন করতে পারেন:
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNames(): Map<User, List<Book>>
আপনার কোয়েরি ফাংশন একটি মাল্টিম্যাপ রিটার্ন করলে, আপনি এমন কোয়েরি লিখতে পারবেন যা
GROUP BY ক্লজ ব্যবহার করে, এর ফলে আপনি
উন্নত গণনা ও ফিল্টারিংয়ের জন্য SQL-এর ক্ষমতা ব্যবহার করতে পারবেন। যেমন, আপনি নিজের
loadUserAndBookNames ফাংশন পরিবর্তন করে শুধুমাত্র তিন বা তার বেশি বই
চেক-আউট করা ব্যবহারকারীদের তথ্য রিটার্ন করতে পারেন:
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id GROUP BY user.name HAVING COUNT(book.id) >= 3 """ ) suspend fun loadUserAndBookNamesGrouped(): Map<User, List<Book>>
আপনাকে যদি সম্পূর্ণ অবজেক্ট ম্যাপ করতে না হয়, তাহলে আপনি রিটার্ন টাইপের জেনেরিক প্যারামিটারে @MapColumn অ্যানোটেশন ব্যবহার করে কোয়েরির
নির্দিষ্ট কলামের মধ্যে ম্যাপিংও রিটার্ন করতে পারেন।
@Query( """ SELECT user.name AS username, book.name AS bookname FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNamesColumns(): Map< @MapColumn(columnName = "username") String, List<@MapColumn(columnName = "bookname") String> >
বিশেষ রিটার্ন টাইপ
অন্যান্য API লাইব্রেরির সাথে ইন্টিগ্রেশনের জন্য Room কিছু বিশেষ রিটার্ন টাইপ প্রদান করে।
পেজিং লাইব্রেরি সহ পৃষ্ঠা নম্বর দেওয়া কোয়েরি
Room, পেজিং লাইব্রেরির সাথে ইন্টিগ্রেশনের মাধ্যমে পৃষ্ঠা নম্বর অনুযায়ী কোয়েরি করার সুবিধা দেয়। Paging 3 রিটার্ন টাইপ ব্যবহার করতে হলে, আপনার ডেটাবেস বা DAO-তে Paging রিটার্ন টাইপ কনভার্টার রেজিস্টার করতে হবে:
- আপনার বিল্ড
কনফিগারেশনে
androidx.room3:room3-pagingআর্টিফ্যাক্ট অন্তর্ভুক্ত করুন। - আপনার
@Databaseবা@Daoঘোষণায়@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)যোগ করুন।
রেজিস্টার করা হয়ে গেলে, আপনার DAO PagingSource অবজেক্ট রিটার্ন করতে পারে যা
পেজিং ৩-এর সাথে ব্যবহার করা হয়:
@Dao @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) interface UserDao { @Query("SELECT * FROM users WHERE label LIKE :query") fun pagingSource(query: String): PagingSource<Int, User> }
PagingSource-এর জন্য টাইপ প্যারামিটার বেছে নেওয়া সম্পর্কে আরও জানতে, কী ও ভ্যালু টাইপ বেছে নিন দেখুন।
সরাসরি ডেটাবেস কানেকশন অ্যাক্সেস
আপনার অ্যাপের লজিক যদি সরাসরি, লো-লেভেল অ্যাক্সেস করার জন্য ডেটাবেস
কানেকশন চায়, তাহলে আপনি এর পরিবর্তে Room-এর কানেকশন API ব্যবহার করতে পারেন। আপনি শুধু-পঠন অপারেশন বা আপনার RoomDatabase ইনস্ট্যান্সে লেখার অপারেশন করার জন্য
useReaderConnection
ব্যবহার করে কানেকশন পেতে পারেন এবং স্টেটমেন্ট এক্সিকিউট করতে
usePrepared
ব্যবহার করতে পারেন:useWriterConnection
val result: List<Pair<Long, String>> = roomDatabase.useReaderConnection { connection -> connection.usePrepared( "SELECT * FROM user WHERE age > :minAge LIMIT 5" ) { stmt -> // Bind arguments if needed stmt.bindLong(1, minAge.toLong()) buildList { // Step through the results while (stmt.step()) { add(stmt.getLong(0) to stmt.getText(1)) } } } }
কানেকশনের
উপর সরাসরি লো-লেভেল ডেটাবেস ট্রানজ্যাকশন পারফর্ম করতে হলে, আপনি useWriterConnection ব্লকের মধ্যে
Transactor ইনস্ট্যান্সে immediateTransaction,
deferredTransaction বা exclusiveTransaction হেল্পার ফাংশন ব্যবহার করতে পারেন:
roomDatabase.useWriterConnection { transactor -> transactor.immediateTransaction { // Perform transactional database operations using transactor } }
অথবা, আপনি যদি কোনও
ট্রানজ্যাকশনে শুধুমাত্র হাই-লেভেল DAO অপারেশন এক্সিকিউট করতে চান, তাহলে আপনার RoomDatabase ইনস্ট্যান্সে withReadTransaction বা withWriteTransaction
সহায়ক এক্সটেনশন ফাংশন ব্যবহার করুন:
// Perform transactional read operations (DEFERRED transaction) val userCount = roomDatabase.withReadTransaction { userDao.countUsers() } // Perform transactional write operations (IMMEDIATE transaction) roomDatabase.withWriteTransaction { userDao.insert(newUser) userDao.update(existingUser) }