需要 NoSQL Android 缓存

Need for NoSQL Android Cache

我 运行 几个基于 MongoDb 的 API 使用 Spring 数据

实现

我的问题是有一个 Android 可以无缝处理文档的缓存数据库,尤其是带有嵌入式文档的文档!

MongoDb 似乎有解决方案(MongoDb 领域)但它已连接到付费托管数据库

作为从 API 调用后端接收到 JSON 假数据的示例可能如下所示

{
  "detail": "Page 1 of 1",
  "count": 1,
  "page": 1,
  "results": [
    {
      "prospectId": "5cb2e9424274072ec4bb419c",
      "name": "Bill",
      "lastName": "Gates",
      "phone": "0108101081",
      "email": "gates.william@gmail.com",
      "company": {
        "companyId": "60847dc8ba7e6a4ae0fa5f93",
        "name": "Microsoft",
        "email": "info@Microsoft.com",
        "address": {
          "addressId": "5cb2e9424274072ec4bb4199",
          "label": "Home",
          "number": "1",
          "street": "Microsoft Way",
          "timestamp": 1631087921460
        },
        "timestamp": 1631087921855
      },
      "address": {
        "addressId": "5cb2e9424274072ec4bb4199",
        "label": "Home",
        "number": "1",
        "street": "Microsoft Way",
        "timestamp": 1631087921460
      },
      "timestamp": 1631087922537
    }
  ]
}

前景Class

class Prospect {
    
    @Id
    var prospectId: String? = null
    var name: String? = null
    var lastName: String? = null
    var phone: String? = null
    var email: String? = null
    
    @DBRef
    var company: Company? = null
    
    @DBRef
    var address: Address? = null
    var timestamp: Long = nowToLongUTC()
}

我的公司

class Company {
    
    @Id
    var companyId: String? = null
    var name: String? = null
    var email: String? = null
    
    @DBRef
    var address: Address? = null
    var timestamp: Long? = null
}

我的地址

class Address {
    @Id
    var addressId: String? = null
    var label: String? = null
    var number: String? = null
    var street: String? = null
    var timestamp: Long = nowToLongUTC()
}

它由包含 @dbRef Company@dbRef Address 的 类 生成 公司还包括 @dbRef Address

潜在客户和公司都包含一个地址,结合它具有文档结构中的文档

在noSql类型的文档中是相当自然的参考文档

我需要一个理解 noSql (MongoDb) 的 Android 缓存,并且可以本地处理嵌入在文档中嵌入的文档,

房间银河系失败!作为一个 SQL 并且做得不好。使用 room 的 @Embedded 表示法有助于嵌入文档的一个级别,但下一个级别需要使用 TypeConverter。这就是 SQL 让我失望的原因。

Room fails galactically! being a SQL and suffers doing it properly. Using room's @Embedded notation helps for one level of embedded document but the next level need to be using a TypeConverter. That is how SQL fails me.

也许以下是您根据“假”数据寻找的内容。这忽略了@DBRef,而是有实体(tables)地址、公司和前景,并有支持性的 POJO 的 CompanyWithAddress 和(为了更好的名字)ProspectWithCompanyWithAddressAndWithProspectAddress,这些使用@Relation 嵌入底层各自的实体子对象。

所以实体是:-

地址

@Entity /* This indicates that this is a table */
class Address {
    //@Id
    @PrimaryKey /* Room requires a Primary Key for a table */
    var addressId: String = ""
    var label: String? = null
    var number: String? = null
    var street: String? = null
    var timestamp: Long = /* nowToLongUTC() */ System.currentTimeMillis() /* used for convenience */
}

公司

@Entity(
    foreignKeys = [
        ForeignKey(
            entity = Address::class,
            parentColumns = ["addressId"],
            childColumns = ["addressRef"],
            /* You may or may not want onDelete and onUpdate actions */
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
            /* you may wish to refer to https://sqlite.org/foreignkeys.html */
        )
    ],
    indices = [
        Index("addressRef") /* Room will warn if a FK column is not indexed */
    ]
)
class Company {

    //@Id
    @PrimaryKey
    var companyId: String = ""
    var name: String? = null
    var email: String? = null

    //@DBRef
    //var address: Address? = null
    // Rather than de-normalise DB, waste space,
    // here we reference the address rather than include the address
    // the Foreign Key above adds a rule saying that the value of addressRef
    // MUST be a value that is in an addressId column in the address table
    // Foreign Keys are optional but recommended to enforce referential integrity
    // break the rule and an error occurs
    var addressRef: String? = null
    var timestamp: Long? = null
}

前景

@Entity(
    foreignKeys = [
        ForeignKey(
            entity = Company::class,
            parentColumns = ["companyId"],
            childColumns = ["companyRef"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = Address::class,
            parentColumns = ["addressId"],
            childColumns = ["prospectAddressRef"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        )
    ],
    indices = [
        Index("companyRef"),
        Index("prospectAddressRef")
    ]
)
class Prospect {

    //@Id
    @PrimaryKey
    var prospectId: String = ""
    var name: String? = null
    var lastName: String? = null
    var phone: String? = null
    var email: String? = null

    //@DBRef
    var companyRef: String? = null

    //@DBRef
    @ColumnInfo(name = "prospectAddressRef") /* defines the column name in the table to avoid ambiguities */
    var addressRef: String? = null
    var timestamp: Long = /* nowToLongUTC() */ System.currentTimeMillis()
}

POJO 的 :-

公司地址

class CompanyWithAddress {
    @Embedded
    var company: Company? = null
    @Relation(
        entity = Address::class,
        parentColumn = "addressRef",
        entityColumn = "addressId"
    )
    var address: Address? = null
}
  • 所以一家有地址的公司注意到 Address 对象是通过包含 相关 地址的 addressId 的 addressRef 获得的(不需要 TypeConverter)

ProspectWithCompanyWithAddressAndWithProspectAddress

class ProspectWithCompanyWithAddressAndWithProspectAddress {
    @Embedded
    var prospect: Prospect? = null
    @Relation(
        entity = Company::class,
        parentColumn = "companyRef",
        entityColumn = "companyId"
    )
    var companyWithAddress: CompanyWithAddress? = null
    @Relation(
        entity = Address::class,
        parentColumn = "prospectAddressRef",
        entityColumn = "addressId"
    )
    var address: Address? = null
}

Dao 在名为 AllDao

的 class 中用于上述演示
@Dao
abstract class AllDao {

    @Insert
    abstract fun insert(address: Address): Long
    @Insert
    abstract fun insert(company: Company): Long
    @Insert
    abstract fun insert(prospect: Prospect): Long
    @Transaction
    @Query("SELECT * FROM prospect")
    abstract fun getALlFullProspects(): List<ProspectWithCompanyWithAddressAndWithProspectAddress>
}
  • 这些允许插入相应的 tables
  • 请注意,由于外键,必须在使用该地址的公司和潜在客户之前插入地址,并且必须在插入潜在客户之前插入公司。
    • 使用外键(可选)没关系。

一个非常基本的@Database TheDatabase

@Database(
    entities = [Address::class,Company::class,Prospect::class],
    version = 1
)
abstract class TheDatabase: RoomDatabase() {
    abstract fun getAllDao(): AllDao

    companion object {
        @Volatile
        private var instance: TheDatabase? = null
        fun getInstance(context: Context): TheDatabase {
            if (instance == null) {
                instance = Room.databaseBuilder(
                    context,
                    TheDatabase::class.java,
                    "thedatabase.db"
                )
                    .allowMainThreadQueries() /* for convenience/brevity of demo */
                    .build()
            }
            return instance as TheDatabase
        }
    }
}

将它们放在一个 Activity 中(运行 在 brevity/convenience 的主线程上) MainActivity : -

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

        db = TheDatabase.getInstance(this)
        dao = db.getAllDao()

        val a1Id = "5cb2e9424274072ec4bb4199"
        val c1Id = "60847dc8ba7e6a4ae0fa5f93"
        val p1Id = "5cb2e9424274072ec4bb419c"

        var a1 = Address()
        a1.addressId = a1Id
        a1.label = "Home"
        a1.number = "1"
        a1.street = "MS Way"
        dao.insert(a1)

        var c1 = Company()
        c1.companyId = c1Id
        c1.email = "info@Microsoft.com"
        c1.name = "Microsoft"
        c1.addressRef = a1Id
        dao.insert(c1)

        var p1 = Prospect()
        p1.prospectId = p1Id
        p1.name = "Bill"
        p1.lastName = "Gates"
        p1.email = "gates.william@gmail.com"
        p1.phone = "0108101081"
        p1.companyRef = c1Id
        p1.addressRef = a1Id

        dao.insert(p1)

        /* Going to add a 2nd Prospect with a 2nd Company with separate addresses */ 
        val a2Id = "6cb2e9424274072ec4bb4199"
        val a3Id = "7cb2e9424274072ec4bb4199"
        val c2Id = "70847dc8ba7e6a4ae0fa5f93"
        val p2Id = "6cb2e9424274072ec4bb419c"

        /* change the initial address ready for the insert of the 2nd*/
        a1.addressId = a2Id
        a1.street = "Apple lane"
        a1.number = "33"
        a1.label = "Work"
        dao.insert(a1)

        /* change the 2nd address ready for the insert of the 3rd */
        a1.addressId = a3Id
        a1.label = "Holiday"
        a1.number = "111"
        a1.street = "Lazy Street"
        dao.insert(a1)

        /* Change the company and insert a new one */
        c1.companyId = c2Id
        c1.name = "Apple Inc"
        c1.email = "info@apple.inc"
        c1.addressRef = a2Id
        dao.insert(c1)

        /* Change the prospect and insert a new one */
        p1.addressRef = a3Id
        p1.companyRef = c2Id
        p1.prospectId = p2Id
        dao.insert(p1)


        /* Now the data has been added extract it and output the extracted to the log*/
        var TAG = "PROSPECTINFO"
        for(p: ProspectWithCompanyWithAddressAndWithProspectAddress in dao.getALlFullProspects()) {
            Log.d(TAG,
                "Prospect: Name = ${p.prospect!!.name}, ${p.prospect!!.lastName} Phone is ${p.prospect!!.phone}" +
                        "\n\tCompany is ${p.companyWithAddress!!.company!!.name} " +
                        "\n\t\tCompany Address is ${p.companyWithAddress!!.address!!.number} ${p.companyWithAddress!!.address!!.street}" +
                        "\n\tProspect Address is ${p.address!!.number} ${p!!.address!!.street}"
            )
        }
    }
}

结果

上面是运行时的日志(设计成只运行一次,运行第二次会出现UNIQIUE冲突):-

D/PROSPECTINFO: Prospect: Name = Bill, Gates Phone is 0108101081
        Company is Microsoft 
            Company Address is 1 MS Way
        Prospect Address is 1 MS Way
D/PROSPECTINFO: Prospect: Name = Bill, Gates Phone is 0108101081
        Company is Apple Inc 
            Company Address is 33 Apple lane
        Prospect Address is 111 Lazy Street

通过 App Inspector,数据库是:-

地址 table :-

公司table:-

  • 时间戳为 NULL,因为它们未设置(根据您的原始代码)

前景 table :-

  • Name/Last 名称等是相同的值,因为这些没有改变。重要的是,虽然 companyRef 和 prospectAddressRef 值引用各自的 companies/addresses.

你可以看看亚硝酸盐数据库here。 Nitrite 是一个用 Java 编写的开源 nosql 嵌入式文档存储,非常适合 Android 或 Java 桌面应用程序。幸运的是,它支持 mongodb,例如 api。

在您的特定用例中,这里有两个选项。

  1. 您删除 @DBRef 注释(nitrite 不支持数据库引用)并将 Prospect 对象及其所有嵌入对象插入到 nitrite 对象存储库中。
class Prospect {
    @Id
    var prospectId: String? = null
    var name: String? = null
    var lastName: String? = null
    var phone: String? = null
    var email: String? = null
    var company: Company? = null
    var address: Address? = null
    var timestamp: Long = System.currentTimeMillis()
}

class Company {
    var companyId: String? = null
    var name: String? = null
    var email: String? = null
    var address: Address? = null
    var timestamp: Long? = null
}

class Address {
    var addressId: String? = null
    var label: String? = null
    var number: String? = null
    var street: String? = null
    var timestamp: Long = System.currentTimeMillis()
}

val db = nitrite {
    file = File(fileName)
    autoCommitBufferSize = 2048
    compress = true
    autoCompact = false
}

// create repository
val prospects = db.getRepository<Prospect>()

// create/get your object
val obj = Prospect()
...

// insert the object into the database
prospects.insert(obj)

// retrieve like
val cursor = prospects.find(Prospect::prospectId eq "xyz")

// or
val cursor = prospects.find("prospectId.company.address.number" eq 123)
  1. 您分别为 ProspectsCompanyAddress 创建了 3 个对象存储库,并将 ID 存储在引用位置。但是你必须调用适当的对象存储库来获取实际的对象。
class Prospect {
    @Id
    var prospectId: String? = null
    var name: String? = null
    var lastName: String? = null
    var phone: String? = null
    var email: String? = null
    var companyId: String? = null
    var addressId: String? = null
    var timestamp: Long = System.currentTimeMillis()
}

class Company {
    @Id
    var companyId: String? = null
    var name: String? = null
    var email: String? = null
    var addressId: String? = null
    var timestamp: Long? = null
}

class Address {
    @Id
    var addressId: String? = null
    var label: String? = null
    var number: String? = null
    var street: String? = null
    var timestamp: Long = System.currentTimeMillis()
}

val db = nitrite {
    file = File(fileName)
    autoCommitBufferSize = 2048
    compress = true
    autoCompact = false
}

// create repositories
val prospects = db.getRepository<Prospect>()
val companies = db.getRepository<Company>()
val addresses = db.getRepository<Address>()

// create/get your objects
val pObj = Prospect()
val cObj = Company()
val aObj = Address()
...

// insert the object into the database
prospects.insert(pObj)
companies.insert(cObj)
addresses.insert(aObj)

// retrieve like
val p = prospects.find(Prospect::prospectId eq "xyz").firstOrNull()
val a = addresses.find(Address::addressId eq p.addressId)

这里是完整的documentation帮助你,如果你被卡住了。我建议您使用 3.4.2 版,因为它已准备就绪且稳定。

免责声明:我是 Nitrite 数据库的开发者。