Polymorphism in Prisma Schema - Best practices? - graphql

This is more a design question than a coding question. Suppose the following schema:
// schema.prisma
// Solution 1
model Entity {
id Int #id #default(autoincrement())
attrs EntityAttr[]
}
model EntityAttr {
id Int #id #default(autoincrement())
value Json // or String, doesnt matter much here
// the point is I need to attach info on the
// join table of this relation
attr Attr #relation(fields: [attrId], references: [id])
entity Entity #relation(fields: [entityId], references: [id])
entityId Int
attrId Int
##unique([entityId, attrId])
}
model Attr {
id Int #id #default(autoincrement())
entities EntityAttr[]
}
// Solution 2
model Entity {
id Int #id #default(autoincrement())
dateAttrs DateAttr[]
recordAttrs RecordAttr[]
// ... this pattern could continue for more Attr-like models
}
model DateAttr {
id Int #id #default(autoincrement())
name String
entity Entity #relation(fields: [entityId], references: [id])
value DateTime // Stronger typing in generated code
}
model RecordAttr {
// ... define another Entity #relation(...)
name String
value String
// ...
}
// ... and so on
Please note that the schema might not be 100% complete or accurate. It is mainly to get the point across.
Solution 1 has its merits where redundancy and the number of tables in the database is reduced significantly (depending on the number of Attrs). Its downfall comes as confusing queries*, possible case-specific type casting and no code-completion for the value field for each Attr-like model.
* by confusing, I mean that the option for simplified m-n queries in prisma is functionally disabled when using a custom join table (e.g. EntityAttr)
Solution 2 has its merits where the generated code results in more strongly typed code generation for the value field, however it falls in the number of generated tables (I don't actually know if more tables is a good thing or a bad thing, all I think is that if you have similar values, they ought to be in the same table).
What would you do in my shoes?

I was looking pretty long for an appropriate answer and found it here.
I'm not sure if it could be applied to your question, but this is question about prisma and polymorphism, so I think this code snippet might be useful for developers:
model Photo {
id Int #id #default(autoincrement())
likes Like[] #relation("PhotoLike")
}
model Video {
id Int #id #default(autoincrement())
likes Like[] #relation("VideoLike")
}
enum LikableType {
Photo
Video
}
model Like {
id Int #id #default(autoincrement())
Photo Photo? #relation("PhotoLike", fields: [likableId], references: [id], map: "photo_likableId")
Video Video? #relation("VideoLike", fields: [likableId], references: [id], map: "video_likableId")
likableId Int
likableType LikableType
}
Resuling relations in dbdocs:

Sometimes the use case can't be generalized to abstract and have a typing's.
if you control them and has a limited attribute sure you can create each attribute as a separate table each has it is own schema.
Some Times more freedom is needed or the blocks are dynamic.
Use Case: Build A Block Document Editor Like 'notion.so' and you want to let the user create custom blocks or configure them.
you can do it like :
model Document {
id String #id
blocks Block[]
}
model Block {
id String #id
value Json
index Int
customConfig Json?
document Document? #relation(fields: [documentID], references: [id])
documentID String?
blockType BlockType #relation(fields: [blockTypeID], references: [id])
blockTypeID String
}
model BlockType {
id String #id
name String
config Json
blocks Block[]
}
where config and custom config can contains html,custom css classes, link attribute color or anything.
using type script you can create block.types.ts and add different let say templates for the config's .
I hope that I was useful to you, To sum it, it depends on the requirements :>)

Related

Should I explicitly create a relation symmetrical to "Belongs To" or "Has Many"?

I am new to ORM (and GORM) so apologies if this is an obvious question, but it does not seem to be covered by the documentation.
I will be using the examples from the documentation as a base to my questions
Question 1: Belongs To
// `User` belongs to `Company`, `CompanyID` is the foreign key
type User struct {
gorm.Model
Name string
CompanyID int
Company Company
}
type Company struct {
ID int
Name string
}
A User belongs to one Company only → this is handled by the code
above
A Company has many User → is this implied by the code
above? Or should I add somehow a relation O2M in Company?
Question 2: Has Many
// User has many CreditCards, UserID is the foreign key
type User struct {
gorm.Model
CreditCards []CreditCard
}
type CreditCard struct {
gorm.Model
Number string
UserID uint
}
A User has 1+ CreditCard→ this is handled by the code
A CreditCard can belong to several users (say, a shared family CC) → is it implied? (if not: how to set up the O2M relationship).
Or is it, instead, a case where a CreditCard is explicitly configured to belong to only one user?
Q1: Based on how you defined your structs, you don't need an explicit O2M relationship in the Company struct, but when loading Company details, if you want to load all users that are assigned to that specific company, you need to add that field as well. It will need an additional function call like Preload or Joins, but you shouldn't need an explicit definition of this relationship.
type Company struct {
ID int
Name string
Users []User
}
Q2: The way the relationship is defined now, it is configured so that a CreditCard belongs to only one user. If you want a many2many relationship, you need to specify the relation table. There is more documentation on it here, but it should look something like this:
type User struct {
gorm.Model
CreditCards []CreditCard `gorm:"many2many:users_creditcards"`
}
type CreditCard struct {
gorm.Model
Number string
}

How to gracefully transform entity into DTO in Kotlin?

I am working on Kotlin + SpringBoot web service, in which I want to transform DTOs into entities in the most convenient way.
Entities:
#Entity
data class Shop(
#Id
#GeneratedValue
val id: Long,
val name: String
#OneToOne
val owner: User,
...
)
#Entity
data class User(
#Id
#GeneratedValue
val id: Long,
val name: String,
...
)
DTO:
data class ShopDTO(
val id: Long,
val name: String,
val ownerId: Long,
val ownerName: String,
...
)
So when someone wants to create a new Shop, my service gets a ShopDTO(name, ownerId) as request body, then I need to transform it into Shop object to be able to save it to the DB. Now here is how my mapper function looks like:
fun fromDTO(source: ShopDTO) = Shop(
id = source.id,
name = source.name,
owner = ???,
...
)
To be able to store a Shop with an owner I only need an id. It would be enough to create a new User with the given ownerId.
To achive this I tried these solutions:
Add default value to the fields in the User class.
Make the fields nullable.
Add a secondary constructor. This also needs default values.
Use some reflection magic to create an empty object and then set the id.
Call a findById method on the UserRepository with the given id.
I want to keep the non-null, immutable fields of my entities and do not want to use reflection. Also do not want to run an unnecessary select DB query just to get back the user by the id.
Could you please suggest me other options? How would you handle this situation? Is there any good mapper framework in Kotlin which can solve this problem?
Firstly, your question says you want to do entity -> DTO, but actually you want to do DTO -> entity, so you should clear that up.
Secondly, you are getting the shop name and owner Id in the ShopDTO. But you are assigning the owner Id to the shop Id in the your fromDTO(source: ShopDTO) function. Changing it up would be sufficient.
fun fromDTO(source: ShopDTO) = Shop(
name = source.name,
owner = ownerRepo.findById(source.ownerId)
)
Obviously, if you're using JPA, then you have to make a DB call to get the owner first. If your business logic doesn't ensure that a User with that Id exists, then you could write a method like this to make a user.
fun getOrCreateUser(ownerId: Long) =
ownerRepo.findUserById(ownerId) ?: User(
id = ownerId,
name = "Some random DefaultName"
).run(ownerRepo::save)
This would get a User by the Id if it exists, or create a new user with some generic name.
Do let me know if this solves your issue!

how to delete relational item in prisma2

My data model includes the following nodes:
model User {
id Int #id #default(autoincrement())
name String
posts Post[]
}
model Post {
id Int #id #default(autoincrement())
body String
user User #relation(fields: [userId], references: [id])
userId Int
}
I tried to delete one User like this:
async function deleteUser(_, args) {
const { id } = args
return prisma.user.delete({
where: { id: id }
})
}
But it gives an error: ... The change you are trying to make would violate the required relation UserToPost between the User and Post models.
Then how to delete one user? Even I tried to delete the post first then the user but again same error happened.
This has now been released as a preview feature behind a preview feature flag. You can read about it in the release notes for 2.26.0: https://github.com/prisma/prisma/releases/tag/2.26.0
The preview feature can be enabled by setting the preview feature flag referentialActions in the generator block of Prisma Client in your Prisma schema file:
generator client {
provider = "prisma-client-js"
previewFeatures = ["referentialActions"]
}
Looks like your table does not support CASCADE deletions and prisma does not automatically add it for you. You will have to manually update the definition of your table either while migration or after the fact.
so basically, alter your table definition.
ALTER TABLE public.Post
DROP CONSTRAINT Post_user_fkey,
ADD CONSTRAINT Post_user_fkey
FOREIGN KEY (user)
REFERENCES public.User(user)
ON DELETE CASCADE
ON UPDATE CASCADE;
Refer to these docs on options of configuring relational queries.

How to use same generated ID in two fields prisma-graphql

I'm implementing a graphql prisma datamodel. Here I have a type called BankAccount . I may need to update and delete them as well. I'm implementing this as immutable object. So, when updating I'm adding updating the existing record as IsDeleted and add a new record. And when updating an existing record I need to keep the id of the previous record to know which record is updated. So, I've came up with a type like this
type BankAccount {
id: ID! #unique
parentbankAccount: String!
bankName: String!
bankAccountNo: String!
isDeleted: Boolean! #default(value: "false")
}
Here the parentBankAccount keeps the id of previous BankAccount. I'm thinking when creating a bank account, setting the parentBankAccount as same as the id as it doesn't have a parent. The thing is I'm not sure it's possible. I'm bit new to GraphQL. So, any help would be appreciated.
Thanks
In GraphQL, generally if one object refers to another, you should directly refer to that object; you wouldn't embed its ID. You can also make fields nullable, to support the case where some relationship just doesn't exist. For this specific field, then, this would look like
type BankAccount {
parentBankAccount: BankAccount
...
}
and that field would be null whenever an account doesn't have a parent.
At an API level, the layout you describe seems a little weird. If I call
query MyBankAccount {
me { accounts { id } }
}
I'll get back some unique ID. I'd be a little surprised to later call
query MyBalance($id: ID!) {
node(id: $id) {
... on BankAccount {
name
isDeleted
balance
}
}
}
and find out that my account has been "deleted" and that the balance is from a week ago.
Using immutable objects in the underlying data store makes some sense, particularly for auditability reasons, but that tends to not be something you can expose out through a GraphQL API directly (or most other API layers: this would be equally surprising in a REST framework where the object URL is supposed to be permanent).

Servicestack Ormlite multi-column constraint fails where constraint includes Enum

I am using ServiceStack.Ormlite, and also make heavy use of the automatic handling of enums whereby they are stored in the db as strings but retrieved and parsed nicely back into Enums on retrieval, so I can do easy type-comparison - say, for a property "UserRole" in the db/table class "User" of enum type "UserRoleEnum" (just for demonstration).
This works great.. until I want to use the enum property to define a multi-column unique constraint
CompositeIndexAttribute(bool unique, params string[] fieldNames);
like:
[CompositeIndex(true, nameof(UserId), nameof(UserRole)]
public class User
{
public long UserId {get;set;}
public UserRoleEnum UserRole {get;set;
}
(as per :
How to Create Unique Constraint with Multiple Columns using ServiceStack.OrmLite? ).
At which time i get:
System.Data.SqlClient.SqlException
Column 'UserRole' in table 'User' is of a type that is invalid for use as a key column in an index.
I currently see options as:
a) Define UserRole as a string (isntead of UserRoleEnum ) in the table entity class and lose the Enum use.... having to manually test the value each time to confirm that the db value is one that i expect in my business logic later
b) Continue to use UserRoleEnum but lose the ability to declare multicolumn uniqueconstraints using the class attribute, and probably have to create these manually using a subsequent db migration script?
Is there any way to make the enums and the multicolumn constraint play nicely, out of the box?
This issue was because enum properties were using the default string definition fallback of VARCHAR(MAX) which SQL Server doesn't let you create indexes on whereas the column definition of a string property is VARCHAR(8000).
This issue is now resolved from this commit which now uses the VARCHAR(255) string definition of the EnumConverter Type Converter. This change is available from v4.5.5 that's now available on MyGet.
Otherwise you can also change the size of the column definition to match a string property by adding a [StringLength] attribute, e.g:
[CompositeIndex(true, nameof(UserId), nameof(UserRole))]
public class User
{
public long UserId { get; set; }
[StringLength(8000)]
public string UserRole { get; set; }
}

Resources