Skip to content

Commit

Permalink
Initial support for entities with pre-provided IDs: #8
Browse files Browse the repository at this point in the history
  • Loading branch information
mvysny committed Jan 18, 2019
1 parent a274e0b commit f87affe
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 9 deletions.
56 changes: 47 additions & 9 deletions src/main/kotlin/com/github/vokorm/Mapping.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ annotation class Ignore
* Note that Sql2o works with all pojos and does not require any annotation/interface. Thus, if your table has no primary
* key or there is other reason you don't want to use this interface, you can still use your class with [db], you'll only
* lose those utility methods.
*
* ### Auto-generated IDs vs pre-provided IDs
* There are generally three cases for ID generation:
* * IDs generated by the database when the `INSERT` statement is executed
* * Natural IDs, such as a NaturalPerson with ID pre-provided by the government (social security number etc).
* * IDs created by the application, for example via [UUID.randomUUID]
*
* You can only use the [save] to create a row in the database when the ID is generated by the database. To create an
* entity with a pre-provided ID, you need to use the [create] method:
*
* ```
* NaturalPerson(id = "12345678", name = "Albedo").create()
* ```
* @param ID the type of the primary key. All finder methods will only accept this type of ids.
*/
interface Entity<ID: Any> : Serializable {
Expand All @@ -77,21 +90,18 @@ interface Entity<ID: Any> : Serializable {
*
* The bean is validated first, by calling [Entity.validate]. You can bypass this by setting [validate] to false, but that's not
* recommended.
*
* **WARNING**: if your entity has pre-provided (natural) IDs or IDs generated by the database (UUID), you must not call
* this method with the intent to insert the entity into the database - this method will always run UPDATE and then
* fail (since nothing has been updated since the row is not in the database yet).
* To force create the database row, call [create].
* @throws IllegalStateException if the database didn't provide a new ID (upon new row creation), or if there was no row (if [id] was not null).
*/
fun save(validate: Boolean = true) {
if (validate) { validate() }
db {
if (id == null) {
// not yet in the database, run the INSERT statement
val fields = meta.persistedFieldDbNames - meta.idProperty.dbColumnName
con.createQuery("insert into ${meta.databaseTableName} (${fields.joinToString()}) values (${fields.map { ":$it" }.joinToString()})", true)
.bindAliased(this@Entity)
.setColumnMappings(meta.getSql2oColumnMappings())
.executeUpdate()
val key = requireNotNull(con.key) { "The database have returned null key for the created record. Have you used AUTO INCREMENT or SERIAL for primary key?" }
@Suppress("UNCHECKED_CAST")
id = convertID(key)
create(validate = false) // no need to validate again
} else {
val fields = meta.persistedFieldDbNames - meta.idProperty.dbColumnName
con.createQuery("update ${meta.databaseTableName} set ${fields.map { "$it = :$it" }.joinToString()} where ${meta.idProperty.dbColumnName} = :${meta.idProperty.dbColumnName}")
Expand All @@ -103,6 +113,34 @@ interface Entity<ID: Any> : Serializable {
}
}

/**
* Always issues the database `INSERT`, even if the [id] is not null. This is useful for two cases:
* * When the entity has a natural ID, such as a NaturalPerson with ID pre-provided by the government (social security number etc),
* * ID auto-generated by the application, e.g. UUID
*
* It is possible to use this function with entities with IDs auto-generated by the database, but it may be simpler to
* simply use [save].
*/
fun create(validate: Boolean = true) {
if (validate) { validate() }
db {
var fields = meta.persistedFieldDbNames
if (id == null) {
// the ID is auto-generated by the database, do not include it in the INSERT statement.
fields -= meta.idProperty.dbColumnName
}
con.createQuery("insert into ${meta.databaseTableName} (${fields.joinToString()}) values (${fields.joinToString { ":$it" }})", true)
.bindAliased(this@Entity)
.setColumnMappings(meta.getSql2oColumnMappings())
.executeUpdate()
if (id == null) {
val key = requireNotNull(con.key) { "The database have returned null key for the created record. Have you used AUTO INCREMENT or SERIAL for primary key?" }
@Suppress("UNCHECKED_CAST")
id = convertID(key)
}
}
}

/**
* Deletes this entity from the database. Fails if [id] is null, since it is expected that the entity is already in the database.
*/
Expand Down
14 changes: 14 additions & 0 deletions src/test/kotlin/com/github/vokorm/Databases.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import java.time.Instant
import java.time.LocalDate
import java.util.*

/**
* A test table that tests the most basic cases. The ID is auto-generated by the database.
*/
@Table("Test")
data class Person(
override var id: Long? = null,
Expand Down Expand Up @@ -63,6 +66,13 @@ data class EntityWithAliasedId(
companion object : Dao<EntityWithAliasedId>
}

/**
* A table demoing natural person with government-issued ID (birth number, social security number, etc).
*/
data class NaturalPerson(override var id: String? = null, var name: String) : Entity<String> {
companion object : Dao<NaturalPerson>
}

private fun DynaNodeGroup.usingDockerizedPosgresql() {
check(Docker.isPresent) { "Docker not available" }
beforeGroup { Docker.startPostgresql(port = databasePort) }
Expand All @@ -87,6 +97,7 @@ private fun DynaNodeGroup.usingDockerizedPosgresql() {
maritalStatus varchar(200)
)""")
ddl("""create table if not exists EntityWithAliasedId(myid bigserial primary key, name varchar(400) not null)""")
ddl("""create table if not exists NaturalPerson(id varchar(10) primary key, name varchar(400) not null)""")
}
}

Expand Down Expand Up @@ -125,6 +136,7 @@ fun DynaNodeGroup.usingDockerizedMysql() {
maritalStatus varchar(200)
)""")
ddl("""create table if not exists EntityWithAliasedId(myid bigint primary key auto_increment, name varchar(400) not null)""")
ddl("""create table if not exists NaturalPerson(id varchar(10) primary key, name varchar(400) not null)""")
}
}

Expand Down Expand Up @@ -165,6 +177,7 @@ fun DynaNodeGroup.usingH2Database() {
maritalStatus varchar
)""")
ddl("""create table EntityWithAliasedId(myid bigint primary key auto_increment, name varchar not null)""")
ddl("""create table if not exists NaturalPerson(id varchar(10) primary key, name varchar(400) not null)""")
}
}
afterEach {
Expand Down Expand Up @@ -202,6 +215,7 @@ private fun DynaNodeGroup.usingDockerizedMariaDB() {
)"""
)
ddl("""create table if not exists EntityWithAliasedId(myid bigint primary key auto_increment, name varchar(400) not null)""")
ddl("""create table if not exists NaturalPerson(id varchar(10) primary key, name varchar(400) not null)""")
}
}

Expand Down
24 changes: 24 additions & 0 deletions src/test/kotlin/com/github/vokorm/MappingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,30 @@ class MappingTest : DynaTest({
expect(setOf("myid", "name")) { meta.persistedFieldDbNames }
}
}
group("NaturalPerson") {
test("save fails") {
val p = NaturalPerson(id = "12345678", name = "Albedo")
expectThrows(IllegalStateException::class, message = "We expected to update only one row but we updated 0 - perhaps there is no row with id 12345678?") {
p.save()
}
}
test("Save") {
val p = NaturalPerson(id = "12345678", name = "Albedo")
p.create()
expect(listOf("Albedo")) { NaturalPerson.findAll().map { it.name } }
p.name = "Rubedo"
p.save()
expect(listOf("Rubedo")) { NaturalPerson.findAll().map { it.name } }
NaturalPerson(id = "aaa", name = "Nigredo").create()
expect(listOf("Rubedo", "Nigredo")) { NaturalPerson.findAll().map { it.name } }
}
test("delete") {
val p = NaturalPerson(id = "foo", name = "Albedo")
p.create()
p.delete()
expect(listOf()) { NaturalPerson.findAll() }
}
}
}
})

0 comments on commit f87affe

Please sign in to comment.