how do you feel about having different objects for...
# announcements
b
how do you feel about having different objects for writing and reading database objects? Writing an object will for instance not require an id whereas querying an object will always have one. Otherwise I have to add null checks everywhere.
d
How about this?
Copy code
class DbEntry<ID : UUID?>(val id: ID, val name: String)

typealias SavedDbEntry = DbEntry<UUID>
typealias UnsavedDbEntry = DbEntry<Nothing?>
UnsavedDbEntry
can only hold
null
for
id
,
SavedDbEntry
can only hold non-null
UUID
.
And you can use
DbEntry<*>
to refer to either.
b
neat trick however I'm also using this to generate a swagger api
and some attributes are unfortunately de-normalized and can't be written
I suppose the Nothing will not translate cleanly to java
it will fail at runtime, right?
d
No. It's
Nothing?
, not
Nothing
.
And
DbEntry
(the only thing visible from a Java standpoint), has a field (and getter) of type
UUID
with
@Nullable
.
b
what happens if the user sets it in Java though
d
They can set it to null
As always in java
b
and also to a value?
d
Sure
Java sees
UUID
, which to it's mind can be null.
Java does not see those typealiases at all
t
Is there a reason you don’t want to just use different objects for input vs output?
My experience is that it solves way more problems than it causes. Not only does it help your
id
vs
no-id
issue here, it helps you be sure that adding an attribute on one (save, read etc.) path doesn’t have an unintended affect on another path
b
@tddmonkey that's what I was thinking as well. Just wanted to get some feedback since I need some boilerplate code for that which converts between these objects (e.g. fetch object from db, update value and save it)
ok, I think I found a solution for that as well: I will just create an interface for both objects and require that for creating an object.
Copy code
data class User(val id: String, override val name: String) : SavableUser

data class CreateUser(override val name: String) : SavableUser

interface SavableUser {
    val name: String
}
in java I probably would have gone for inheritance but interface feel like an even neater solution
d
I am not sure why that is better than my typealias solution
t
That feels like it solves a single issue of the type - what about when the difference is larger?
d
I am not sure I follow you
t
When the representations differ by more than just having an ID or not
d
You could add more type parameters. The problem with having two classes is that there is no easy way to convert between them without spelling out all attributes again.
t
ymmv. tbh I use this technique a lot and don’t usually find myself converting between them so much
Even if I did, I think I’d rather have that than the risk of unwarranted changes to a flow I’m not working on
d
don’t usually find myself converting between them so much
Not sure how you are doing this. You will need to write a separate
saveToDb
function for every type you have...
t
The majority of my work is in RESTful BE services, so there will be a
GET
endpoint for a client to retrieve data, then a
PUT/POST
endpoint to update data. There’s no read-update-write cycle inside my service
d
Well, that is a totally different scenario. The original question was about database objects.
t
I don’t see how - I’m still reading and writing to a database
how do you feel about having different objects for writing and reading database objects? Writing an object will for instance not require an id whereas querying an object will always have one. Otherwise I have to add null checks everywhere.
. Nothing in there suggests that isn’t/can’t be a stateless RESTful service
d
The way I understood the question:
Copy code
val unsaved = UnsavedEntity(... data ...)
// unsaved.id does not exist / is null
val saved: SavedEntity = db.save(unsaved)
println(saved.id) // guaranteed to be not null
Inside
db.save
you need to go from
UnsavedEntity
to
SavedEntity
.
If those are different classes, that process is tedious and needs to be done separately for every entity type.
t
If you need to return a saved representation straight away, perhaps. I typically don’t see that though. I’m coming from the point of view of a more CQRS approach where the representations can happily differ and there’s no need to treat them the same if they don’t have to be. If your saved object is truly and always going to be the same as a new object, but with an ID then yeah, code sharing is probably the right way to go. My experience is typically that they have a tendency to diverge, and when that happens I don’t want to try and shoehorn a representation in that doesn’t quite fit, just because I used the same class.
The OPs question was “how do you feel about having different objects for writing and reading database objects?“. I was just stating it’s something I do regularly and find it not only useful, but potentially solving other classes (heh) of problems
d
I agree with you, it seems we just understood the question differently.
t
Out of interest - why would you go the
typealias
route instead of having a (potentially reusable) container object to represent the saved state and using composition? Say something like: class SavedItem<T: Any>(val id: UUID, val item: T) Then you either use
User
or
Saved<User>
as necessary in the right place?
d
That would also work, but has the disadvantage of having to do another hop "all the time" (
user.item.name
instead of
user.name
).
t
Inheritance + delegation?
d
Can't delegate to a
data class
😄
t
Hmm, yeah, messy
d
Database access is always messy... It's one of my constant pain points in any language.
t
tbh I try to architect my systems to avoid as much of that mess as possible