在 Room 数据库中使用事务处理联表操作

首先是错误版本的相关代码内容:

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 {
    @Insert
    suspend fun insertEvent(event: Event): Long

    @Query("SELECT * FROM events WHERE eventId = :id")
    suspend fun getEventById(id: Long): Event

    @Delete
    suspend fun deleteEvent(event: Event)

    @Insert
    suspend 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 {
    @Insert
    suspend fun insertAttendee(attendee: Attendee)

    @Query("SELECT * FROM attendees WHERE eventId = :eventId")
    suspend fun getAttendeesForEvent(eventId: Long): List<Attendee>

    @Delete
    suspend fun deleteAttendee(attendee: Attendee)
}

EventWithAttendees.kt

import androidx.room.Embedded
import androidx.room.Relation
import com.plcoding.roomtransactions.Attendee
import com.plcoding.roomtransactions.Event

data 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(): EventDao
    abstract 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
    @Singleton
    fun 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 {
    @Insert
    suspend fun insertEvent(event: Event): Long

    @Query("SELECT * FROM events WHERE eventId = :id")
    suspend fun getEventById(id: Long): Event

    @Delete
    suspend fun deleteEvent(event: Event)

    @Insert
    suspend fun insertAttendees(attendees: List<Attendee>)

    @Transaction
    suspend 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)
        }
    }
}

这里只需要调用上面定义的事务方法即可。

  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值