Welcome!
You can view the training on YouTube:
→ View the training on YouTube
Follow along using the instructions below.
If you have any questions,
- Ask us (@samselikoff or @ryanto) in the
#topic-media
channel in the Ember Community Discord - Email us at [email protected]
From a directory,
git clone [email protected]:embermap/emberconf-2020-domain-modeling.git
cd emberconf-2020-domain-modeling
yarn install
ember s
In most web apps, the database is the source of truth.
As frontend developers we often don't have to deal directly with it. But we do need to understand enough about our server resources to be able to do our job.
Traditional SSR has the luxury of getting to speak directly to the database (the source of truth) on every request. But SSR can't build rich experiences on the web, like Slack or Google Calendar.
These types of JS apps - like the apps we build with Ember - must grapple with the fact that the source of truth is remote, and requires a network call to communicate with.
All of this motivates the question of how best to get data from our servers to our frontends, which is where domain modeling comes into play.
Explore the inspector
Resources vs. database records.
[ Add Model ]
[ Add seeds ]
[ Replace the 5 routes with the resource shorthand ]
Let's add another model, called a Message.
Messages look like
let message = {
text: "hello emberconf!"
};
Define a Message model and create some messages in seeds
. Add a resource to routes. Explore them in the database tab and make requests to them from the client.
How can we associate these two things? Using a foreign key.
[ Make a belongsTo relationship ]
Mirage's ORM helps manage foreign keys for us, just like most backend systems.
Users can also have activities.
Create a new activity model, associate it with a user, and create some activities for the user.
A sample activity looks like this:
let activity = {
userId: 1,
kind: "mention" // upload, reaction
};
[ Exercise: Create activity ]
Try deleting a user! What happens to the associated messages and activites?
The other association type is a Has Many.
Let's go back to just having users and messages that are unassociated.
Instead of associating the user with the message, we can associate the messages with the user.
[ Add user.messages hasMany association ]
So far we've seen one-way relationships. Mirage (and most backend systems) support two-way relationships, or relationships that have an inverse.
Let's make user and messages a one-to-many relationship.
[ Add user.messages and message.users ]
Notice how Mirage keeps the fks in sync, regardless of which side you edit.
Alright - now that we have our data modeled in our database, it's time to fetch it. But how can our client best fetch it?
Let's say we're building Slack and we want to display a list of messages. Need to show the message and author for each message.
[ Fetch /messages. Fetch /users. ]
Could fetch separately and then stitch.
We want to enable our clients to fetch a graph of data in one request.
[ Define message serializer, add include ]
The default is to sideload the related data. Sideloading produces normalized data.
[ Set embed: true ]
This data is denormalized. You can see duplicated information when compared to our db.
You might hear this referred to as a materialized view of the database.
Now, let's say we wanted to build a screen for a single user, and show them all their messages. How might we get the data? What would the query be?
[ Exercise: Fetch the user and their messages ]
Did you go with embedded or sideloaded? Was there data duplication?
In the last exercise the server made the choice about what related data to include in the response. Sometimes this makes sense, but in recent years tools like JSON:API and GraphQL have shifted the control to the client.
Let's look at a JSON:API backend.
[ Fetch messages ]
[ Fetch messages with their users ]
Q: Does JSON:API produce normalized or denormalized data?
Putting power in the client keeps the server more flexible + able to support more UI use cases.
...sound familiar?
Schema. Our domain modeling is the same. No more serializer. One route handler.
So, it's like JSON:API, but often will produce denormalized data. Hyperfocused on being suitable for the particular view. Completely generalized.
Q: What are some of the pros/cons of normalized vs. denormalized data? (Client-side identity)
Ok, circling back to some more domain modeling. Let's look at another type of relationship.
Q: How should we associate users to channels?
[ Add user.channels and channel.users ]
Notice how the arrays of foreign keys stay in sync.
How might the client add and remove users to channels?
[ Send PATCH removing Sam from a channel ]
Can be tempting to add new ad hoc endpoints here: PATCH /users/1/add-channel
.
What are the alternatives?
[ New model ]
Time for some practice with joins. We want users to be able to be friends with each other.
One thing to know: if your relationship name doesn't match your model name, you can specify it like this:
message: Model.extend({
author: belongsTo("user")
});
Some keywords from this exercise: inverse, self-reflexive relationships.
Now you should know just enough about databases and server resources (without having to understand the details, like managing database indexes) to have a much better grasp on how data gets transferred between your frontend and backend.
The data layer is one of the most complex aspects of building a JavaScript app. Domain modeling - how you choose to store and relate your data - has huge ramifications for how much your app's complexity can scale as you add more features, and how clean (or not) your frontend code stays.
Data modeling is complex and there are definitely situations where experienced developers disagree over the correct data model. The best way to get better is with practice! You will develop an intuition for when it makes sense to introduce new resources to your system, and how best to relate those resources to your existing graph of models.