관리 메뉴

막내의 막무가내 프로그래밍 & 일상

[안드로이드] 구글 공식 프로젝트 Sunflower 스터디 (7) MVVM - Model 본문

안드로이드/코틀린 & 아키텍처 & Recent

[안드로이드] 구글 공식 프로젝트 Sunflower 스터디 (7) MVVM - Model

막무가내막내 2021. 5. 9. 00:00
728x90

 

 

[출처 및 참고] 

github.com/android/sunflower

 

android/sunflower

A gardening app illustrating Android development best practices with Android Jetpack. - android/sunflower

github.com

developer.android.com/training/data-storage/room/relationships?hl=ko

 

객체 간 관계 정의  |  Android 개발자  |  Android Developers

SQLite는 관계형 데이터베이스이므로 항목 간 관계를 지정할 수 있습니다. 대부분의 객체 관계 매핑(ORM) 라이브러리에서는 항목 객체가 서로를 참조할 수 있지만, Room은 이러한 상호 참조를 명시

developer.android.com

medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1#4785

 

7 Pro-tips for Room

Learn how you can get the most out of Room

medium.com

medium.com/harrythegreat/%EB%B2%88%EC%97%AD-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-room-7%EA%B0%80%EC%A7%80-%EC%9C%A0%EC%9A%A9%ED%95%9C-%ED%8C%81-18252a941e27

 

[번역] 안드로이드 Room 7가지 유용한 팁

본 내용은 원작자의 허락을 맡아 번역한 내용이며 개인적인 커멘츠는 역주로 표기하였습니다.

medium.com

www.charlezz.com/?p=368

 

Android Architecture Component – Room | 찰스의 안드로이드

Room을 활용하여 Local 데이터베이스에 data를 저장해보자 Room 은 SQLite를 추상계층으로 감싸고 있으며, 쉽게 데이터베이스에 접근하여 SQLite를 마구마구 자유롭게 풀파워로 사용할 수 있다. Room을 사

www.charlezz.com

youngest-programming.tistory.com/456

 

[안드로이드] Room 로컬 데이터베이스에서 기본형이 아닌 객체 필드값 저장방법 (feat. @Embeded, @TypeC

[2021-04-14 업데이트]  [2020.12.16 블로그 포스팅 스터디 3 번째 글] github.com/mtjin/NoMoneyTrip mtjin/NoMoneyTrip [SKT/한국관광공사] 2020 스마트 관광 앱 개발 공모전 '무전여행' 앱. Contribute to mtj..

youngest-programming.tistory.com

 

 

[Model]

이전 챕터에서 ViewModel에 대해 보았는데 이어서 MVVM에서 서버 or 로컬 DB와의 통신을 담당하는 Data Layer인 Repository 클래스를 살펴보려고한다. 

 

 


 

 

[PlantRepository]

/**
 * Repository module for handling data operations.
 *
 * Collecting from the Flows in [PlantDao] is main-safe.  Room supports Coroutines and moves the
 * query execution off of the main thread.
 */
@Singleton
class PlantRepository @Inject constructor(private val plantDao: PlantDao) {

    fun getPlants() = plantDao.getPlants()

    fun getPlant(plantId: String) = plantDao.getPlant(plantId)

    fun getPlantsWithGrowZoneNumber(growZoneNumber: Int) =
        plantDao.getPlantsWithGrowZoneNumber(growZoneNumber)
}

Dao로 부터 데이터를 불러오는 함수가 구현되어 있다. DI 관련은 이전 챕터에서 했으므로 생략한다.

Room의 구성요소인 Entity, Dao, RoomDatabase도 한번 살펴보겠다.

 

 

[Entity]

@Entity(tableName = "plants")
data class Plant(
    @PrimaryKey @ColumnInfo(name = "id") val plantId: String,
    val name: String,
    val description: String,
    val growZoneNumber: Int,
    val wateringInterval: Int = 7, // how often the plant should be watered, in days
    val imageUrl: String = ""
) {

    /**
     * Determines if the plant should be watered.  Returns true if [since]'s date > date of last
     * watering + watering Interval; false otherwise.
     */
    fun shouldBeWatered(since: Calendar, lastWateringDate: Calendar) =
        since > lastWateringDate.apply { add(DAY_OF_YEAR, wateringInterval) }

    override fun toString() = name
}

먼저 @Entity + tableName 속성으로 테이블 이름이 "plants"로 설정되어 있다. 그리고 plantId 가 테이블상 이름으로는 @ColumInfo 로 "id" 라는 이름을 갖게되고 @PrimaryKey로 고유키가 된다. 

 

 

[PlantDao]

@Dao
interface PlantDao {
    @Query("SELECT * FROM plants ORDER BY name")
    fun getPlants(): Flow<List<Plant>>

    @Query("SELECT * FROM plants WHERE growZoneNumber = :growZoneNumber ORDER BY name")
    fun getPlantsWithGrowZoneNumber(growZoneNumber: Int): Flow<List<Plant>>

    @Query("SELECT * FROM plants WHERE id = :plantId")
    fun getPlant(plantId: String): Flow<Plant>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(plants: List<Plant>)
}

@Dao 로 Dao 인터페이스임을 알려준다. 그리고 @Query 를 사용하여 쿼리문을 작성할 수 있고 함수의 파라미터는 쿼리문에서 ' : ' 를 앞에 붙임으로써 사용할 수 있다. 그리고 @Insert와 onConfilct 충돌전략 속성을 사용하여 데이터를 데이터베이스에 쉽게 저장할 수도 있다. 

 

그리고 코루틴에서 실행될 함수는 suspend 키워드가 함수 앞에 붙어 있는것을 확인 할 수 있다.

 

 

 

 

 

[AppDatabase]

/**
 * The Room database for this app
 */
@Database(entities = [GardenPlanting::class, Plant::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun gardenPlantingDao(): GardenPlantingDao
    abstract fun plantDao(): PlantDao

    companion object {

        // For Singleton instantiation
        @Volatile private var instance: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return instance ?: synchronized(this) {
                instance ?: buildDatabase(context).also { instance = it }
            }
        }

        // Create and pre-populate the database. See this article for more details:
        // https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1#4785
        private fun buildDatabase(context: Context): AppDatabase {
            return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
                .addCallback(
                    object : RoomDatabase.Callback() {
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            super.onCreate(db)
                            val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build()
                            WorkManager.getInstance(context).enqueue(request)
                        }
                    }
                )
                .build()
        }
    }
}

RoomDatabase는 RoomDatabase()를 상속받아야한다. @Database 에는 테이블을 담당하는 Entity들과 데이터베이스 관련 속성들이 들어가 있다. 그리고 @TypeConverter도 있는데 테이블에 저장할 값 타입을 변환하거나 다른 형태로 변환할 경우 사용한다. (ex. 데이터베이스에는 List타입을 저장못해 json형식으로 변환해야한다.) 이에 대해서는 youngest-programming.tistory.com/456 에서 다룬적이 있다.

 

그리고 데이터베이스가 계속 생성되어 메모리가 낭비되는 것을 방지하기 위해 @Volatule과 getInstance() 함수로 싱글톤으로 데이터베이스가 생성되게 되어있다.

 

마지막으로 buildDatabase()를 살펴보면 RoomDatabase를 빌드(생성)하는 건데 데이터베이스의 이름을 설정할 수 있다. 그리고 여기서는 RoomDatabase callback을 사용해서 데이터베이스가 생성되면 한번만 실행되는 OneTimeWorkRequest 형식의 WorkManager 컴포넌트를 사용하여 데이터베이스에 초기값을 넣는 로직이 실행된다. (WorkManager는 뒤에 챕터에서 다룰예정이다.) buildDatabase()에 대한 설명과 이점은 주석에도 적혀있듯이 medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1#4785 에서 자세히 볼 수 있다.

 

 

 

 

[Converters]

/**
 * Type converters to allow Room to reference complex data types.
 */
class Converters {
    @TypeConverter fun calendarToDatestamp(calendar: Calendar): Long = calendar.timeInMillis

    @TypeConverter fun datestampToCalendar(value: Long): Calendar =
        Calendar.getInstance().apply { timeInMillis = value }
}

TypeConverter는 위와 같이 구현되어 있다. 타임스탬프를 날짜로 변환하는데 사용한다.



 

 


 

 

[GardenPlantingRepository]

@Singleton
class GardenPlantingRepository @Inject constructor(
        private val gardenPlantingDao: GardenPlantingDao
) {

    suspend fun createGardenPlanting(plantId: String) {
        val gardenPlanting = GardenPlanting(plantId)
        gardenPlantingDao.insertGardenPlanting(gardenPlanting)
    }

    suspend fun removeGardenPlanting(gardenPlanting: GardenPlanting) {
        gardenPlantingDao.deleteGardenPlanting(gardenPlanting)
    }

    fun isPlanted(plantId: String) =
            gardenPlantingDao.isPlanted(plantId)

    fun getPlantedGardens() = gardenPlantingDao.getPlantedGardens()
}

위에서 본 것과 똑같이 Dao 인터페이스의 함수를 호출하고 있다. 그리고 코루틴에서 실행될 함수 앞에 suspend가 붙어 있는것을 확인할 수 있다.

이와 관련된 Entity 클래스와 Dao 인터페이스를 살펴보겠다.

 

 

[Entity]

/**
 * [GardenPlanting] represents when a user adds a [Plant] to their garden, with useful metadata.
 * Properties such as [lastWateringDate] are used for notifications (such as when to water the
 * plant).
 *
 * Declaring the column info allows for the renaming of variables without implementing a
 * database migration, as the column name would not change.
 */
@Entity(
    tableName = "garden_plantings",
    foreignKeys = [
        ForeignKey(entity = Plant::class, parentColumns = ["id"], childColumns = ["plant_id"])
    ],
    indices = [Index("plant_id")]
)
data class GardenPlanting(
    @ColumnInfo(name = "plant_id") val plantId: String,

    /**
     * Indicates when the [Plant] was planted. Used for showing notification when it's time
     * to harvest the plant.
     */
    @ColumnInfo(name = "plant_date") val plantDate: Calendar = Calendar.getInstance(),

    /**
     * Indicates when the [Plant] was last watered. Used for showing notification when it's
     * time to water the plant.
     */
    @ColumnInfo(name = "last_watering_date")
    val lastWateringDate: Calendar = Calendar.getInstance()
) {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var gardenPlantingId: Long = 0
}

처음 본 Entity와 다른점이 몇가지가 있는데 먼저 foreignKeys 즉 외래키 제약이 설정되어 있다.

 

외부키 제약을 맺음으로서 참조한 엔티티가 업데이트 될때 어떤일이 발생했는지 명시하는것을 허용하고 있다. @ForeignKey(onDelete=CASCADE) 어노테이션을 가지고 있는 어떤 Plant(parent) 객체가 삭제된다면 모든 해당 Plant의 모든 GardenPlanting(child)을 삭제하라는 명령을 SQLite에게 줄수도 있다.

 

Plant라는 Entity와 관계를 맺을거고 parentColums상위 항목의 기본 키 열 이름으로 설정(Plant Entity의 id)하고 childColumns에는 현재의 Entity인 GardenPlanting의 plant_id 키를 설정한다. 

이때 parentColums와 childColumns 개수는 동일해야한다.

 

indicies는 인덱싱을 해서 SELECT 쿼리의 속도를 높여주는 역할을 한다. plant_id 로 인덱싱을 설정하였다. 만약 인덱스를 따로 설정하지 않으면  Room에서 index_${tableName} 로 기본값을 설정해놓는다.

 

자세한 내용은 다음을 세개의 링크를 참고하면 된다.

developer.android.com/training/data-storage/room/relationships?hl=ko

 

객체 간 관계 정의  |  Android 개발자  |  Android Developers

SQLite는 관계형 데이터베이스이므로 항목 간 관계를 지정할 수 있습니다. 대부분의 객체 관계 매핑(ORM) 라이브러리에서는 항목 객체가 서로를 참조할 수 있지만, Room은 이러한 상호 참조를 명시

developer.android.com

medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1#4785

 

7 Pro-tips for Room

Learn how you can get the most out of Room

medium.com

medium.com/harrythegreat/%EB%B2%88%EC%97%AD-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-room-7%EA%B0%80%EC%A7%80-%EC%9C%A0%EC%9A%A9%ED%95%9C-%ED%8C%81-18252a941e27

 

[번역] 안드로이드 Room 7가지 유용한 팁

본 내용은 원작자의 허락을 맡아 번역한 내용이며 개인적인 커멘츠는 역주로 표기하였습니다.

medium.com

www.charlezz.com/?p=368

 

Android Architecture Component – Room | 찰스의 안드로이드

Room을 활용하여 Local 데이터베이스에 data를 저장해보자 Room 은 SQLite를 추상계층으로 감싸고 있으며, 쉽게 데이터베이스에 접근하여 SQLite를 마구마구 자유롭게 풀파워로 사용할 수 있다. Room을 사

www.charlezz.com

 

 

 

[Dao]

@Dao
interface GardenPlantingDao {
    @Query("SELECT * FROM garden_plantings")
    fun getGardenPlantings(): Flow<List<GardenPlanting>>

    @Query("SELECT EXISTS(SELECT 1 FROM garden_plantings WHERE plant_id = :plantId LIMIT 1)")
    fun isPlanted(plantId: String): Flow<Boolean>

    /**
     * This query will tell Room to query both the [Plant] and [GardenPlanting] tables and handle
     * the object mapping.
     */
    @Transaction
    @Query("SELECT * FROM plants WHERE id IN (SELECT DISTINCT(plant_id) FROM garden_plantings)")
    fun getPlantedGardens(): Flow<List<PlantAndGardenPlantings>>

    @Insert
    suspend fun insertGardenPlanting(gardenPlanting: GardenPlanting): Long

    @Delete
    suspend fun deleteGardenPlanting(gardenPlanting: GardenPlanting)
}

@Transaction 을 활용하여 메서드의 질의들이 하나의 트랜잭션 안에서 실행하게 구현되어있다. 여기서는 두 개의 테이블에서 쿼리하는데 그것들을 하나의 트랜잭션에서 실행시키게 된다. 트랜잭션은 메서드의 Body 내부에서 Exception이 발생하면 반영되지않는다. 

 

@Delete는 Primary Key 로 매칭하여 알아서 해당 객체를 데이터베이스에서 삭제해준다.

 

 

 

 

 

 

 

댓글과 공감은 큰 힘이 됩니다. 감사합니다. !! 

 

 

 

 

728x90
Comments