I'm working on modeling entities that will be persisted in a DB. Using a User entity as an example, I'd like to work with them in this way:
val userBeforePersisting = new User("Joe", "joe#gmail.com")
// DB access code (where rs is a ResultSet)
val foundUser = new User(rs.getLong("id"), rs.getString("name"), rs.getString("email"))
I'd like to use the same User code (i.e. minimize code duplication), while having two types of users:
Pre-persisted users do not have an ID
Persisted users retrieved from the DB have an ID
I'd like to enforce this as strictly as possible at compile-time.
I'd like to be able to treat all Users the same, except if I try and get an ID from an un-persisted User, an error will be raised or it would not compile.
I'd like to avoid having to make separate classes like this
class NewUser(val name: String, val email: String)
class PersistedUser(val id: Long, val name: String, val email: String)
I don't like this solution because of the code duplication (name and email fields).
Here's kind of what I'm thinking:
class User(val id: Long, val name: String, val email: String) {
this(name: String, email: String) = this(0l, name, email)
this(id: Long, name: String, email: String) = this(id, name, email)
}
But then my un-persisted users have an id of 0l.
Here's another approach:
trait User {
val name: String
val email: String
}
class NewUser(val name: String, val email: String) extends User
class PersistedUser(val id: Long, val name: String, val email: String) extends User
This gives me the compile-time checks that I'd like. I'm not sure if there are any draw-backs to this.
Maybe I could try something like this:
class User(val name: String, val email: String)
trait Persisted { val id: Long }
class PersistedUser(val id: Long, val name: String, val email: String)
extends User(name, email)
with Persisted
Any thoughts to these approaches? I've never done it this way, so I'm not sure if I understand all the consequences.
Sounds like a possible use of Option.
class User(val id: Option[Long], val name: String, val email: String)
So persisted users have an id of Some(id) whereas non-persisted users have None.
As a convenience, you could grant id a default value of None:
class User(val id: Option[Long] = None, val name: String, val email: String)
// When you have an id...
val foundUser = new User(Some(rs.getLong("id")),
name = rs.getString("name"), email = rs.getString("email"))
// When you don't
val userBeforePersisting = new User(name = "Joe", email = "joe#gmail.com")
// However this will throw a runtime error:
val idThatDoesntExist: Long = userBeforePersisting.id.get
This should also work with your multi-constructor example:
class User(val id: Option[Long], val name: String, val email: String) {
def this(name: String, email: String) = this(None, name, email)
def this(id: Long, name: String, email: String) = this(Some(id), name, email)
}
I thought Option might make sense because you'd like to express in the same class that a certain field can either have a value or not. The only other way seems to be to have two classes (possibly one inheriting from the other) with only one having an id field.
Related
I am creating an app where the user can log in or register for an account. I have created the register screen and it's successfully saving data into the database. However, I am now trying to integrate some validation. For example, the usernames would have to be unique and the email can't already exist.
I tried to write a custom query of-course to print out all the rows in the username column like this:
SELECT userName from cx_table
and I also tried to write a separate custom query of-course to print out all the rows in the email column like this:
SELECT email from cx_table
Then my approach was to take the user input and compare it to the values returned by that column, if it exists, print an error message. But when I run the app, I get the following error message
The columns returned by the query does not have the fields [id,firstName,lastName,password,address,city,postalcode,email,phone] in com.cxpro.data.Customer even though they are annotated as non-null or primitive. Columns returned by the query: [userName]
here is all my code for the Room Database:
Customer.kt
#Entity(tableName = "cx_table")
data class Customer(
#PrimaryKey(autoGenerate = true)
val id: Int,
val firstName: String,
val lastName: String,
val userName: String,
val password: String,
val address: String,
val city: String,
val postalcode: String,
val email: String,
val phone: String
)
CustomerDao.kt
#Dao
interface CustomerDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun addCustomers(customer: Customer)
#Query("SELECT * FROM cx_table ORDER BY id ASC")
fun readAllData(): LiveData<List<Customer>>
#Query("SELECT userName FROM cx_table")
fun readUserName(): LiveData<List<Customer>>
}
CustomerDatabase.kt
#Database(entities = [Customer::class],version = 1, exportSchema = false)
abstract class CustomerDatabase: RoomDatabase() {
abstract fun customerDao(): CustomerDao
companion object{
#Volatile
private var INSTANCE: CustomerDatabase? = null
fun getDatabase(context: Context): CustomerDatabase{
val tempInstance = INSTANCE
if(tempInstance != null){
return tempInstance
}
synchronized(this){
val instance = Room.databaseBuilder(
context.applicationContext,
CustomerDatabase::class.java,
"customer_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
CustomerRepository.kt
class CustomerRepository(private val customerDao: CustomerDao) {
val readAllData: LiveData<List<Customer>> = customerDao.readAllData()
val readUserName: LiveData<List<Customer>> = customerDao.readUserName()
suspend fun addCustomer(customer: Customer){
customerDao.addCustomers(customer)
}
}
CustomerViewModel.kt
class CustomerViewModel(application: Application): AndroidViewModel(application) {
val readAllData: LiveData<List<Customer>>
val readUserName: LiveData<List<Customer>>
private val repository: CustomerRepository
init {
val customerDao = CustomerDatabase.getDatabase(application).customerDao()
repository = CustomerRepository(customerDao)
readAllData = repository.readAllData
readUserName = repository.readUserName
}
fun addCustomer(customer: Customer){
viewModelScope.launch(Dispatchers.IO){
repository.addCustomer(customer)
}
}
}
How can I validate that the username and/or email doesn't already exist in the table?
Then my approach was to take the user input and compare it to the values returned by that column, if it exists, print an error message. But when I run the app, I get the following error message
This is because there are insufficient values to build a Customer object. As you just returning a single value per row you can use List<String> instead of List<Customer>. If multiple values then you need an object probably a POJO. and that the object field/variable names match the columns names
However, rather then having to loop through 2 lists you may wish to consider changing the Customer Entity to :-
#Entity(tableName = "cx_table",
indices = [
Index(value = ["userName"],unique = true),
Index(value = ["email"],unique = true)]
)
data class Customer(
#PrimaryKey(autoGenerate = true)
val id: Int,
val firstName: String,
val lastName: String,
val userName: String,
val password: String,
val address: String,
val city: String,
val postalcode: String,
val email: String,
val phone: String
)
and also changing the insert Dao to be :-
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun addCustomers(customer: Customer): Long //<<<<< ADDED Long
You can the check the return value if it is greater than 0 then the row was inserted, otherwise the row wasn't inserted and thus was invalid.
That is, as the indexes on username and email are UNIQUE then attempting to insert whilst duplicating either would result in a conflict, which is ignored. However, the row is not inserted and hence -1 being returned.
Yet another option could be to test the values e.g.
#Query("SELECT count(*) FROM customer WHERE userName=:userNameToCheck OR email=emailToCheck")
fun validateNewCustomer(userNameToCheck: String,emailToCheck): Int
If the result is 0 then OK to insert. You could split into two checks if you wanted to check them individually.
You could ascertain whether it is userName or email that is resulting in the invalid (non-zero) result using something like:-
#Query("SELECT ((SELECT count(*) FROM customer WHERE username=:userNameToCheck) + (SELECT count(*) * 1000 FROM customer WHERE email=:emailToCheck));")
fun validateNewCustomer(userNameToCheck: String,emailToCheck): Int
If the returned value is 0 then valid, if less than 1000 (if greater than 1 then duplicates exist) then the userName is invalid, if greater 1000 then the email is invalid, if greater than 1000 but not an exact multiple of 1000 then both are invalid.
1000 caters for up to 998 userName duplicates (if there is a UNIQUE index on userName then there should only be 1, similar for email)
I have seen Kotlin-examples using a Primary-constructor like this:
class Person(firstName: String, lastName: String) {}
And I have seen example with the var or val keyword, like this:
class Person(val firstName: String, val lastName: String) {}
What the difference? When do I have to use which variation?
Regarding the documentation, with var or val in the constructor you create a property in the class. If you do not write it, then it is only a parameter that is passed to the constructor. As an example:
class Person(val firstName: String, lastName: String) {
// firstName and lastName accessible
fun getFirstName() = firstName // firstName accessible
fun getLastName() = lastName // lastName not accessible
}
So if you want to continue to use the firstName and lastName, I would make a property out of it.
I have a set of policies that I want to match a request. If the policy exists, I want to match the request and see if the value matches.
The policies are List<Policies> -> (key: String, value: String) and the request can contain different keys.
Example:
The policies are a set of rules that the request should match.
class Policy {
val key: String,
val value: String
}
The request is a data class that contains different values (all optional), for example surname, firstName, address, ++++
data class Request (
id: Long = 12,
firstName: String = "test",
surname: String = "test",
address: String = "somewhere"
...// more fields
)
The list of policies can look like this (List):
List<Policy> => [
{
key: "surname",
value: "test"
},
{
key: "firstName",
value: "test"
}
]
I don't know how I can match the policies with the request. The Policy.key is a String and the Request can contain all different variations of properties.
How do I match the List of policies with my data class Request?
For your puroposes you need use reflection (you want to find field by name and get value), or change something in your model.
Solution with reflection can be like following:
data class Policy(
val key: String,
val value: String?
)
data class Request(
val id: Int,
val firstName: String? = null,
val surname: String? = null,
val address: String? = null
)
class PolicyException : Exception()
fun checkPolicies(request: Request, policies: List<Policy>) {
policies.forEach { policy ->
val member = request::class.members.find { member -> member.name == policy.key }
val requestMemberValue = member?.call(request)
if (requestMemberValue != policy.value) throw PolicyException()
}
}
fun main(args: Array<String>) {
println("Hello, reflection!")
checkPolicies(Request(id = 0, firstName = "Johnn"), listOf(Policy("firstName", "John")))
}
Also, I changed your policy model to handle nullable values (and still handling properly "null" as string).
But, with this solution you have to be very careful with changing model names. And remeber to do not obfuscate your models.
Also, quite better soltuion is adding Annotation which keeps policy name as annotation property (then problem with changing field name in app will disappear).
I have some JSON that looks like this:
{
"name" : "Credit Card",
"code" : "AUD",
"value" : 1000
}
and am using Moshi to unmarshall this into a data structure like:
data class Account(
#Json(name = "name")
val name: String,
#Json(name = "currency")
val currency: String,
#Json(name = "value")
val value: Int
)
Everything works well. However, I really would like to extract the currency and value parameters into a separate Money object. So my model looks more like:
data class Money(
#Json(name = "currency")
val currency: String,
#Json(name = "value")
val value: Int
)
data class Account(
#Json(name = "name")
val name: String,
#Json(name = "???")
val money: Money
)
The challenge I'm struggling with is how to annotate things so that the Money object can be given two different fields (currency and value) that come from the same level as the parent account.
Do I need to create an intermediate "unmarshalling" object called, say, MoshiAccount and then use a custom adapter to convert that to my real Account object?
I saw How to deseralize an int array into a custom class with Moshi? which looks close (except that in that case, the adapted object (VideoSize) only needs a single field as input... in my case, I need both currency and value)
Any thoughts or suggestions would be much appreciated. Thanks
Moshi's adapters can morph your JSON structure for you.
object ADAPTER {
private class FlatAccount(
val name: String,
val currency: String,
val value: Int
)
#FromJson private fun fromJson(json: FlatAccount): Account {
return Account(json.name, Money(json.currency, json.value))
}
#ToJson private fun toJson(account: Account): FlatAccount {
return FlatAccount(account.name, account.money.currency, account.money.value)
}
}
Don't forget to add the adapter to your Moshi instance.
val moshi = Moshi.Builder().add(Account.ADAPTER).add(KotlinJsonAdapterFactory()).build()
val adapter = moshi.adapter(Account::class.java)
data class UserRto(val lastName: String, val firstName: String, val email: String, val password: String) {
constructor() : this("", "", "", "")
}
Is this the easiest way to get a second ctor without arguments (for json desrialization)?
There is a convention (just for that case :) that a parameterless constructor is generated if all parameters have default values:
//Kotlin
data class UserRto(val lastName: String = "",
val firstName: String = "",
val email: String = "",
val password: String = "")
//Java
new UserRto();
new UserRto("a", "a", "a", "a");
Your solution with an explicit empty constructor has an advantage that all parameters must be provided, or non. But it is not a big deal in most cases and it is rarely used.
You do not specify which JSON deserialization library you are using. It is likely that you do not need this secondary constructor at all and can work directly with the class constructor that contains all of the properties as parameters.
For Jackson, use the Jackson-Kotlin module which automatically handles all of Kotlins unqiue cases including ability to call a constructor or factory with all of the parameters being properties (and in 2.8.x this includes default values for parameters being used for missing properties in the JSON)
For Gson, I think Kotson might do the same.
Google might find others for these or other libraries as well.
Then you would only need:
data class UserRto(val lastName: String, val firstName: String, val email: String, val password: String)
And with the Jackson-Kotlin module you would simply:
val user: UserRto = jacksonObjectMapper().readValue(jsonString)