A Deep Dive Into Relationship CRUD with Models
In this lesson, we'll take a deep look at how we can perform CRUD operations with one-to-one, one-to-many, many-to-one, and many-to-many relationships using our Lucid Models.
- Author
- Tom Gobich
- Published
- Apr 02
- Duration
- 18m 5s
Developer, dog lover, and burrito eater. Currently teaching AdonisJS, a fully featured NodeJS framework, and running Adocasts where I post new lessons weekly. Professionally, I work with JavaScript, .Net C#, and SQL Server.
Adocasts
Burlington, KY
Transcript
A Deep Dive Into Relationship CRUD with Models
-
[MUSIC]
-
So at this point, we know most of the big picture,
-
but there is one final piece of the puzzle to put together,
-
and that's how we combine our relationships together
-
with our models to actually create data with our models.
-
So I'm going to hit "Control tilde" here inside of
-
our text editor and this will bring up the terminal.
-
Now, I just have it taking up full screen,
-
you can adjust this right here.
-
This is going to give us a little bit more room to work with
-
compared to working directly inside of our terminal application.
-
I'm going to run node.ace.repl to jump into a REPL session.
-
Await.loadModels to load up our models.
-
So let's start with one-to-one relationships.
-
So let's create a brand new user.
-
So let's do const user equals awaitModels.user.read.
-
We'll pass in an object here.
-
I think this takes a full name.
-
I'll put my name here,
-
email, we'll just do [email protected],
-
and then a password of password.
-
Let's go ahead and run that.
-
Cool. So now we should have our user.
-
If we go ahead and serialize that,
-
we'll see my details printed out.
-
However, at present, if we go ahead and try to await
-
user.loadProfile and we do user.profile,
-
we're going to get null because we haven't
-
actually created a profile for this user.
-
With our factory, we have this creating
-
all of our profiles along with our users,
-
which is why we went ahead and created
-
a brand new user here so that we can walk through this.
-
So there's a couple of ways that we can actually add
-
data with a relationship for our user.
-
So we could do await user.related as we've
-
done prior to reach through the relationship,
-
and then provide the relationship name.
-
So that would be our profile.
-
So far, we've just used this to
-
access the relation query builder,
-
but we can also call create directly off of this as
-
well for relationships pointing to a single record.
-
We just provided an object
-
for the record that we want to create.
-
So from our user model,
-
we're grabbing the relationship for our profile,
-
and then we're going to create a new profile
-
specifically for the user that we started this call from.
-
Lucid will automatically bind this user's ID
-
onto the profile that ends up getting created.
-
So it will automatically set that user ID that's on
-
our profile model just via this relationship.
-
Meaning, all that we have to do is define the,
-
I think we call it the biography column,
-
and everything else will be automatically
-
taken care of for our current profile.
-
So this is my bio.
-
Let's hit "Enter" there.
-
Cannot define biography on profile model since
-
it's not defined as a model property.
-
So I got the name wrong, my bad.
-
Let's go check our profile model here.
-
Oh, it's called description.
-
We'll elevate our panel size back up for our terminal.
-
I'll go ahead and hit "Up",
-
and I'll switch our biography column to description.
-
Run that, and there we go.
-
So now if we await user.loadProfile,
-
and then user.profile,
-
and we go ahead and serialize that,
-
we're going to see that we actually get back
-
that newly created profile instance.
-
Additionally, whenever we called create,
-
it immediately returned the model
-
that was created as a result as well.
-
So you could do cost profile equals await
-
user related profile.create and get
-
back that brand new profile that way as well.
-
So let's go ahead and await user.profile.delete
-
to delete that user's profile.
-
So now if we do user.profile.isDeleted,
-
we're going to get back true.
-
Remember that this is a flag that holds whether or not
-
the current record has been
-
deleted out of the database using the model.
-
This paves way for us to create a brand new profile.
-
So I've cleared out our terminal,
-
reloaded our rep session to give us more working room here.
-
Let's go ahead and grab that user again.
-
So cost user equals awaitModelsUser.findByOrFail,
-
we'll find by e-mail,
-
and we've provided that with [email protected].
-
So now we should have our user, and if we type out user,
-
we could see in the hint down here that we do indeed have that user.
-
We can also create a brand new record by
-
instantiating the model itself as well.
-
So we can do const profile equals newModels.profile.
-
Within our code, I'm going to scroll down to the bottom here.
-
This would be the same as doing const profile
-
equals new profile directly with our model class.
-
So we'll run that on the profile.
-
We'll set the description value equal to,
-
we'll just do test right now to give it something.
-
So at this point, we have a profile instance of
-
our model and a user instance of our model,
-
but our profile instance is not persisted into the database,
-
nor is it bound to our user.
-
Well, we can do both of those with one call by doing
-
awaitUser.relatedProfile to reach through with the relationship again,
-
and calling .save and providing it
-
the entire model that we want to relate with,
-
which would be our profile.
-
This will both persist our profile into the database
-
and relate it to our user record as well.
-
So we've run this, and then awaitUser.loadProfile,
-
user.profile, let's go ahead and serialize that.
-
There is our brand new profile with our test description.
-
Once more, let's go ahead and awaitUserProfile.delete.
-
Back with a fresh slate here,
-
we have our user once more.
-
Another way that we can join these relationships together,
-
and this one is unique to belongs to relationships.
-
So this will be coming at it from our profile side,
-
is to use associate and disassociate.
-
So const profile equals new,
-
and let's do models.profile once more,
-
instantiating a new instance of our profile class.
-
Then we'll do profile.description equals,
-
I probably spelled that wrong,
-
but we'll just do associated there.
-
Now from our profile side,
-
we can awaitProfile.related,
-
reach for the user relationship,
-
and we can associate our profile
-
with the particular user instance that we have.
-
Again, this will save our profile into the database,
-
and set its user ID with the user's ID that we've provided in.
-
We'll go ahead and run that.
-
Now if we await,
-
and let's do this from our user side,
-
user.loadProfile, user.profile.serialize once more,
-
we'll see that our profile was successfully associated with this user.
-
Now, the user ID relationship for our profile is required.
-
But if this relationship were nullable,
-
we could then dissociate these two from one another as well,
-
by doing something relatively similar to our associate call.
-
We could awaitProfile.related user,
-
and then call.dissociate to pluck the two apart from one another,
-
essentially removing the user ID off our profile and setting it to null.
-
Again, our profile's user ID is required,
-
it's not nullable in the database.
-
So if I were to run this,
-
we're going to get an error due to that violation of the not null constraint.
-
But if that relationship were nullable,
-
that's one way that you could dissociate the two from one another.
-
So we're back into a fresh REPL session with everything cleared out.
-
Similarly to how we created a record through
-
the relationship for our one-to-one relationship between our user and our profile,
-
we can do the same using a create many call for one-to-many relationships as well.
-
So if we create a new Sinist here,
-
I'm going to do const me because I find Sinist rather hard to spell correctly,
-
equals awaitModels.sinist.create,
-
providing a first name of Tom and a last name of Govij.
-
So we'll just create a record here for myself.
-
I believe headshot URL has an empty string default,
-
so we should be good to just create a record with that.
-
Cool. Everything looks good there.
-
If we do await me.related,
-
and I'm going to collapse this back down and take
-
a look to see what we call this relationship.
-
We have moviesDirected and moviesWritten as our hasMany relationships here.
-
So we can do either one of those for our example,
-
moviesWritten.createMany here to create many in one go.
-
So we can provide in an array,
-
and then inside of our array,
-
we just need to create objects for however many movies we
-
want to add and fill them out with each movie's details.
-
So let's see, we have our status ID.
-
I'm just going to default that to one.
-
Our director ID,
-
we'll default that to one.
-
Our writer ID will be automatically set via the relationship because we're reaching
-
through this user via the movie's written relationship,
-
which is bound by the written ID.
-
So we can pass over the written ID,
-
go to title, do written by me or something like that.
-
I believe everything else there is optional.
-
So let's move on to our second one, status ID.
-
We'll just do one here, director ID.
-
We'll do one again, title,
-
written by me too.
-
Let's go ahead and run that.
-
As you can see, just like create,
-
it's going to return back each of the movies that was created with this relationship.
-
Now we should be able to do await me.loadMoviesWritten,
-
and then me.moviesWritten,
-
and we'll map over those,
-
movie.serialize like so,
-
and we get back both movies written by me and written by me too.
-
So we're back with a fresh REPL session with me defined once more.
-
Again, just like with our user profile example,
-
whenever we called save through the relationship,
-
we took a model instance that wasn't yet persisted into the database,
-
saved it, and bound it to the user all in one blow.
-
We can do that exact same thing here using save many for our one-to-many relationships.
-
So let's do const movie1 equals new models movie,
-
and we'll create a new instance of that.
-
Now, rather than doing movie1.statusId,
-
so on and so forth, taking up multiple lines here,
-
what we could alternatively do is do movie1.merge,
-
and this will merge whatever object data we provide into our current movie data.
-
So for example, we could just set just as we did with our create call,
-
our status, id of one.
-
How about for this one, we set these movies as directed.
-
So we'll set our writer ID rather than our director ID,
-
and then our title directed by me1,
-
and we'll merge that data in.
-
So now if we do movie1.title,
-
we see it was directed by me1.
-
Cool. So let's do const movie2 equals new models movie,
-
create an instance of that,
-
and we can do movie2.merge,
-
statusId of one,
-
writerId of two,
-
and title of directed by me2,
-
and we'll merge that in.
-
So now for movie2.title,
-
we have directed by me.
-
So let's take await me.related,
-
we'll reach for our movies directed relationship,
-
and we'll .saveMany and provide an array of all of
-
the movies that we want to save with this movies directed relationship to me.
-
So we'll do movie1 and movie2.
-
We'll run that, and now we should be able to await me.loadMoviesDirected,
-
me.moviesDirected.mapMovie, movie.serialize,
-
and we should see directed by me1 and directed by me2.
-
Awesome. So now we know how to create one-to-one and one-to-many
-
relationships through our models via their helper methods.
-
What about many-to-many?
-
So let's go ahead and get a movie.
-
So the const movie equals awaitModels.movie.create,
-
we'll set the status ID to one,
-
writer ID to one,
-
and director ID to one with a title of attached.
-
Create that movie here, type it out,
-
there we are, we see it.
-
So thanks to our seeder, we know that we have plenty of
-
synapse and we know that we have somebody with an ID of 1,
-
2, 3, and so on and so forth.
-
So if we took those IDs and we wanted to bind them to
-
this movie as either cast or crew members,
-
what we could do is await movie.related to reach for
-
the relationship applicable for the action that we want to take.
-
So we do cast members to bind in a cast member here,
-
.attach, and then provide in an array of IDs for
-
synapse that we want to attach as cast members to this movie.
-
So if we run that and we do await movie,
-
load cast members,
-
movie.castmembers.map, cast, cast.serialize,
-
we're going to see three synapse records bound to
-
this movie as cast members.
-
But hold that thought.
-
What about our pivot table data?
-
Well, that information isn't defined on the model itself.
-
Anytime that we're working with the models,
-
they will always return back instances of
-
those models unless we call Poyo as we have prior.
-
Anything extra not defined on the model itself is bound onto
-
an extras object and by default extras will not
-
serialize with the model unless you explicitly tell it to.
-
So first, let's check and verify that this was
-
the relationship that we had eerily
-
loading our pivot table data.
-
Right there, yes, it is.
-
So let's jump over to our synapse model.
-
So on our model, all that we need to do is add
-
serialize extras and set that to true.
-
Now, it will include those extras
-
anytime that we serialize our models results.
-
So for example, if we take a look at
-
movie.castmembers as a whole and we take a look at those raw data,
-
within one of our synapse models as we see right here,
-
we have a $extras object.
-
This is where our pivot table data is.
-
This is also where any aggregates go.
-
So if we reach for movie.castmembers and we go for the first index,
-
.extras, we'll see our pivot movie ID,
-
pivot synapse ID, character name,
-
sort order, created at and updated at columns.
-
So all are accessible.
-
However, we've bound these cast members
-
without specifying a sort nor a character name.
-
So let's go ahead and remove these synapse from this movie.
-
To do that, we can either remove
-
everybody by doing await movie.related castmembers.detach.
-
If we don't provide anything to detach,
-
it will remove all cast members from this movie.
-
However, we can scope this to just specific IDs by providing those in as well.
-
So we've attached one, two, and three.
-
They are the only synapse bound to this movie.
-
But if we only supply one and two,
-
three will remain as a cast member.
-
Right now though, we want to go ahead and delete everybody.
-
So we'll just go ahead and detach altogether.
-
I'm going to go ahead and reload our model so that we get
-
that serialize extra applied and we can see exactly what it does.
-
So as a reminder,
-
here's the serialized results for
-
our cast members without the serialize extra applied.
-
So we're back with a fresh travel session in our movie re-queried.
-
Let's go ahead and do await movie related castmembers.attach.
-
For right now, let's just do the IDs one and two.
-
We'll run that. We'll do await movie.load
-
the cast members and then movie.castmembers.map,
-
cast, cast.serialize.
-
Now, whenever we run that,
-
we're going to see meta applied onto these results as
-
well with that extra information that
-
was on the raw model instance whenever we inspected it.
-
Now, we need to take care of including
-
pivot data information whenever we're attaching.
-
So how can we do that?
-
Well, let's go ahead and detach once more.
-
So await movie related cast members detach,
-
and let's get a fresh instance.
-
So let's get back to where we're attaching.
-
So we'll do await movie related cast members attach.
-
Instead of providing an array of IDs,
-
since we need to provide additional information
-
with each one of the IDs that we're attaching,
-
which is what character they played and what sort we want them to display in,
-
we'll want to provide an object.
-
I'm going to go ahead and get rid of our ending here and
-
hit "Enter" so that we do multi-lines.
-
The key for the object is going to be the ID of the record that we want to attach.
-
So if we want to attach a sentence with an ID of one,
-
the key would be one.
-
Then for the value of this ID of one,
-
we would provide an object with the additional information
-
that should be attached onto the pivot table.
-
So for our cast, this would consist of the character name,
-
maybe that's Linus, and then the sort order,
-
and we'll do a zero-based index here.
-
So we can do comma, and then attach in a sentence with an ID of two,
-
give them a character name of Snoopy,
-
and a sort order of one.
-
Go ahead and end our attachment there and run that.
-
My bad, I missed the curly brace there,
-
so I retyped it here.
-
We'll do curly brace and then end our parentheses,
-
and then we can call that, and there we go.
-
So now we can await movie.load our cast members,
-
and then movie.castmembers.map,
-
cast, cast.serialize, so that we can actually
-
see the results here in our terminal and run that.
-
And there we go, we have our synapse with an ID of one,
-
and then their meta pivot table data
-
includes a character name of Linus,
-
and a sort order of zero,
-
and then our synapse with an ID of two here
-
has a character name of Snoopy,
-
and a sort order of one.
-
So that successfully got applied onto our pivot table data
-
for those two relationships.
-
So we know attach will take an array of IDs,
-
or an object with a key where the key is the ID
-
and a value where the value is pivot table data
-
as an argument.
-
We know detach will take an array of IDs or nothing at all.
-
If we provide nothing at all, it will detach everybody.
-
If we provide an array of IDs,
-
it will only attach those with the IDs that we've provided.
-
And there's actually a third argument as well.
-
So if we do movie.related, stick with our cast members here,
-
and there is a sync method that we can call
-
that accepts the same arguments as attach does.
-
So we could provide an array of IDs,
-
or we could provide a key value pair
-
where the key is our attached ID,
-
and the value for that key is the pivot table data.
-
To keep things simple here, just to show what this does,
-
we'll just provide an array of IDs.
-
So we have an ID of one and two attached onto this movie.
-
If we sync a synapse with an ID of three, run that.
-
Oh, I forgot to await it,
-
but it will have completed successfully by now.
-
So we can move forward and do await movie.load,
-
and we'll just reload our cast members.
-
So reload them, and movie.castmembers.mapcastcast.serialize,
-
we'll see that all that we get back
-
is the synapse with an ID of three.
-
The other two were removed,
-
and that's because sync will detach everybody
-
and then reattach only those with the information
-
that we've provided inside of sync,
-
essentially syncing the data with whatever we have provided.
-
Anything not provided is considered outdated and removed.
-
And there we go.
-
That's how you can manage your relationships
-
with your Lucid models for one-to-one,
-
one-to-many, many-to-one, and many-to-many relationships.
-
Introduction
-
Fundamentals
-
2.0Routes and How To Create Them5m 23s
-
2.1Rendering a View for a Route6m 29s
-
2.2Linking Between Routes7m 51s
-
2.3Loading A Movie Using Route Parameters9m 17s
-
2.4Validating Route Parameters6m 6s
-
2.5Vite and Our Assets6m 38s
-
2.6Setting Up Tailwind CSS9m 5s
-
2.7Reading and Supporting Markdown Content4m 32s
-
2.8Listing Movies from their Markdown Files8m 51s
-
2.9Extracting Reusable Code with Services7m 4s
-
2.10Cleaning Up Routes with Controllers4m 52s
-
2.11Defining A Structure for our Movie using Models9m 38s
-
2.12Singleton Services and the Idea of Caching6m 11s
-
2.13Environment Variables and their Validation4m 16s
-
2.14Improved Caching with Redis10m 44s
-
2.15Deleting Items and Flushing our Redis Cache6m 46s
-
2.16Quick Start Apps with Custom Starter Kits6m 28s
-
2.17Easy Imports with NodeJS Subpath Imports8m 40s
-
-
Building Views with EdgeJS
-
3.0EdgeJS Templating Basics8m 49s
-
3.1HTML Attribute and Class Utilities6m 9s
-
3.2Making A Reusable Movie Card Component10m 24s
-
3.3Component Tags, State, and Props4m 53s
-
3.4Use Slots To Make A Button Component6m 56s
-
3.5Extracting A Layout Component5m 13s
-
3.6State vs Share Data Flow2m 59s
-
3.7Share vs Global Data Flow6m 7s
-
3.8Form Basics and CSRF Protection6m 13s
-
3.9HTTP Method Spoofing HTML Forms3m 3s
-
3.10Easy SVG Icons with Edge Iconify7m 57s
-
-
Database and Lucid ORM Basics
-
4.0Configuring Lucid and our Database Connection4m 3s
-
4.1Understanding our Database Schema9m 35s
-
4.2Introducing and Defining Database Migrations18m 35s
-
4.3The Flow of Migrations8m 28s
-
4.4Introducing Lucid Models5m 43s
-
4.5Defining Our Models6m 49s
-
4.6The Basics of CRUD11m 56s
-
4.7Defining Required Data with Seeders11m 11s
-
4.8Stubbing Fake Data with Model Factories13m 48s
-
4.9Querying Our Movies with the Query Builder15m 30s
-
4.10Unmapped and Computed Model Properties3m 24s
-
4.11Altering Tables with Migrations7m 6s
-
4.12Adding A Profile Model, Migration, Factory, and Controller2m 57s
-
4.13SQL Parameters and Injection Protection9m 19s
-
4.14Reusable Query Statements with Model Query Scopes8m 11s
-
4.15Tapping into Model Factory States9m 15s
-
4.16Querying Recently Released and Coming Soon Movies4m 59s
-
4.17Generating A Unique Movie Slug With Model Hooks7m 59s
-
-
Lucid ORM Relationships
-
5.0Defining One to One Relationships Within Lucid Models5m 49s
-
5.1Model Factory Relationships2m 54s
-
5.2Querying Relationships and Eager Vs Lazy Loading5m 17s
-
5.3Cascading and Deleting Model Relationships5m 16s
-
5.4Defining One to Many Relationships with Lucid Models6m 56s
-
5.5Seeding Movies with One to Many Model Factory Relationships5m 24s
-
5.6Listing A Director's Movies with Relationship Existence Queries8m 41s
-
5.7Listing and Counting a Writer's Movies8m 41s
-
5.8Using Eager and Lazy Loading to Load A Movie's Writer and Director5m 18s
-
5.9Defining Many-To-Many Relationships and Pivot Columns9m 48s
-
5.10Many-To-Many Model Factory Relationships4m 50s
-
5.11A Deep Dive Into Relationship CRUD with Models18m 5s
-
5.12How To Create Factory Relationships from a Pool of Data13m 55s
-
5.13How To Query, Sort, and Filter by Pivot Table Data9m 47s
-
-
Working With Forms
-
6.0Accepting Form Data12m 15s
-
6.1Validating Form Data with VineJS9m 29s
-
6.2Displaying Validation Errors and Validating from our Request7m 16s
-
6.3Reusing Old Form Values After A Validation Error2m 3s
-
6.4Creating An EdgeJS Form Input Component5m 28s
-
6.5Creating A Login Form and Validator5m 1s
-
6.6How To Create A Custom VineJS Validation Rule9m 7s
-
-
Authentication & Middleware
-
7.0The Flow of Middleware7m 49s
-
7.1Authenticating A Newly Registered User4m 14s
-
7.2Checking For and Populating an Authenticated User2m 10s
-
7.3Logging Out An Authenticated User2m 24s
-
7.4Logging In An Existing User6m 54s
-
7.5Remembering A User's Authenticated Session6m 55s
-
7.6Protecting Routes with Auth, Guest, and Admin Middleware5m 36s
-
-
Filtering and Paginating Queries
-
8.0Creating A Movie List Page3m 43s
-
8.1Filtering A Query By Pattern Likeness7m 9s
-
8.2Filtering Our List by Movie Status5m 47s
-
8.3How To Apply A Dynamic Sort Filter To Your Query7m 12s
-
8.4Joining SQL Tables To Order By A Related Column4m 49s
-
Validating Query String Filter Values7m 23s
-
How To Paginate Filtered Query Results9m 15s
-
Pagination First, Last, Next, and Previous Buttons4m 2s
-
-
User Watchlist
-
An Alternative Approach to Many-To-Many Relationships4m 56s
-
Toggling A Movie in an Authenticated User's Watchlist9m 56s
-
Listing and Filtering User Watchlist Items7m 34s
-
Allowing Users To Toggle A Movie As Watched4m 44s
-
Filtering By User's Watched Status6m 7s
-
Defining A Composite Unique Constraint4m 46s
-
Join The Discussion! (0 Comments)
Please sign in or sign up for free to join in on the dicussion.
Be the first to Comment!