In the last set of lecture notes I introduced Hibernate and we saw how to construct a set of Entity classes to model objects stored in the auction database. I also showed example code that demonstrated how to use an EntityManager to move objects into and out of the database.
In this lecture we are going to use the Spring Data framework to simplify access to the database. We will still be working with Entity classes, but we will no longer be working with EntityManager objects.
In the last lecture I introduced Hibernate Entity classes, which are designed to work with an EntityManager. The first version of these classes I showed was pretty complex, because I set myself the goal of building Entity classes on top of an existing database.
Fortunately for us, there is a simpler way to use Entity classes. If you don't have to build on top of an existing database you can have Hibernate build the database for you. The advantage of doing things this way is that the Entity classes end up being much simpler. For example, here is the new version of the Auction entity class:
@Entity
public class Auction {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID auctionid;
@ManyToOne
private User seller;
private String item;
private String description;
private String imageurl;
private int reserve;
private LocalDate opens;
private LocalDate closes;
private boolean completed;
@OneToMany(mappedBy="auction")
List<Tag> tags;
@OneToMany(mappedBy="auction")
List<Bid> bids;
public Auction() {}
// Getters and setters left out
}
We still need to go through the exercise of setting up relationships between entity classes, but everything else here is pretty simple to set up.
If you want to operate Hibernate in the mode in which it generates the database tables for you automatically there is one small additional step you will need to take. In your application.properties file you need to add the line
spring.jpa.hibernate.ddl-auto = create
to explicitly tell Hibernate to make the database tables for you automatically. Before you run your project you will need to use MySQL to create new empty schema for the database you are working with. The first time you run your application Hibernate will now generate all of the tables in your database for you.
Important warning: This setting is only appropriate to use the first time you run your application. Once you start putting data in the database you should switch this setting from create to update. The create setting destroys your existing database, including any data in the database, at application start-up time, while update only updates the database structure if you make a change to one of your entity classes, such as adding a new member variable.
One last small thing you may need to contend with has to do with storing UUIDs in a database. By default Hibernate chooses the most efficient datatype for storing UUIDs, the BINARY(16) MySQL datatype. This is a binary formal, which means that when you look at these fields in the database you will see a BLOB box which indicates that the data is binary and is not displyed in the workbench. If you need to convert those BLOBs to text for any purpose in the workbench you can run this query in the workbench:
SELECT BIN_TO_UUID(auctionid) FROM auction;
The BIN_TO_UUID() function can convert these UUIDs from binary to text form for you.
Spring Data is a framework designed to make it easier to work with a variety of different types of data stores, including relational databases.
Data access in Spring Data is managed through the use of Spring Data repository objects. In a project where we are working with a relational database Spring Data will expect us to create a repository for every table in our database.
Here is a very simple example. One of the tables in our auction database is the bids table, where we will be storing Bid objects. Here is the code for the Spring Data repository we will be using to access this table:
public interface BidRepository extends JpaRepository<Bid,Integer> {
}
Here are some things to note about this code:
The textbook discussed the JpaRepository interface in chapter 7. You can also find documentation on this interface online.
The JpaRepository interface contains many useful methods, but in some cases we may need to supplement that interface with a few custom methods. Here is a simple example: this is the repository we will use to interact with the users table:
public interface UserRepository extends JpaRepository<User,UUID>{
List<User> findByName(String name);
}
This example shows an example of a custom find method. When Spring Data generates a class to go with this interface it will construct the code for this method automatically based on just the name we provided. This example will search the table of users to find all of the users with a particular name.
Spring Data can generate a wide range of custom methods for you automatically based solely on the name of the method. You can read more about Spring Data's automatic code generation in chapter seven.
Finally, even though the built-in methods in JpaRepository and the automatic method generation technique will allow us to handle almost everything we need to do, there will be a few unusual cases where we will need to construct custom code. To make this possible Spring Data also allows us to construct custom queries.
Here is an example. During the purchasing process we will need to run a number of custom queries to locate Purchase records that are at a particular step in the purchase process. Here is the repository that deals with Purchase objects:
public interface PurchaseRepository extends JpaRepository<Purchase,UUID>{
@Query("select p from Purchase p where p.status='Won_bid' and p.bid.bidder.userid=:user")
List<Purchase> findOffers(UUID user);
@Query("select p from Purchase p where p.status='Confirmed' and p.bid.auction.seller.userid=:user")
List<Purchase> findSales(UUID user);
@Query("select p from Purchase p where p.status='Charged' and p.bid.bidder.userid=:user")
List<Purchase> findBills(UUID user);
@Query("select p from Purchase p where p.status='Paid' and p.bid.auction.seller.userid=:user")
List<Purchase> findSoldBills(UUID user);
@Query("select p from Purchase p where p.status='Charged' and p.bid.bidder.userid=:user")
List<Purchase> findShippedBills(UUID user);
}
In each of these cases I have annotated a custom find method with a @Query annotation that contains some JPQL code. When Spring Boot generates the class to go with this interface it will automatically generate code for each of these methods, and that code will execute the JPQL query we have provided.
You can also see in this example that each of the queries features placeholders. To ensure that those placeholders get filled in correctly when the query runs, we set up method parameters that the method will use to supply values for the placeholders. Most importantly, the parameter name matches the name of the JPQL placeholder we want to match it with.
These repositories will now provide us will all of the access to the database we will need. This also means that we will no longer be constructing DAO classes to manage database interactions.
One thing that our former DAO classes did for us that the new repository classes can not do was to manage the application logic for our application. For example, when a user wants to post a new User object to the system we had to check to make sure that there was not already a User in the system with the same user name.
To give this application logic a home, we are going to be supplementing the repository classes with service classes. The service classes will talk to the repositories, and the controllers in turn will only communicate with the service classes. The new version of the project has UserService, AuctionService, and PurchaseService classes. The controller classes have all been rewritten to work with the new service classes.
Here is a sample of code from one of the new service classes. Here is the method to insert a new User into the system.
@Service
public class UserService {
@Autowired
PasswordService passwordService;
@Autowired
UserRepository userRepository;
public String save(UserDTO user) {
List<User> existing = userRepository.findByName(user.getName());
if(existing.size() > 0)
return "Duplicate";
User newUser = new User();
newUser.setName(user.getName());
String hash = passwordService.hashPassword(user.getPassword());
newUser.setPassword(hash);
userRepository.save(newUser);
return newUser.getUserid().toString();
}
}