One-to-One Relationships

Loading "Database Design"
👨‍💼 We still need to deal with the Note images. And we're also going to add images to the users as well. Before we do, Olivia the Owl would like to have a word with you about data modeling...
🦉 One thing you want to think about when you're building out your data model is... the future.
CAUTION: The future is impossible to predict. All you really can reasonably predict about the future is that things will change. But you don't know how. That said, it can be useful to build things in such a way that optimizes for change provided it doesn't make things sub-optimal for the present.
Consider, for example, that you are building a house. You're considering what to do about the pool you want one day. You know you don't want it now, but you may want to add one in the future. With that in mind, you may think about where the pool should go, and you decide to avoid planting trees there. Depending on how confident you are that you will get the pool you may even consider placing the plumbing for the pool in the ground before you pour the foundation so you don't have to pay the cost of digging things up later. You just need to weigh the costs of doing that now vs. the cost of doing it later and the likelihood you'll actually get the pool.
For our notes to have images, we just need a place to store the bytes for the image, its content type, and the alt text. It's not much data. But if you think about a note taking app, it's reasonable to assume folks will want to upload more than just images to their notes. Maybe they'll want to upload a csv or a pdf or any number of other types of files.
So we should think about modeling things in such a way that it makes supporting these other file types possible without complicating our data model in the present or having a difficult migration in the future.
👨‍💼 Thanks for that Olivia.
🦉 Oh, one other thing... On the subject of images... We'll be storing our images directly in the database. Turns out, in some cases, serving files from SQLite can be 35% faster than the file system (and can be more space efficient as well)! Even in situations where that's not the case, it's certainly not much slower. So while you definitely can bring in another service to manage storing the images for you, or store it to a persistent volume, starting out with the images in the database is a perfectly reasonable approach for many applications.
👨‍💼 Ok, great. Thanks Olivia! So, with that said, instead of making a single Image model and using that for both the User and Note models, we're going to have UserImage and NoteImage models that each hold the content type, file bytes, and alt text.
This may feel like we're duplicating code and schema, which we definitely are, but if you consider the future possibilities, it will be much easier for us to add a NotePDF model later without impacting other models than to try and get all polymorphic with a single generic File model (polymorphism and databases don't mix well).
Here's a visualized model of this:
visual representation of polymorphism as described with a 👎 emoji
And here's what we're going to do instead:
visual representation of duplication as described with a 👍 emoji
With these new models in place, we can connect them to the Note and User models. So we'll have the following relationships:
  • User -> UserImage: one-to-one
  • Note -> NoteImage: one-to-many
Don't forget that the Prisma VSCode extension, can automatically capitalize the property names it adds which is annoying. Just double check that all properties are lower-case.
The emoji should be in the schema file to help guide you through this.
💰 Remember, if you get stuck on the syntax, remember you can check the Diff Tab to check your work against the solution.
Once you've got the model in a good place, then let's push that to the database:
npx prisma db push
And now let's check out the studio:
npx prisma studio
Now we want to create a new image/file. The tricky bit here is Prisma studio represents files as base64 encoded strings which is fine, I even prepared this base64 image for you:
data:image/jpeg;base64,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
Sadly, I couldn't find a way to create one-to-one required relationships in the Prisma studio. So it's time to move to some code!
Let's create a script to do this for us:
Go ahead and stick this in there:
import { PrismaClient } from '@prisma/client'
import fs from 'node:fs'

const prisma = new PrismaClient()

const firstNote = await prisma.note.findFirst()

if (!firstNote) {
	throw new Error('You need to have a note in the database first')
}

await prisma.note.update({
	where: { id: firstNote.id },
	data: {
		images: {
			create: [
				{
					altText: 'an adorable koala cartoon illustration',
					contentType: 'image/png',
					blob: await fs.promises.readFile(
						'./tests/fixtures/images/kody-notes/cute-koala.png',
					),
				},
				{
					altText: 'a cartoon illustration of a koala in a tree eating',
					contentType: 'image/png',
					blob: await fs.promises.readFile(
						'./tests/fixtures/images/kody-notes/koala-eating.png',
					),
				},
			],
		},
	},
})
We'll dive deeper into this later...
Next, let's run it. Because this is a TypeScript file, you'll need to use tsx instead of node:
npx tsx ./prisma/seed.ts
Great, with that done, now open up the studio:
npx prisma studio
You should have your note with the two images and two files in there now. Huzzah! We'll get further into the seeding capability later, but this is a good verification that our model is working.