首先是错误版本的相关代码内容:
Event.kt
:
import androidx.room.Entity
import androidx.room.PrimaryKey@Entity(tableName = "events")
data class Event(val title: String,val description: String,val timestamp: Long,@PrimaryKey(autoGenerate = true) val eventId: Long = 0L
)
EventDao.kt
:
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query@Dao
interface EventDao {@Insertsuspend fun insertEvent(event: Event): Long@Query("SELECT * FROM events WHERE eventId = :id")suspend fun getEventById(id: Long): Event@Deletesuspend fun deleteEvent(event: Event)@Insertsuspend fun insertAttendees(attendees: List<Attendee>)
}
Attendee.kt
:
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey@Entity(tableName = "attendees",foreignKeys = [ForeignKey(entity = Event::class,parentColumns = ["eventId"],childColumns = ["eventId"],onDelete = ForeignKey.CASCADE)]
)
data class Attendee(val eventId: Long,val name: String,val profilePictureUrl: String?,@PrimaryKey(autoGenerate = true) val attendeeId: Long = 0L
)
AttendeeDao.kt
:
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import com.plcoding.roomtransactions.Attendee@Dao
interface AttendeeDao {@Insertsuspend fun insertAttendee(attendee: Attendee)@Query("SELECT * FROM attendees WHERE eventId = :eventId")suspend fun getAttendeesForEvent(eventId: Long): List<Attendee>@Deletesuspend fun deleteAttendee(attendee: Attendee)
}
EventWithAttendees.kt
:
import androidx.room.Embedded
import androidx.room.Relation
import com.plcoding.roomtransactions.Attendee
import com.plcoding.roomtransactions.Eventdata class EventWithAttendees(@Embedded val event: Event,@Relation(parentColumn = "eventId",entityColumn = "eventId")val attendees: List<Attendee>
)
AppDatabase.kt
:
import androidx.room.Database
import androidx.room.RoomDatabase@Database(entities = [Event::class, Attendee::class], version = 1)
abstract class AppDatabase : RoomDatabase() {abstract fun eventDao(): EventDaoabstract fun attendeeDao(): AttendeeDao
}
AppModule.kt
:
import android.app.Application
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton@Module
@InstallIn(SingletonComponent::class)
object AppModule {@Provides@Singletonfun provideAppDatabase(app: Application): AppDatabase {return Room.databaseBuilder(app, AppDatabase::class.java, "app.db").build()}
}
以上代码中 Event
实体和 Attendee
实体之间通过EventWithAttendees
定义了一对多的关系。
更多关于 Room 数据库中对象与对象之间的关系定义,请参考Jetpack架构组件库:Room。
MainViewModel.kt
:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject@HiltViewModel
class MainViewModel @Inject constructor(private val db: AppDatabase
): ViewModel() {fun insertEvent() {viewModelScope.launch {val event = Event(title = "Test event",description = "My event",timestamp = System.currentTimeMillis())val eventId = db.eventDao().insertEvent(event)val attendees = (1..10).map {Attendee(eventId = eventId,name = "Test attendee$it",profilePictureUrl = null)}attendees.forEach {db.attendeeDao().insertAttendee(it)}}}
}
MainActivity.kt
:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.plcoding.roomtransactions.ui.theme.RoomTransactionsTheme
import dagger.hilt.android.AndroidEntryPoint@AndroidEntryPoint
class MainActivity : ComponentActivity() {private val viewModel by viewModels<MainViewModel>()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {RoomTransactionsTheme {Column(modifier = Modifier.fillMaxSize(),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {Button(onClick = {viewModel.insertEvent()}) {Text(text = "Insert event")}}}}}
}
App.kt
:
import android.app.Application
import dagger.hilt.android.HiltAndroidApp@HiltAndroidApp
class App: Application()
在MainViewModel
中先向event
表中插入event
对象,拿到 eventId
构造出 attendees
对象,再插入到 Attendee
表中。
这里的主要问题是下面三部分代码:
val eventId = db.eventDao().insertEvent(event)
val attendees = (1..10).map {Attendee(eventId = eventId,name = "Test attendee$it",profilePictureUrl = null)
}
attendees.forEach {db.attendeeDao().insertAttendee(it)
}
这三块代码的操作不是原子性的,假设第一块代码向数据库插入成功了,但是在跑第二块代码时应用崩溃了,第三块代码就没有执行,从而导致bug。同样的,如果第一块代码和第二块代码执行成功了,但是第三块代码执行失败了,前面部分的操作也不会回滚,从而导致bug。
正确的处理方法是使用 Room 数据库中的@Transaction
事务来处理多个数据库操作:
修改EventDao.kt
内容如下:
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow@Dao
interface EventDao {@Insertsuspend fun insertEvent(event: Event): Long@Query("SELECT * FROM events WHERE eventId = :id")suspend fun getEventById(id: Long): Event@Deletesuspend fun deleteEvent(event: Event)@Insertsuspend fun insertAttendees(attendees: List<Attendee>)@Transactionsuspend fun insertEventWithAttendees(event: Event, attendees: List<Attendee>) {val eventId = insertEvent(event)val attendeesWithEventId = attendees.map {it.copy(eventId = eventId)}insertAttendees(attendeesWithEventId)}
}
这里主要增加了insertEventWithAttendees
方法,并为其添加@Transaction
注解,在其中执行相关的表操作。这样表示insertEventWithAttendees
方法中对数据库表的操作,要么全部成功,要么全部失败,即便在中间某个操作应用崩溃了,也不会只执行了一半,即保证事务是原子性的。
然后修改MainViewModel.kt
内容如下:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject@HiltViewModel
class MainViewModel @Inject constructor(private val db: AppDatabase
): ViewModel() {fun insertEvent() {viewModelScope.launch {val event = Event(title = "Test event",description = "My event",timestamp = System.currentTimeMillis())val attendees = (1..10).map {Attendee(eventId = 0,name = "Test attendee$it",profilePictureUrl = null)}db.eventDao().insertEventWithAttendees(event, attendees)}}
}
这里只需要调用上面定义的事务方法即可。