In this issue, we'll use Spring Data JPA (Java Persistence API) to manipulate the database automatically. There's a lot of "magic" here, meaning Spring will handle many things automatically, like connecting to the database, forming SQL queries, and retrieving and parsing results. You'll still need to tell JPA what kinds of queries to run and the structure of the database, but we'll walk through that together.
Steps
Setting Up Spring Data JPA
First, we need to add the Spring Data JPA dependency. In the build.gradle at the root of your project, go to the dependencies section, and add this bit:
You're probably wondering what this application.properties file is. Spring, and many Java systems, use .properties files to define configurations for the applicaton.
In this case, we configure the datasource for Spring and jpa configurations for Spring. The spring.datasource.url is what we call a "connection string." Usually, it's easiest to just search for the connection string for Spring and your database, in this case, PostgreSQL.
Why localhost:5432? If you look at your compose.yml again, you'll see that the database is exposed on port 5432. This means that if you connect to port 5432 on your local computer, Docker will forward that connection to the database in the Docker container.
The driver-class-name specifies what kind of database we're connecting to and where to find the code to make that connection. We could use MySQL, SQL Server, even Oracle. We added org.postgresql as a dependency so that we could use it to connect to the PostgreSQL database. This configuration is how we tell Spring about all of that.
The spring.jpa.database-platform value tells Spring what type of SQL to use. Remember that every database has slightly different SQL? This is how we tell Spring exactly what kind of SQL to use. Why couldn't Spring just use the driver to decide? Well, there could be more than one driver for a type of database. For example, you can use the MySQL driver to connect to a MariaDB server, but there may be differences between MySQL and MariaDB SQL. (I don't recall. It's been awhile.)
The generate-ddl option tells Spring Data JPA that it should generate the table definitions. This is useful when we don't have a separate system to manage database structure for us. Often, teams will use a separate system, but for now, this is okay.
The spring.jpa.hibernate.ddl-auto option tells Spring how it should handle the generated DDL automatically. There are case where you may want to generated the DDL for you to check but not automatically update your database. This allows you to control this behavior. If you're wondering what hibernate is, it's the name of the main library that Spring Data JPA uses to manage database entities.
Well done! Now, Spring has everything it needs to connect to your database.
Setting Up Our Cat Model
TODO: CatNotFoundException
Open up your Cat.java and add the @Entity annotation to the Cat class. Don't forget to add the import! You can either use your editor to help you do it automatically or manually add import jakarta.persistence.Entity; near your other imports.
The @Entity annotation tells Spring Data JPA that our Cat class should correspond to a database table.
Now, we need to tell Spring about which class is our primary key. We can do this easily by specifying the @Id annotation. There's just one problem... We don't have an ID for our cats! So let's add the below to our Cat class:
@Id
private Long id;
Don't forget to import! jakarta.persistence.Id
Why Long? The database supports larger numbers than a simple int can support. The long data type is large enough to support the possible values stored in our database.
Why the capital Long instead of the primitive long? The Long object allows for null values. The primitive long type initializes to 0.0, but we want to allow a proper null.
Spring now has an ID field that it can use in the database as the primary key. Remember in #11 how we used GENERATED BY DEFAULT to tell PostgreSQL to automatically set the ID? We need to tell Spring that PostgreSQL should do this work. You can do this by adding the @GeneratedValue(strategy = GenerationType.AUTO) annotation to your new id attribute. Do that now. Don't forget to import GeneratedValue and GenerationType! I bet you can figure out the correct import statements by now.
On your Cat class, add the @NoArgsConstructor to have Lombok generate a constructor that creates an empty Cat. Spring Data JPA will need this, later. Import lombok.NoArgsConstructor to make sure Java can find it.
Delete your existing constructor. We won't need the random selection for the future. If you're worried about losing the code, remember that Git has all of your history safely stored away.
In your handleBreeding method, look for when you create a new Cat. Make sure to add a parameter for the id that we've added to the Cat class. Just add a new first parameter as null to the new Cat() method call.
Remove all @Getter and @Setter lines. Instead, put a @Data annotation on the Cat class. The @Data annotation acts as both @Getter and @Setter. Spring Data JPA will need setters for all attributes in order to do its work. Don't forget to remove the imports for @Getter and @Setter and to add the import for @Data.
Well done! With this, Spring Data JPA knows about your cat objects and how they should be stored in the database.
Setting up Data Transfer Objects
There are cases where we'll want to not just display the raw data from the database, for example, providing user information but not user passwords. We call the data formats that go in and out of API endpoints as "DTOs," or "Data Transfer Objects." We also need to convert between the DTOs and our database model objects. We do this using a "model mapper." In this context, "mapping" is the act of connecting data fields from one object to another. There is a library to help us do model mapping, so we'll need to add that.
Lastly, we'll have input and output data for three different endpoints: a list of cats, one cat's details, and creating a new cat. Usually, the resources (cats) in a list have only summary information, whereas the resource details have all or most of the information. The information to create a new resource is usually all of the data except database-generated values, like IDs. So we'll create three DTOs - one for each.
Reload your Gradle configuration to download the new dependency. You can use your IDE or just run ./gradlew in your project directory.
In your CatsonedayApplication class, add the below method:
@Bean
public ModelMapper getModelMapper() {
return new ModelMapper();
}
Create a new dto package under your org.yuhee.catsoneday package.
Create a new class in your dto package named CatInListDto.
Add the @Data and @NoArgsConstructor annotations to the class.
Add the id, name, sex, and color attributes from your Cat model without the annotations.
Create a new class in your dto package named CatDetailDto.
Add the @Data and @NoArgsConstructor annotations to the class.
Add all of the attributes from your Cat model without the annotations.
Create a new class in your dto package named CreateCatDto.
As usual, add the @Data and @NoArgsConstructor annotations.
Add all of the attributes from your Cat model without the id field.
Done! Now, we have separate objects to return from our API and pass into our API that are separate from our database model object, and we have the ModelMapper to help us convert between the model and DTO objects. Now, let's hook up the database!
Hooking Up the Database
Now that our application can talk to the database and knows what a Cat is, let's set up the actual mechanisms to do the database work.
Create a new file CatRepository.java in the same folder as Cat.java.
In this file, create a new public interface named CatRepository that extends JpaRepository<Cat, long>. Also, add the annotation Repository.
Don't forget your imports! Use your IDE to help.
The JpaRepository interface is Spring's way of executing database queries. You might be wondering how Spring does this, since it's an interface instead of a class. Spring generates an implementation class of the repository interface at compile-time, based on how you define the interface. It also allows you to define your own implementation if you prefer, but this often isn't necessary. Let's move forward to see how Spring uses your interface definition to make things happen.
Add a new method signature to the interface like this:
List<Cat> findByName(String name);
Here's the cool, magic part. Spring Data JPA uses the name of the methods you define to figure out what kinds of queries to run. The find part means we want to do a SELECT. The By means we want to apply a filter (WHERE). It recognizes that Name is an attribute in our Cat class. So this method will select cats from the table where their name matches the input parameter.
Be careful with spelling and casing! If you spell attributes wrong or don't use uppercase and lowercase properly, Spring won't understand what you mean.
Create a new file, CatService.java. In this case, define a new public interface name CatService.
Why an interface? Mostly because it's a good habit. Later, you may want to create an alternative implementation for testing. By working with the interface rather than a class directly, you can avoid the need to go rewrite old code.
Create a new file, CatServiceImpl.java. The Impl is short for "implementation." We often use this to show that this is a class implementing a specific inteface.
In your CatServiceImpl.java file, create a new public class, CatServiceImpl that implements the CatService interface. Add the annotation @Service above the class definition.
This creates a place to store a CatRepository implementation object. This will allow use to use a CatRepository object to interact with cats in our database.
This also creates a place to store our ModelMapper for converting between our models and DTOs.
Add AllArgsConstructor to have Lombok create a constructor that takes in and sets the catRepository attribute, just like we've done before.
This is an example of constructor-based dependency injection. When Spring Boot runs your code, it will see this constructor, create a CatRepository instance, and give it to the constructor. This is part of Spring's "magic." This is called dependency injection because your CatServiceImpl class depends on CatRepository. Instead of providing the CatRepository directly, you just provide a way (the constructor) for Spring Boot to "inject" the dependency for you.
In your CatServiceImpl class, create methods to match the methods defined in the interface.
In the addCat method, call modelMapper.map() and pass the cat object as the first parameter and Cat.class as the second parameter. Store this in a variable, maybe catModel, and note that the output type will be Cat, since that's what we're converting to.
The first parameter is the object we want to convert. We want to save the cat data into the database, but only the Cat model class can do that.
The second parameter is the class we want to convert the object to. We can't just pass Cat. That will confuse Java. Java allows us to reference the class as an object by accessing the class property of the class.
Call the catRepository object's save() method, providing the catModel parameter as the input. Store the output in some variable.
"Hey Beege, where did this save() method come from?" It's part of the JpaRepository interface. These are standard methods that you'll learn over time. You can also lookup the JpaRepository interface in Spring's docs to get more info.
Why do we want to return the output? When we save a new cat to the database, PostgreSQL will create a new ID for that cat and provide it to Spring. It's helpful to people using our API to get the ID of the newly created cat. For ease, we just return the whole cat object, which will include the ID after we've saved it.
Call your modelMapper's map() function again. Pass in the result of the save() method as the first parameter and CatDetailDto.class as the second method. Return the output of the map() function, then you're done here!
The find method will be a little more complicated. First, create a variable to store our retrieved cats, maybe called retrievedCats.
Add a condition to check if name is null.
If name is null, call the catRepository object's findAll method, and store the output in retrievedCats.
If name isn't null, call the catRepository object's findByName method using the name parameter, and store it in retrievedCats.
We're going to do something a little complicated now using functional programming. Write out the following, then I'll explain it:
stream() tells Java that we're going to do functional programming on the list of objects. In programming, a "stream" if a series of data that is continuously processed. In this case, we'll continuously process each item in the list.
map() in functional programming means performing some function on each member in a set. In this case, we'll perform some function on each item in the list that we're streaming.
the var -> logic syntax is for defining an inline, anonymous function. It's "inline" because we're not using the standard, multiple-line way of writing functions, like you've been doing. It's anonymous because we never give a name to the function. Here, catModel is the parameter of the function. Everything after -> is the function body. Usually, this is only one statement, since an inline function should be very small.
The toList() method converts the stream back into a normal List. We need this for returning from the API.
In the findOne method, call the findById method on the catRepository object, and provide the id parameter as input. Store the output into a variable, maybe result.
Why? The findOne() method outputs an Optional<Cat>. The Optional means that there may or may not actually be a Cat object. In the next steps, you'll see how we handle that.
Check if result's .isPresent() returns true.
If isPresent() is true, call modelMapper.map(). Set the first parameter as the output of result.get() and the second parameter as CatDetailDto. Return the output of map().
If isPresent() is false, return null.
Nice! Now we have logic to actually retrieve and save cats from and to the database. But there's something not good here... Let's fix it.
Handling Missing Cats
Currently, your CatServiceImpl returns null when a cat is not found for a given ID. This isn't great. Instead, we should throw some kind of exception. Let's create an exception and incorporate it into our service layer.
In the org.yuhee.catsoneday.service package, create a new exception: CatNotFoundException. It should extend Exception.
Give it a constructor that takes in a Long as a parameter. This parameter will be the Cat's ID that is being requested.
In the constructor, call the parent class's constructor using super(), and pass in some descriptive text. Maybe something like "No cat found for ID " + catId.
Now, go back to your CatServiceImpl class. In your findOne() method, instead of returning null when isPresent() is false, throw a new CatNotFoundException with the ID.
Your IDE is probably complaining now. In the findOne() method signature, add throws CatNotFoundException. Make sure to also make this change on the CatService interface.
Building Our REST Endpoint to Test
Now that we have ways to get and save the data, let's connect this logic to our API to allow ther applications to work with us.
In your CatController.java file, add the below to your CatController class:
CatService catService;
Add the @AllArgsConstructor annotation to the CatController class to again set up constructor-based dependency injection.
Similarly to the CatRepository, Spring will intelligently find your CatServiceImpl and inject it into this variable for you. It's more Spring magic.
First, let's fix your getOne method. Change the @GetMapping annotation to look like this: @GetMapping("/{id}).
This tells Spring to not just use /cats (see the @RequestMapping on the CatController class). This tells Spring that if a request comes in to /cats/{id}, then call this method. The curly brackets {} around id tell Spring that this is a variable. This means that if an API user goes to /cats/5, this method will be executed, and Spring will recognize 5 as id.
Let's finish fixing the getOne method.
Change the output type from Cat to CatDetailDto.
Have it take in a @PathVariable long id as a parameter.
Note that the @PathVariable annotation is inline, not on its own separate line.
The @PathVariable annotation tells Spring that the id parameter here corresponds to some {id} in our mapping. This basically tells Spring to use the id from the @GetMapping annotation as the id parameter in this function.
Why is it called PathVariable? The /cats/{id} part is the path. The cats part of that path is fixed. The id part is variable and is meant to act as a method parameter.
Remove the existing method body.
Create a new CatDetailDto variable to store the cat we retrieve.
Call the catService object's findOne() method, passing in the id parameter as input, and store the output in your Cat variable.
Wrap this findOne() method call in a try ... catch block.
In your catch block, catch the CatNotFoundException.
In response to that exception, throw a new ResponseStatusExcepton, and pass HttpStatus.NOT_FOUND as the input.
Return the CatDetailDto object returned by the findOne method.
Create a createCat method. It should return a CatDetailDto object and take in a CreateCatDto object as a parameter as cat.
Add a @PostMapping annotation to the createCat method. Before your CreateCatDto cat parameter, add a @RequestBody annotation, similarly to the @PathVariable annotation in step 3.
What is a RequestBody? Remember learning about HTTP back in #7? The request body is the data passed to the server as part of the HTTP request. Spring will read data from the HTTP request, parse it, and provide it as input to our server. This is a lot of magic: parsing the text and creating our CreateCatDto object from that text.
In createCat, call the catService object's addCat method. As usual, pass in your cat parameter as input, and return
the method's output.
Now for the final method. Create a get method that outputs a List of Cat objects. It should take in a @RequestParam(required = false) String name as a parameter. It should also have a @GetMapping annotation.
What is this @RequestParam annotation? You learned about PathVariables and RequestBody. This is an optional part of requests that allows requesters to specify some options to modify the request. This is done by adding ?key=value&otherKey=otherValue to the end of the URL. If you start paying attention to websites you visit on the internet, you'll see this everywhere. In this case, if there's a parameter name=something, the name parameter would be set to "something".
The required = false parameter to the annotation tells Spring that this request parameter is not required. If we were missing this, callers would get an error if they tried to retrieve cats without providing a name.
In this get method, call the catService object's find method, passing the name. Return the output.
Note that we're not doing a null check here, even though the name parameter is not required. To me, this is part of the business logic, not part of parsing the API request, so I put it into the service method.
Notice that our new API endpoints show up now. Try the POST /cats endpoint. Click the Try it button, change the inputs a little, make sure to remove "id": 0,, and submit.
Try the GET /cats endpoint. Try executing it without any parameters. Try using the name to filter correctly and incorrectly.
Take note of the new cat's ID. Try the GET /cats/{id} endpoint with that ID, and make sure you get the correct cat back.
Well done!!! This is a very important milestone for you! You have a working API that persists data. You can create data, retrieve data, and even filter data. This is the core essence of what being a backend engineer is. Congratulations!
The remaining issues will help you write better APIs, learn to work with other systems, test your code, and actually deploy what you have. It's a more supplemental type of knowledge, but it still critical in your day-to-day work. For now, take joy in the fact that you've accomplished the fundamentals needed to be a backend engineer!
Goal
Let's save your cats to the database!
Context
In this issue, we'll use Spring Data JPA (Java Persistence API) to manipulate the database automatically. There's a lot of "magic" here, meaning Spring will handle many things automatically, like connecting to the database, forming SQL queries, and retrieving and parsing results. You'll still need to tell JPA what kinds of queries to run and the structure of the database, but we'll walk through that together.
Steps
Setting Up Spring Data JPA
build.gradle
at the root of your project, go to thedependencies
section, and add this bit:./gradlew
or follow available directions your editor gives you to have Gradle download the dependency.src/main/resources/application.properties
.application.properties
file is. Spring, and many Java systems, use.properties
files to define configurations for the applicaton.datasource
for Spring andjpa
configurations for Spring. Thespring.datasource.url
is what we call a "connection string." Usually, it's easiest to just search for the connection string for Spring and your database, in this case, PostgreSQL.localhost:5432
? If you look at yourcompose.yml
again, you'll see that the database is exposed on port 5432. This means that if you connect to port 5432 on your local computer, Docker will forward that connection to the database in the Docker container.driver-class-name
specifies what kind of database we're connecting to and where to find the code to make that connection. We could use MySQL, SQL Server, even Oracle. We addedorg.postgresql
as a dependency so that we could use it to connect to the PostgreSQL database. This configuration is how we tell Spring about all of that.spring.jpa.database-platform
value tells Spring what type of SQL to use. Remember that every database has slightly different SQL? This is how we tell Spring exactly what kind of SQL to use. Why couldn't Spring just use the driver to decide? Well, there could be more than one driver for a type of database. For example, you can use the MySQL driver to connect to a MariaDB server, but there may be differences between MySQL and MariaDB SQL. (I don't recall. It's been awhile.)generate-ddl
option tells Spring Data JPA that it should generate the table definitions. This is useful when we don't have a separate system to manage database structure for us. Often, teams will use a separate system, but for now, this is okay.spring.jpa.hibernate.ddl-auto
option tells Spring how it should handle the generated DDL automatically. There are case where you may want to generated the DDL for you to check but not automatically update your database. This allows you to control this behavior. If you're wondering whathibernate
is, it's the name of the main library that Spring Data JPA uses to manage database entities.Well done! Now, Spring has everything it needs to connect to your database.
Setting Up Our Cat Model
TODO: CatNotFoundException
Cat.java
and add the@Entity
annotation to theCat
class. Don't forget to add the import! You can either use your editor to help you do it automatically or manually addimport jakarta.persistence.Entity;
near your other imports.@Entity
annotation tells Spring Data JPA that ourCat
class should correspond to a database table.@Id
annotation. There's just one problem... We don't have an ID for our cats! So let's add the below to ourCat
class:jakarta.persistence.Id
Long
? The database supports larger numbers than a simpleint
can support. Thelong
data type is large enough to support the possible values stored in our database.Long
instead of the primitivelong
? TheLong
object allows fornull
values. The primitivelong
type initializes to0.0
, but we want to allow a propernull
.GENERATED BY DEFAULT
to tell PostgreSQL to automatically set the ID? We need to tell Spring that PostgreSQL should do this work. You can do this by adding the@GeneratedValue(strategy = GenerationType.AUTO)
annotation to your newid
attribute. Do that now. Don't forget to importGeneratedValue
andGenerationType
! I bet you can figure out the correct import statements by now.Cat
class, add the@NoArgsConstructor
to have Lombok generate a constructor that creates an emptyCat
. Spring Data JPA will need this, later. Importlombok.NoArgsConstructor
to make sure Java can find it.handleBreeding
method, look for when you create a new Cat. Make sure to add a parameter for theid
that we've added to theCat
class. Just add a new first parameter asnull
to thenew Cat()
method call.@Getter
and@Setter
lines. Instead, put a@Data
annotation on theCat
class. The@Data
annotation acts as both@Getter
and@Setter
. Spring Data JPA will need setters for all attributes in order to do its work. Don't forget to remove the imports for@Getter
and@Setter
and to add the import for@Data
.Well done! With this, Spring Data JPA knows about your cat objects and how they should be stored in the database.
Setting up Data Transfer Objects
There are cases where we'll want to not just display the raw data from the database, for example, providing user information but not user passwords. We call the data formats that go in and out of API endpoints as "DTOs," or "Data Transfer Objects." We also need to convert between the DTOs and our database model objects. We do this using a "model mapper." In this context, "mapping" is the act of connecting data fields from one object to another. There is a library to help us do model mapping, so we'll need to add that.
Lastly, we'll have input and output data for three different endpoints: a list of cats, one cat's details, and creating a new cat. Usually, the resources (cats) in a list have only summary information, whereas the resource details have all or most of the information. The information to create a new resource is usually all of the data except database-generated values, like IDs. So we'll create three DTOs - one for each.
build.gradle
, add the below:./gradlew
in your project directory.CatsonedayApplication
class, add the below method:dto
package under yourorg.yuhee.catsoneday
package.dto
package namedCatInListDto
.@Data
and@NoArgsConstructor
annotations to the class.id
,name
,sex
, andcolor
attributes from yourCat
model without the annotations.dto
package namedCatDetailDto
.@Data
and@NoArgsConstructor
annotations to the class.Cat
model without the annotations.dto
package namedCreateCatDto
.@Data
and@NoArgsConstructor
annotations.Cat
model without theid
field.Done! Now, we have separate objects to return from our API and pass into our API that are separate from our database model object, and we have the
ModelMapper
to help us convert between the model and DTO objects. Now, let's hook up the database!Hooking Up the Database
Now that our application can talk to the database and knows what a
Cat
is, let's set up the actual mechanisms to do the database work.CatRepository.java
in the same folder asCat.java
.public interface
namedCatRepository
that extendsJpaRepository<Cat, long>
. Also, add the annotationRepository
.JpaRepository
interface is Spring's way of executing database queries. You might be wondering how Spring does this, since it's an interface instead of a class. Spring generates an implementation class of the repository interface at compile-time, based on how you define the interface. It also allows you to define your own implementation if you prefer, but this often isn't necessary. Let's move forward to see how Spring uses your interface definition to make things happen.find
part means we want to do aSELECT
. TheBy
means we want to apply a filter (WHERE
). It recognizes thatName
is an attribute in ourCat
class. So this method will select cats from the table where their name matches the input parameter.CatService.java
. In this case, define a newpublic interface
nameCatService
.CatServiceImpl.java
. TheImpl
is short for "implementation." We often use this to show that this is a class implementing a specific inteface.CatServiceImpl.java
file, create a new public class,CatServiceImpl
thatimplements
theCatService
interface. Add the annotation@Service
above the class definition.CatRepository
implementation object. This will allow use to use aCatRepository
object to interact with cats in our database.ModelMapper
for converting between our models and DTOs.AllArgsConstructor
to have Lombok create a constructor that takes in and sets thecatRepository
attribute, just like we've done before.CatRepository
instance, and give it to the constructor. This is part of Spring's "magic." This is called dependency injection because yourCatServiceImpl
class depends onCatRepository
. Instead of providing theCatRepository
directly, you just provide a way (the constructor) for Spring Boot to "inject" the dependency for you.CatServiceImpl
class, create methods to match the methods defined in the interface.addCat
method, callmodelMapper.map()
and pass thecat
object as the first parameter andCat.class
as the second parameter. Store this in a variable, maybecatModel
, and note that the output type will beCat
, since that's what we're converting to.Cat
model class can do that.Cat
. That will confuse Java. Java allows us to reference the class as an object by accessing theclass
property of the class.catRepository
object'ssave()
method, providing thecatModel
parameter as the input. Store the output in some variable.save()
method come from?" It's part of theJpaRepository
interface. These are standard methods that you'll learn over time. You can also lookup theJpaRepository
interface in Spring's docs to get more info.modelMapper
'smap()
function again. Pass in the result of thesave()
method as the first parameter andCatDetailDto.class
as the second method. Return the output of themap()
function, then you're done here!find
method will be a little more complicated. First, create a variable to store our retrieved cats, maybe calledretrievedCats
.name
isnull
.name
isnull
, call thecatRepository
object'sfindAll
method, and store the output inretrievedCats
.name
isn'tnull
, call thecatRepository
object'sfindByName
method using thename
parameter, and store it inretrievedCats
.stream()
tells Java that we're going to do functional programming on the list of objects. In programming, a "stream" if a series of data that is continuously processed. In this case, we'll continuously process each item in the list.map()
in functional programming means performing some function on each member in a set. In this case, we'll perform some function on each item in the list that we're streaming.var -> logic
syntax is for defining an inline, anonymous function. It's "inline" because we're not using the standard, multiple-line way of writing functions, like you've been doing. It's anonymous because we never give a name to the function. Here,catModel
is the parameter of the function. Everything after->
is the function body. Usually, this is only one statement, since an inline function should be very small.toList()
method converts the stream back into a normalList
. We need this for returning from the API.findOne
method, call thefindById
method on thecatRepository
object, and provide theid
parameter as input. Store the output into a variable, mayberesult
.findOne()
method outputs anOptional<Cat>
. TheOptional
means that there may or may not actually be aCat
object. In the next steps, you'll see how we handle that.result
's.isPresent()
returns true.isPresent()
is true, callmodelMapper.map()
. Set the first parameter as the output ofresult.get()
and the second parameter asCatDetailDto
. Return the output ofmap()
.isPresent()
is false, returnnull
.Nice! Now we have logic to actually retrieve and save cats from and to the database. But there's something not good here... Let's fix it.
Handling Missing Cats
Currently, your
CatServiceImpl
returnsnull
when a cat is not found for a given ID. This isn't great. Instead, we should throw some kind of exception. Let's create an exception and incorporate it into our service layer.org.yuhee.catsoneday.service
package, create a new exception:CatNotFoundException
. It should extendException
.Long
as a parameter. This parameter will be theCat
's ID that is being requested.super()
, and pass in some descriptive text. Maybe something like"No cat found for ID " + catId
.CatServiceImpl
class. In yourfindOne()
method, instead of returningnull
whenisPresent()
is false, throw a newCatNotFoundException
with the ID.findOne()
method signature, addthrows CatNotFoundException
. Make sure to also make this change on theCatService
interface.Building Our REST Endpoint to Test
Now that we have ways to get and save the data, let's connect this logic to our API to allow ther applications to work with us.
CatController.java
file, add the below to yourCatController
class:@AllArgsConstructor
annotation to theCatController
class to again set up constructor-based dependency injection.CatRepository
, Spring will intelligently find yourCatServiceImpl
and inject it into this variable for you. It's more Spring magic.getOne
method. Change the@GetMapping
annotation to look like this:@GetMapping("/{id})
./cats
(see the@RequestMapping
on theCatController
class). This tells Spring that if a request comes in to/cats/{id}
, then call this method. The curly brackets{}
aroundid
tell Spring that this is a variable. This means that if an API user goes to/cats/5
, this method will be executed, and Spring will recognize5
asid
.getOne
method.Cat
toCatDetailDto
.@PathVariable long id
as a parameter.@PathVariable
annotation is inline, not on its own separate line.@PathVariable
annotation tells Spring that theid
parameter here corresponds to some{id}
in our mapping. This basically tells Spring to use theid
from the@GetMapping
annotation as theid
parameter in this function.PathVariable
? The/cats/{id}
part is the path. Thecats
part of that path is fixed. Theid
part is variable and is meant to act as a method parameter.CatDetailDto
variable to store the cat we retrieve.catService
object'sfindOne()
method, passing in theid
parameter as input, and store the output in yourCat
variable.findOne()
method call in atry ... catch
block.catch
block, catch theCatNotFoundException
.ResponseStatusExcepton
, and passHttpStatus.NOT_FOUND
as the input.CatDetailDto
object returned by thefindOne
method.createCat
method. It should return aCatDetailDto
object and take in aCreateCatDto
object as a parameter ascat
.@PostMapping
annotation to thecreateCat
method. Before yourCreateCatDto cat
parameter, add a@RequestBody
annotation, similarly to the@PathVariable
annotation in step 3.RequestBody
? Remember learning about HTTP back in #7? The request body is the data passed to the server as part of the HTTP request. Spring will read data from the HTTP request, parse it, and provide it as input to our server. This is a lot of magic: parsing the text and creating ourCreateCatDto
object from that text.createCat
, call thecatService
object'saddCat
method. As usual, pass in yourcat
parameter as input, and return the method's output.get
method that outputs aList
ofCat
objects. It should take in a@RequestParam(required = false) String name
as a parameter. It should also have a@GetMapping
annotation.@RequestParam
annotation? You learned aboutPathVariable
s andRequestBody
. This is an optional part of requests that allows requesters to specify some options to modify the request. This is done by adding?key=value&otherKey=otherValue
to the end of the URL. If you start paying attention to websites you visit on the internet, you'll see this everywhere. In this case, if there's a parametername=something
, thename
parameter would be set to"something"
.required = false
parameter to the annotation tells Spring that this request parameter is not required. If we were missing this, callers would get an error if they tried to retrieve cats without providing a name.get
method, call thecatService
object'sfind
method, passing thename
. Return the output.null
check here, even though thename
parameter is not required. To me, this is part of the business logic, not part of parsing the API request, so I put it into the service method../gradlew bootRun
to run your server and visit http://localhost:8080/swagger-ui/index.html to see your changes.POST /cats
endpoint. Click theTry it
button, change the inputs a little, make sure to remove"id": 0,
, and submit.GET /cats
endpoint. Try executing it without any parameters. Try using the name to filter correctly and incorrectly.GET /cats/{id}
endpoint with that ID, and make sure you get the correct cat back.Well done!!! This is a very important milestone for you! You have a working API that persists data. You can create data, retrieve data, and even filter data. This is the core essence of what being a backend engineer is. Congratulations!
The remaining issues will help you write better APIs, learn to work with other systems, test your code, and actually deploy what you have. It's a more supplemental type of knowledge, but it still critical in your day-to-day work. For now, take joy in the fact that you've accomplished the fundamentals needed to be a backend engineer!