YUHEE1984 / CatsOneday

1 stars 0 forks source link

10.5 - Save the Cats #17

Closed TheBeege closed 6 days ago

TheBeege commented 2 months ago

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

  1. 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:
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.postgresql:postgresql'
  2. Either run ./gradlew or follow available directions your editor gives you to have Gradle download the dependency.
  3. Now, we need to tell Spring Data JPA how to connect to our database. Open this file: src/main/resources/application.properties.
  4. Add the below lines to set up the proper configurations:
    spring.datasource.url = jdbc:postgresql://localhost:5432/catsoneday
    spring.datasource.username = postgres
    spring.datasource.password = postgres
    spring.datasource.driver-class-name = org.postgresql.Driver
    spring.jpa.generate-ddl = true
    spring.jpa.hibernate.ddl-auto = update
    • 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.

  1. In your build.gradle, add the below:
    implementation 'org.modelmapper:modelmapper:3.2.1'
  2. Reload your Gradle configuration to download the new dependency. You can use your IDE or just run ./gradlew in your project directory.
  3. In your CatsonedayApplication class, add the below method:
    @Bean
    public ModelMapper getModelMapper() {
        return new ModelMapper();
    }
  4. Create a new dto package under your org.yuhee.catsoneday package.
  5. Create a new class in your dto package named CatInListDto.
  6. Add the @Data and @NoArgsConstructor annotations to the class.
  7. Add the id, name, sex, and color attributes from your Cat model without the annotations.
  8. Create a new class in your dto package named CatDetailDto.
  9. Add the @Data and @NoArgsConstructor annotations to the class.
  10. Add all of the attributes from your Cat model without the annotations.
  11. Create a new class in your dto package named CreateCatDto.
  12. As usual, add the @Data and @NoArgsConstructor annotations.
  13. 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.

  1. Create a new file CatRepository.java in the same folder as Cat.java.
  2. 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.
  3. 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.
  4. Create a new file, CatService.java. In this case, define a new public interface name CatService.
  5. Add three methods to this interface:
    CatDetailDto addCat(CreateCatDto cat);
    List<CatInListDto> find(String name);
    CatDetailDto findOne(long id);
    • 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.
  6. 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.
  7. In your CatServiceImpl.java file, create a new public class, CatServiceImpl that implements the CatService interface. Add the annotation @Service above the class definition.
  8. In this new class, create two new attributes:
    CatRepository catRepository;
    ModelMapper modelMapper;
    • 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.
  9. 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.
  10. In your CatServiceImpl class, create methods to match the methods defined in the interface.
  11. 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.
  12. 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.
  13. 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!
  14. The find method will be a little more complicated. First, create a variable to store our retrieved cats, maybe called retrievedCats.
  15. Add a condition to check if name is null.
  16. If name is null, call the catRepository object's findAll method, and store the output in retrievedCats.
  17. If name isn't null, call the catRepository object's findByName method using the name parameter, and store it in retrievedCats.
  18. We're going to do something a little complicated now using functional programming. Write out the following, then I'll explain it:
    return retrievedCats.stream().map(catModel -> modelMapper.map(catModel, CatInListDto.class)).toList();
    • 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.
  19. 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.
  20. Check if result's .isPresent() returns true.
  21. 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().
  22. 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.

  1. In the org.yuhee.catsoneday.service package, create a new exception: CatNotFoundException. It should extend Exception.
  2. Give it a constructor that takes in a Long as a parameter. This parameter will be the Cat's ID that is being requested.
  3. 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.
  4. 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.
  5. 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.

  1. In your CatController.java file, add the below to your CatController class:
    CatService catService;
  2. 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.
  3. 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.
  4. Let's finish fixing the getOne method.
    1. Change the output type from Cat to CatDetailDto.
    2. 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.
    3. Remove the existing method body.
    4. Create a new CatDetailDto variable to store the cat we retrieve.
    5. Call the catService object's findOne() method, passing in the id parameter as input, and store the output in your Cat variable.
    6. Wrap this findOne() method call in a try ... catch block.
    7. In your catch block, catch the CatNotFoundException.
    8. In response to that exception, throw a new ResponseStatusExcepton, and pass HttpStatus.NOT_FOUND as the input.
    9. Return the CatDetailDto object returned by the findOne method.
  5. Create a createCat method. It should return a CatDetailDto object and take in a CreateCatDto object as a parameter as cat.
  6. 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.
  7. In createCat, call the catService object's addCat method. As usual, pass in your cat parameter as input, and return the method's output.
  8. 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.
  9. 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.
  10. Let's try it! In terminal, use ./gradlew bootRun to run your server and visit http://localhost:8080/swagger-ui/index.html to see your changes.
  11. 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.
  12. Try the GET /cats endpoint. Try executing it without any parameters. Try using the name to filter correctly and incorrectly.
  13. 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!

YUHEE1984 commented 6 days ago

I'll revew it after this all cording