如何在房间数据库上创建同一实体的多个表?

How to create multiple tables of the same entity on Room Database?

问题

假设您有多个花园,每个花园种有不同数量的植物。你的工作是不时检查每个花园的每株植物。每次访问时,您都必须注释植物的某些属性,例如浇水是否良好和高度。该应用程序旨在在这些访问期间提供帮助。

我的做法

我想使用房间数据库。因此,我创建了一个实体 GardenVisit,它具有唯一的 ID 和访问日期。然后我需要一个 GardenAnnotation 实体。该实体将为花园中的每株植物排成一排,并在访问当天标注其 ID 和特征。我考虑过为每个 GardenVisit 创建一个 table 并且 link 它们具有一对一的关系,但我找不到方法来做到这一点。

为什么我要为每次 GardenVisit 创建一个 GardenAnnotationtable?

在应用程序中,您可以删除花园参观。因此,在删除它时,它也应该删除它的 GardenAnnotation table。这似乎是拥有此功能的最简单方法。

结论

如何在 Room 数据库中创建多个 table 同一实体,并 link 它们与另一个 table?

如果您有更好的方法,欢迎分享。实际上,为同一个实体创建很多 table 感觉很奇怪。

为了拆分基本相同的布局(架构)而使用多个 table 可能意义不大,而且可能会使事情复杂化。

根据你的描述,你有一些共同点:-

  • 花园。
  • 植物。
  • 访问次数
  • 特质。
  • 注释(findings/traits 每次访问)。

我会相应地建议 tables。

A Garden table 可能有但可能不限于花园的人类标识符(Kew,巴比伦空中花园....) (因为它已经存在并且有效)标识符 (id).

A Plant table(蒲公英、玫瑰......),其中包含关于植物的 ID、名称和其他信息的列。

一个table(没提到)那个maps/links/associates一个植物到一个花园,允许多对多的关系(一个花园可以有很多植物,一个植物可以用在很多花园) . 2 列,一列用于花园的地图,另一列用于植物。

一个Visit table 可能有访问的date/time start/end 和一个map/link....去花园。

A 特质 table 例如浇水好,死了(如果我正在照料植物)....列将是 id 和 trait(确切要求)

一个 注释 table 将 link 访问(因此花园)和 link 花园内的植物和a link 到要分配的特征。

因此模式可以基于 SQLite(以演示 database/relationships 如何从 SQLite pov 工作):-

DROP TABLE IF EXISTS annotation;
DROP TABLE IF EXISTS trait;
DROP TABLE IF EXISTS visit;
DROP TABLE IF EXISTS garden_plant_map;
DROP TABLE IF EXISTS garden;
DROP TABLE IF EXISTS plant;

CREATE TABLE IF NOT EXISTS garden (garden_id INTEGER PRIMARY KEY, garden_name TEXT UNIQUE);
INSERT INTO garden (garden_name) 
    VALUES('Kew' /* id will be 1 */),('Hanging Gardens of Babylon' /* id will be 2 and so on (probably)*/)
;

CREATE TABLE IF NOT EXISTS plant(plant_id INTEGER PRIMARY KEY, plant_name TEXT UNIQUE);
INSERT INTO plant (plant_name) 
    VALUES('Rose' /* id will be 1 etc*/),('Dandelion'),('Poppy'),('Azelia'),('Oak'),('Beech')
;
CREATE TABLE IF NOT EXISTS garden_plant_map (
    garden_map INTEGER,
    plant_map INTEGER,
    PRIMARY KEY (garden_map,plant_map)
    FOREIGN KEY (garden_map) REFERENCES garden(garden_id) ON DELETE CASCADE ON UPDATE CASCADE
    FOREIGN KEY (plant_map) REFERENCES plant(plant_id) ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO garden_plant_map
    VALUES
        (1 /* Kew */, 3 /* Poppy*/),
        (1 /* Kew */, 1 /* Rose */),
        (2 /* Babylon */, 2 /* Dandelion */),
        (2,5),(2,6) /*Oak and Beech for Babylon */
;
CREATE TABLE IF NOT EXISTS trait (trait_id INTEGER PRIMARY KEY, trait_description UNIQUE);
INSERT INTO trait (trait_description)
    VALUES ('Well watered'),('Dead'),('Stressed'),('Flourishing'),('under watered')
;
CREATE TABLE IF NOT EXISTS visit (
    visit_id INTEGER PRIMARY KEY, 
    garden_map INTEGER,
    start_of_visit TEXT /* will be date in yyyy-mm-dd hh:mm:ss format*/,
    end_of_visit TEXT,
    visit_done INTEGER, /* 0/false or 1 (or greater)/true */
    FOREIGN KEY (garden_map) REFERENCES garden(garden_id) ON DELETE CASCADE ON UPDATE CASCADE
);

INSERT INTO visit (garden_map,start_of_visit,end_of_visit,visit_done)
    VALUES 
        (1,'2020-01-01 08:00','2021-01-01 10:00',true)
        ,(1,'2021-01-01 08:00','2021-01-01 10:00',false)
        ,(2,'2021-02-01 08:00','2021-02-01 10:00',false)
        ,(1,'2021-03-01 08:00','2021-03-01 10:00',false)
        ,(2,'2021-04-01 08:00','2021-04-01 10:00',false)
;

CREATE TABLE IF NOT EXISTS annotation (
    annotation_id INTEGER PRIMARY KEY,
    visit_map INTEGER REFERENCES visit(visit_id) ON DELETE CASCADE ON UPDATE CASCADE,
    trait_map INTEGER REFERENCES trait(trait_id) ON DELETE CASCADE ON UPDATE CASCADE,
    garden_plant_map_garden_map INTEGER, garden_plant_map_plant_map INTEGER,
    FOREIGN KEY (garden_plant_map_garden_map,garden_plant_map_plant_map) REFERENCES garden_plant_map(garden_map,plant_map)
);

INSERT INTO annotation (visit_map, trait_map, garden_plant_map_garden_map, garden_plant_map_plant_map ) 
    VALUES
        (1 /* visit on 1/1/20 */, 1 /* Well watered */, 1 /* Kew */, 3 /* Poppy */ )
        , (1 /* visit on 1/1/20 */, 5 /* under watered */, 1 /* Kew */, 1 /* Rose */ )
        
        , (3 /* visit on 1/2/21 */, 2 /* dead */, 2 /* Babylon */, 2 /* Dandelion */ )
        , (3 /* visit on 1/2/21 */, 4 /* flourishing */, 2 /* babylon */, 6 /* Beech */ )
        , (3 /* visit on 1/2/21 */, 3 /* stressed */, 2 /* babylon */, 5 /* Beech */ )      
;

SELECT 
    garden_name,
    start_of_visit,end_of_visit, visit_done,
    plant.plant_name,
    trait.trait_description,
        CASE WHEN visit_done THEN 'Completed' ELSE 'ToDO' END AS status
FROM annotation
JOIN visit ON visit.visit_id = annotation.visit_map
JOIN garden ON visit.garden_map = garden.garden_id
JOIN plant ON garden_plant_map_plant_map = plant_id
JOIN trait ON trait_map = trait_id
;

查询结果为:-

并且假设 ID 为 1 的访问被删除(尽管您可能认为 visit_done 为真有效删除(因此您总是可以及时返回))例如使用 :-

DELETE FROM visit WHERE visit_id = 3;

然后是同样的查询 returns :-

即访问 3 的三个注释已被删除

忽略删除,即访问时 visit_id 剩余 3,然后 table 看起来像 :-

花园

工厂

特质

访问

garden_plant_map

注释

正在关注

实体。在 Kotlin 中,从上面(在 progress/untested 中工作):-

花园

/* CREATE TABLE IF NOT EXISTS garden (garden_id INTEGER PRIMARY KEY, garden_name TEXT UNIQUE);*/
@Entity( indices = [Index(value = ["garden_name"], unique = true)])
data class Garden (
    @PrimaryKey
    @ColumnInfo(name = "garden_id")
    val id: Long? = null, /* specifying null or not supplying value results in auto-generated id with overheads of autogenerate = true */
    @ColumnInfo(name = "garden_name")
    val name: String
)

植物

/* CREATE TABLE IF NOT EXISTS plant(plant_id INTEGER PRIMARY KEY, plant_name TEXT UNIQUE); */
@Entity(indices = [Index( value = ["plant_name"], unique = true)])
data class Plant (
    @PrimaryKey
    @ColumnInfo(name = "plant_id")
    val id: Long? = null,
    @ColumnInfo(name = "plant_name")
    val name: String
)

特质

/* CREATE TABLE IF NOT EXISTS trait (trait_id INTEGER PRIMARY KEY, trait_description UNIQUE); */
@Entity(indices = [Index(value = ["trait_description"], unique = true)])
data class Trait(
    @PrimaryKey
    @ColumnInfo(name = "trait_id")
    val id: Long? = null,
    @ColumnInfo(name = "trait_description")
    val description: String
)

GardenPlantMap

/* CREATE TABLE IF NOT EXISTS garden_plant_map (
    garden_map INTEGER,
    plant_map INTEGER,
    PRIMARY KEY (garden_map,plant_map)
    FOREIGN KEY (garden_map) REFERENCES garden(garden_id) ON DELETE CASCADE ON UPDATE CASCADE
    FOREIGN KEY (plant_map) REFERENCES plant(plant_id) ON DELETE CASCADE ON UPDATE CASCADE
   );
 */
@Entity(
    tableName = "garden_plant_map",
    primaryKeys = ["garden_map","plant_map"],
    foreignKeys = [
        ForeignKey(
            entity =  Garden::class,
            parentColumns = ["garden_id"],
            childColumns = ["garden_map"],
            onDelete = CASCADE,
            onUpdate = CASCADE
        ),
        ForeignKey(
            entity = Plant::class,
            parentColumns = ["plant_id"],
            childColumns = ["plant_map"],
            onDelete = CASCADE,
            onUpdate = CASCADE
        )
    ]
)
data class GardenPlantMap(
    val garden_map: Long,
    @ColumnInfo(index = true) /* indexed as will likely map via column */
    val plant_map: Long
)

访问

/*
    CREATE TABLE IF NOT EXISTS visit (
        visit_id INTEGER PRIMARY KEY,
        garden_map INTEGER,
        start_of_visit TEXT /* will be date in yyyy-mm-dd hh:mm:ss format*/,
        end_of_visit TEXT,
        visit_done INTEGER, /* 0/false or 1 (or greater)/true */
        FOREIGN KEY (garden_map) REFERENCES garden(garden_id) ON DELETE CASCADE ON UPDATE CASCADE
    );
 */
@Entity(
    foreignKeys = [
            ForeignKey(
                entity = Garden::class,
                parentColumns = ["garden_id"],
                childColumns = ["garden_map"],
                onDelete = CASCADE,
                onUpdate = CASCADE
            )
    ]
)
data class Visit(
    @PrimaryKey
    @ColumnInfo(name = "visit_id")
    val id: Long? = null,
    @ColumnInfo(index = true)
    val garden_map: Long,
    @ColumnInfo(name = "start_of_visit")
    val visitStart: String, /* could be Long */
    @ColumnInfo(name = "end_of_visit")
    val visitEnd: String,
    @ColumnInfo(name = "visit_done")
    val visitDone: Boolean
)

注释

/*
    CREATE TABLE IF NOT EXISTS annotation (
        annotation_id INTEGER PRIMARY KEY,
        visit_map INTEGER REFERENCES visit(visit_id) ON DELETE CASCADE ON UPDATE CASCADE,
        trait_map INTEGER REFERENCES trait(trait_id) ON DELETE CASCADE ON UPDATE CASCADE,
        garden_plant_map_garden_map INTEGER, garden_plant_map_plant_map INTEGER,
        FOREIGN KEY (garden_plant_map_garden_map,garden_plant_map_plant_map) REFERENCES garden_plant_map(garden_map,plant_map)
    );
 */
@Entity(
    indices = [
        Index(value = ["garden_plant_map_garden_map","garden_plant_map_plant_map"])

              ],
    foreignKeys = [
        ForeignKey(
            entity = Visit::class,
            parentColumns = ["visit_id"],
            childColumns = ["visit_map"],
            onDelete = CASCADE,
            onUpdate = CASCADE
        ),
        ForeignKey(
            entity = Trait::class,
            parentColumns = ["trait_id"],
            childColumns = ["trait_map"],
            onDelete = CASCADE,
            onUpdate = CASCADE
        ),
        ForeignKey(
            entity = GardenPlantMap::class,
            parentColumns = ["garden_map","plant_map"],
            childColumns = ["garden_plant_map_garden_map","garden_plant_map_plant_map"] /*,
            onDelete = CASCADE,
            onUpdate = CASCADE
            */
        )

    ]
)
data class Annotation(
    @PrimaryKey
    @ColumnInfo(name = "annotation_id")
    val id: Long? = null,
    @ColumnInfo(name = "visit_map", index = true)
    val visitMap: Long,
    @ColumnInfo(name = "trait_map", index = true)
    val traitMap: Long,
    @ColumnInfo( name ="garden_plant_map_garden_map")
    val gardenPlantMap_garden_map: Long,
    @ColumnInfo( name ="garden_plant_map_plant_map")
    val gardenPlantMap_plant_map: Long
)

用于获取最终查询的两个 POJO(备选方案),其中包含额外的 computed/derived 列 status Example1POJOExample2POJO :-

data class Example1POJO (
    @Embedded
    val garden: Garden,
    @Embedded
    val visit: Visit,
    @Embedded
    val plant: Plant,
    @Embedded
    val trait: Trait,
    val status: String
)

data class Example2POJO(
    val garden_id: Long,
    val garden_name: String,
    val visit_id: Long,
    val visit_done: Boolean,
    val start_of_visit: String,
    val end_of_visit: String,
    val plant_id: Long,
    val plant_name: String,
    val trait_id: Long,
    val trait_description: String,
    val status: String
)

单个 Dao 抽象 class(可以是接口)GardenVisitDao

@Dao
abstract class GardenVisitDao {

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract fun insert(garden: Garden): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract fun insert(plant: Plant): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract fun insert(trait: Trait): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract fun insert(gardenPlantMap: GardenPlantMap): Long /* not really of use */
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract fun insert(visit: Visit): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract fun insert(annotation: Annotation): Long

    @Query("SELECT * FROM garden")
    abstract fun getAllFromGarden(): List<Garden>
    @Query("SELECT garden_id FROM garden WHERE garden_name=:gardenName")
    abstract fun getGardenIdByGardenName(gardenName: String): Long
    @Query("SELECT plant_id FROM plant WHERE plant_name=:plantName")
    abstract fun getPlantIdByPlantName(plantName: String): Long
    @Query("SELECT trait_id FROM trait WHERE trait_description=:traitDescription")
    abstract fun getTraitIdByDescription(traitDescription: String): Long
    @Query("SELECT visit_id FROM visit WHERE start_of_visit=:visitStartDateTime")
    abstract fun getVisitIdByStartDateTime(visitStartDateTime: String): Long

    /*
        SELECT
        garden_name,
        start_of_visit,end_of_visit, visit_done,
        plant.plant_name,
        trait.trait_description,
            CASE WHEN visit_done THEN 'Completed' ELSE 'ToDO' END AS status
        FROM annotation
        JOIN visit ON visit.visit_id = annotation.visit_map
        JOIN garden ON visit.garden_map = garden.garden_id
        JOIN plant ON garden_plant_map_plant_map = plant_id
        JOIN trait ON trait_map = trait_id
        ;
     */

    @Query("SELECT " +
            "garden.*," +
            "visit.*," +
            "plant.*," +
            "trait.*," +
            "CASE WHEN visit_done THEN 'Completed' ELSE 'ToDO' END AS status" +
            " FROM annotation " +
            "JOIN visit ON visit.visit_id = annotation.visit_map " +
            "JOIN garden ON visit.garden_map = garden.garden_id " +
            "JOIN plant ON annotation.garden_plant_map_plant_map = plant.plant_id " +
            "JOIN trait ON trait.trait_id = annotation.trait_map")
    abstract fun getAllAnnotationsOverviewV1(): List<Example1POJO>


    @Query("SELECT " +
            "garden.*," +
            "visit.*," +
            "plant.*," +
            "trait.*," +
            "CASE WHEN visit_done THEN 'Completed' ELSE 'ToDO' END AS status" +
            " FROM annotation " +
            "JOIN visit ON visit.visit_id = annotation.visit_map " +
            "JOIN garden ON visit.garden_map = garden.garden_id " +
            "JOIN plant ON annotation.garden_plant_map_plant_map = plant.plant_id " +
            "JOIN trait ON trait.trait_id = annotation.trait_map")
    abstract fun getAllAnnotationsOverviewV2(): List<Example2POJO>
}

一个@Database class GardenVisitDatabase

@Database(entities = [
    Garden::class,
    Plant::class,
    Trait::class,
    Visit::class,
    Annotation::class,
    GardenPlantMap::class],
    version = 1
)
abstract class GardenVisitDatabase: RoomDatabase() {
    abstract fun getGardenVisitDao(): GardenVisitDao
    companion object {
        @Volatile
        private var instance: GardenVisitDatabase? = null
        fun getInstance(context: Context): GardenVisitDatabase {
            if (instance == null) {
                instance = Room.databaseBuilder(
                    context,GardenVisitDatabase::class.java,
                    "gardenvisit.db"
                )
                    .allowMainThreadQueries()
                    .build()
            }
            return instance as GardenVisitDatabase
        }
    }
}
  • 注意 allowMainThreadQueries 允许一切(大部分)在主线程上 运行(有利于测试)。

最终从 Activity 中加载和提取数据,该 Activity 复制(接近)答案中的 SQL(最后一个查询的两个版本)。

class MainActivity : AppCompatActivity() {
    lateinit var db: GardenVisitDatabase
    lateinit var dao: GardenVisitDao
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        db = GardenVisitDatabase.getInstance(this)
        dao = db.getGardenVisitDao()

        /*
        INSERT INTO garden (garden_name)
            VALUES('Kew' /* id will be 1 */),('Hanging Gardens of Babylon' /* id will be 2 and so on (probably)*/)
        ;
         */
        val keygardens_id = dao.insert(Garden(name = "Kew"))
        val babylon_id = dao.insert(Garden(name = "hanging Gardens of Babylon"))
        /*
        INSERT INTO plant (plant_name)
            VALUES('Rose' /* id will be 1 etc*/),('Dandelion'),('Poppy'),('Azelia'),('Oak'),('Beech')
        ;
         */
        val rose_id = dao.insert(Plant(name = "Rose"))
        val dand_id = dao.insert(Plant(name = "Dandelion"))
        val popp_id = dao.insert(Plant(name = "Poppy"))
        val azel_id = dao.insert(Plant(name = "Azelia"))
        val oak_id = dao.insert(Plant(name = "Oak"))

        val ww_id = dao.insert(Trait(description = "Well Watered"))
        val dead_id = dao.insert(Trait(description = "Dead"))
        val uw_id = dao.insert(Trait(description = "Under Watered"))
        val str_id = dao.insert(Trait(description = "Stressed"))
        val flour_id = dao.insert(Trait(description = "Flourishing"))

        dao.insert(GardenPlantMap( dao.getGardenIdByGardenName("Kew"),dao.getPlantIdByPlantName("Rose")));
        dao.insert(GardenPlantMap(keygardens_id,popp_id))
        dao.insert(GardenPlantMap(babylon_id,dand_id))
        dao.insert(GardenPlantMap(babylon_id,oak_id))
        dao.insert(GardenPlantMap(babylon_id,dao.insert(Plant(name = "Beech"))))

        val vkew20210101 = dao.insert(Visit(garden_map = keygardens_id, visitStart = "2020-01-01 08:00", visitEnd = "2021-01-01 10:00", visitDone = true))
        val vkew20220101 = dao.insert(Visit(garden_map = keygardens_id, visitStart = "2022-01-01 08:00", visitEnd = "2022-01-01 10:00", visitDone = false))
        val vbab20220201 = dao.insert(Visit(garden_map = babylon_id, visitStart = "2022-02-01 08:00", visitEnd = "2022-02-01 10:00", visitDone = false))
        val vkey20220301 = dao.insert(Visit(garden_map = keygardens_id, visitStart = "2022-03-01 08:00", visitEnd = "2022-03-01 10:00", visitDone = false))
        val vbab20220401 = dao.insert(Visit(garden_map = babylon_id, visitStart = "2022-04-01 08:00", visitEnd = "2022-04-01 10:00", visitDone = false))
        val vkey20220501 = dao.insert(Visit(garden_map = keygardens_id, visitStart = "2022-05-01 08:00", visitEnd = "2022-05-01 10:00", visitDone = false))

        dao.insert(Annotation(visitMap = vkew20210101, traitMap =  ww_id, gardenPlantMap_garden_map = keygardens_id, gardenPlantMap_plant_map = popp_id))
        dao.insert(Annotation(visitMap = vkew20210101, traitMap = uw_id, gardenPlantMap_garden_map = keygardens_id, gardenPlantMap_plant_map = rose_id))
        dao.insert(Annotation(visitMap = vbab20220201, traitMap = dead_id, gardenPlantMap_garden_map = babylon_id, gardenPlantMap_plant_map = dand_id))
        dao.insert(Annotation(visitMap = vbab20220201, traitMap = flour_id, gardenPlantMap_garden_map = babylon_id, gardenPlantMap_plant_map = dao.getPlantIdByPlantName("Beech")))
        dao.insert(Annotation(visitMap = vbab20220201, traitMap = str_id, gardenPlantMap_garden_map = babylon_id, gardenPlantMap_plant_map = oak_id))

        for(ex1: Example1POJO in dao.getAllAnnotationsOverviewV1()) {
            Log.d("GARDENDBINFO","Garden is ${ex1.garden.name} starts: ${ex1.visit.visitStart} ends: ${ex1.visit.visitEnd}. Plant is ${ex1.plant.name}. Trait is ${ex1.trait.description}. Status is ${ex1.status} ")
        }
        for(ex2: Example2POJO in dao.getAllAnnotationsOverviewV2()) {
            Log.d("GARDENDBINFO","Garden is ${ex2.garden_name} starts: ${ex2.start_of_visit} ends: ${ex2.end_of_visit}. Plant is ${ex2.plant_name}. Trait is ${ex2.trait_description}. Status is ${ex2.status} ")

        }
    }
}

结果输出到日志

40.959D/GARDENDBINFO: Garden is Kew starts: 2020-01-01 08:00 ends: 2021-01-01 10:00. Plant is Poppy. Trait is Well Watered. Status is Completed 
40.959D/GARDENDBINFO: Garden is Kew starts: 2020-01-01 08:00 ends: 2021-01-01 10:00. Plant is Rose. Trait is Under Watered. Status is Completed 
40.960D/GARDENDBINFO: Garden is hanging Gardens of Babylon starts: 2022-02-01 08:00 ends: 2022-02-01 10:00. Plant is Dandelion. Trait is Dead. Status is ToDO 
40.960D/GARDENDBINFO: Garden is hanging Gardens of Babylon starts: 2022-02-01 08:00 ends: 2022-02-01 10:00. Plant is Beech. Trait is Flourishing. Status is ToDO 
40.960D/GARDENDBINFO: Garden is hanging Gardens of Babylon starts: 2022-02-01 08:00 ends: 2022-02-01 10:00. Plant is Oak. Trait is Stressed. Status is ToDO

 
40.962D/GARDENDBINFO: Garden is Kew starts: 2020-01-01 08:00 ends: 2021-01-01 10:00. Plant is Poppy. Trait is Well Watered. Status is Completed 
40.962D/GARDENDBINFO: Garden is Kew starts: 2020-01-01 08:00 ends: 2021-01-01 10:00. Plant is Rose. Trait is Under Watered. Status is Completed 
40.962D/GARDENDBINFO: Garden is hanging Gardens of Babylon starts: 2022-02-01 08:00 ends: 2022-02-01 10:00. Plant is Dandelion. Trait is Dead. Status is ToDO 
40.962D/GARDENDBINFO: Garden is hanging Gardens of Babylon starts: 2022-02-01 08:00 ends: 2022-02-01 10:00. Plant is Beech. Trait is Flourishing. Status is ToDO 
40.962D/GARDENDBINFO: Garden is hanging Gardens of Babylon starts: 2022-02-01 08:00 ends: 2022-02-01 10:00. Plant is Oak. Trait is Stressed. Status is ToDO