Saturday, April 8, 2017

Implementing Object Persistence for Our Digital Cards with Amazon DynamoDB

In my previous post, I have written about load balancing our digital card store application and updating it with zero downtime.

The application was using an in-memory dummy Spring repository to have a quick start. But if we don't use a persistent repository, our cards will be lost when the EC2 instance is stopped. To provide persistence we can use Amazon DynamoDB.

Amazon DynamoDB is a NoSQL cloud database service. It provides a managed, scalable, schema-free data storage.

When we design scalable, high throughput applications, we should consider using NoSQL databases where they are appropriate.

DynamoDB Concepts

In DynamoDB, data is stored in the tables just like the tables in relational databases. A table holds the items which are similar to the rows in relational databases. Also the columns in relational databases resemble to the attributes in DynamoDB. Main difference between DynamoDB and relational databases is in the table columns. In relational databases, the schema of each table is fixed and pre-defined. In DynamoDB, only the primary key of the table is defined at creation time. This way, every item can have different number of attributes. We can think every item as a hash map with different keys and values. The picture below shows the structure of a DynamoDB table. For more information, see DynamoDB Getting Started Guide.



Every item is defined with its primary key. A primary key is consists of a hash key and an optional range key. Hash key is used to select the DynamoDB partition. Partitions are parts of the table data. Range keys are used to sort the items in the partition, if they exist.

When using DynamoDB, data modeling is very important. So we should pay attention to use cases of the application and the data model we create. For more information, see DynamoDB Best Practices.

Spring Data DynamoDB Module

Spring Data project has a module to provide automatic CRUD repository interface implementations that greatly reduces the effort required to use DynamoDB. In this post, I will use Spring Data DynamoDB module to easily access DynamoDB functionality. For more information, see Spring Data DynamoDB GitHub page.

The steps to add DynamoDB persistence to our card store application are below.

1. Configure DynamoDB
2. Create DynamoDB tables
3. Create Java entity classes
4. Create Spring Data repository interfaces
5. Implement the controllers

Let's start.

1. Configure DynamoDB

First, we add Maven dependencies to use AWS Java SDK for DynamoDB and Spring Data DynamoDB module as below.
             <dependency>
                    <groupId>com.amazonaws</groupId>
                    <artifactId>aws-java-sdk-dynamodb</artifactId>
                    <version>1.9.6</version>
             </dependency>
             <dependency>
                    <groupId>com.github.derjust</groupId>
                    <artifactId>spring-data-dynamodb</artifactId>
                    <version>4.5.0</version>
             </dependency>

Then we create a DynamoDBConfig class to configure DynamoDB. Amazon DynamoDB provides a local version to use at development time. While developing the application we can use the local DynamoDB endpoint. We can specify the value of amazon.dynamodb.endpoint variable in application.properties file. For more information, see Setting Up DynamoDB Local. To use real DynamoDB service use a blank value and set your region accordingly.

We specify our Java package of the repository interfaces with @EnableDynamoDBRepositories annotation.

@Configuration
@EnableDynamoDBRepositories(basePackages = "com.cardstore.dao")
public class DynamoDBConfig {

       @Value("${amazon.dynamodb.endpoint}")
       private String amazonDynamoDBEndpoint;

       @Bean
       public AmazonDynamoDB amazonDynamoDB() {
             AmazonDynamoDB amazonDynamoDB = new AmazonDynamoDBClient();

             if (amazonDynamoDBEndpoint != null && !amazonDynamoDBEndpoint.equals("")) {
                    amazonDynamoDB.setEndpoint(amazonDynamoDBEndpoint);
             } else
                    amazonDynamoDB.setRegion(Region.getRegion(Regions.EU_CENTRAL_1));
             return amazonDynamoDB;
       }
}

2. Create DynamoDB tables

To decide the structure of the tables, first we should consider the use cases and the requirements for our digital card store application.

In our digital card store application there must be users that own the cards. Let's list the requirements for users.

  1. Users should be able to register themselves with their user name, name, email address and password and system should assign an initial balance.
  2. Users should be able to login with their user name and password.
  3. Users should be able to logout.

The requirements for the cards are below.
  1. Users should be able to create a card with a name.
  2. Users should be able to list their cards.
  3. Users should be able to mark the cards as on sale with a price.
  4. Users should be able to list the cards that are on sale.
  5. Users should be able to buy a card.

With these requirements we can design the User and Card tables as below. imageURL attribute of Card table will be used in future posts.



After we decided to table structure, we can use AWS CLI to create the tables with the commands below. To use local DynamoDB, you can use endpoint-url parameter as --endpoint-url http://192.168.99.100:9000 .

aws dynamodb create-table --table-name User --attribute-definitions AttributeName=username,AttributeType=S --key-schema AttributeName=username,KeyType=HASH --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1

aws dynamodb create-table --table-name Card --attribute-definitions AttributeName=owner,AttributeType=S AttributeName=name,AttributeType=S --key-schema AttributeName=owner,KeyType=HASH AttributeName=name,KeyType=RANGE --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1


 3. Create Java entity classes

Once we created the tables, we can create Java entity classes User and Card. We use Spring Data DynamoDB module annotations to associate tables and keys to Java classes and their fields. @DynamoDBTable annotation shows the table name and @DynamoDBHashKey annotation shows the hash key of the table. If the Java field name and DynamoDB attribute name differs we can use @DynamoDBAttribute annotation for fields.

Spring Data DynamoDB module uses composite key classes if range key is used. @Id annotation is used to mark composite key field. In composiye key classes, fields are marked with @DynamoDBHashKey and @DynamoDBRangeKey annotations. Card class uses CardId composite key class as below.

@DynamoDBTable(tableName = "User")
public class User {
    @DynamoDBHashKey
    private String username;

    private String name;
    private String password;
    private String email;
    private double balance;


@DynamoDBTable(tableName = "Card")
public class Card implements Cloneable {
       @Id
       private CardId cardId;
      
       private String dateLoaded;
       private String imageUrl;
       private boolean inSale;
       private double price;

public class CardId implements Serializable {
       @DynamoDBHashKey
       private String owner;
      
       @DynamoDBRangeKey
       private String name;


4. Create Spring Data repository interfaces

After entity classes are defined we can create Spring Data repository interfaces to access DynamoDB tables. Spring Data module creates the implementation in run-time.

@EnableScan
public interface UserRepository extends CrudRepository<User, String> {
}

UserRepository interface extends Spring Data CrudReposity interface which provides basic CRUD methods.

@EnableScan
public interface CardRepository extends CrudRepository<Card, CardId> {
       List<Card> findByOwner(String owner);
      
       List<Card> findByInSale(@Param("inSale") boolean inSale);
}

CardRepository interface extends Spring Data CrudReposity interface for basic CRUD methods and adds two custom methods. findByOwner method will be used for finding the cards by their owner and findByInSale method will be used to find cards by inSale field. Spring Data module generates implementations of these methods by using method name convention for generating filter expressions.


5. Implement the controllers

Now both entity classes and repository clases are ready. We can implement our controller classes that manages users and cards.

UserController class includes user and session management methods. This controller class uses UserRepository class to manage users in DynamoDB.

@Controller
public class UserController {

       public static final String USER_KEY_FOR_SESSION = "USER";

       @Autowired
       UserRepository userRepository;

home method is used to direct user to login page if the user is not logged in. If the user is logged in the dashboard is shown.

@RequestMapping("/")
public String home(Map<String, Object> model, HttpSession session) {

       User user = userfromSession(session);

       if (user == null)
             return "index";
       else {
             // get up to date balance information from table, because balance
             // can be changed any time.
             User uptoDate = userRepository.findOne(user.getUsername());

             user.setBalance(uptoDate.getBalance());

             model.put("user", user);
             return "dashboard";
       }
}
registerUser method is used to register users. The initial balance is set to 100.

@RequestMapping(value = "/users", method = RequestMethod.POST)
@ResponseBody
public boolean registerUser(@RequestBody User user) {

       User previous = userRepository.findOne(user.getUsername());

       if (previous == null) {
             user.setBalance(100);
             userRepository.save(user);
       }

       return previous == null;
}


login method is used to log the users in by checking user's password and creating the http session.

@RequestMapping(value = "/login", method = RequestMethod.POST, produces = "text/plain")
@ResponseBody
public String login(@RequestBody User user, HttpServletRequest request) {
       String error = "None";
       User existing = userRepository.findOne(user.getUsername());

       boolean canLogin = existing != null && existing.getPassword().equals(user.getPassword());

       if (!canLogin)
             error = "User name and password mismatch.";
       else {
             HttpSession session = request.getSession(true);

             session.setAttribute(USER_KEY_FOR_SESSION, existing);
       }
       return error;
}

logout method is used to log the users out by invalidating the current http session.

@RequestMapping(value = "/logout", method = RequestMethod.POST)
@ResponseBody
public boolean logout(HttpServletRequest request) {
       HttpSession session = request.getSession(false);

       if (session != null)
             session.invalidate();
       return true;
}

userFromSession and loggedIn methods are used for session management.

public static User userfromSession(HttpSession session) {
       User user = (User) session.getAttribute(USER_KEY_FOR_SESSION);

       return user;
}

public static boolean loggedIn(HttpSession session) {
       return userfromSession(session) != null;
}

After UserController class, we can create CardController class to manage cards. This class uses both UserRepository and CardRepository classes to access both users and cards in DynamoDB.


@RestController
public class CardController {

       @Autowired
       CardRepository cardRepository;
      
       @Autowired
       UserRepository userRepository;

listCards method is used to list user's cards and list on sale cards.

@RequestMapping(value = "/cards", method = RequestMethod.GET)
public List<Card> listCards(@RequestParam(name = "inSale", required = false) boolean inSale, HttpSession session) {

       User user = UserController.userfromSession(session);

       if (inSale)
             return (List<Card>) cardRepository.findByInSale(inSale);
       else
             return (List<Card>) cardRepository.findByOwner(user.getUsername());
}

saveCard method is used to create a new cardc with a name.

@RequestMapping(value = "/cards", method = RequestMethod.POST)
public Card saveCard(@RequestBody Card card, HttpSession session) {

       User user = UserController.userfromSession(session);

       card.setOwner(user.getUsername());
       card.setDateLoaded(new SimpleDateFormat("yyyy-MM-dd").format(new Date()));

       cardRepository.save(card);

       return card;
}

getCard method is used to get details of a specific card.

// we used {name:.+} instead of {name} to get . in path like
// '/cards/ceyhun.ozgun'
@RequestMapping(value = "/cards/{name:.+}", method = RequestMethod.GET)
public Card getCard(@PathVariable("name") String name, HttpSession session) {
       User user = UserController.userfromSession(session);
       Card card = cardRepository.findOne(new CardId(user.getUsername(), name));

       return card;
}


sellCard method is used to put a specific card on sale with a price.

@RequestMapping(value = "/sell", method = RequestMethod.POST)
public boolean sellCard(@RequestBody Card card, HttpSession session) {
       User user = UserController.userfromSession(session);
       card.setOwner(user.getUsername());

       Card existing = cardRepository.findOne(new CardId(card.getOwner(), card.getName()));

       if (existing != null && !existing.isInSale()) {
             existing.setInSale(true);
             existing.setPrice(card.getPrice());

             cardRepository.save(existing);

             return true;
       }
       return false;
}

buyCard method is used to buy a card on sale. As the owner of the card changed, we must first delete the old card and save the card again with the new owner. Also we decrease the balance of the buyer and increase the balance of the seller.

@RequestMapping(value = "/buy", method = RequestMethod.POST)
public BuyResult buyCard(@RequestBody Card card, HttpSession session) {
       User user = UserController.userfromSession(session);

       Card cardToBuy = cardRepository.findOne(new CardId(card.getOwner(), card.getName()));

       if (cardToBuy == null || !cardToBuy.isInSale())
             return new BuyResult("Can't buy a non existing or not buyable card.");

       if (cardToBuy.getOwner().equals(user.getUsername()))
             return new BuyResult("Can't buy your own card.");

       // get latest balance from table, do not use the balance in the session
       User currentUser = userRepository.findOne(user.getUsername());
       if (currentUser.getBalance() < cardToBuy.getPrice())
             return new BuyResult("Can't buy a card with price " + cardToBuy.getPrice() + " with a balance " + currentUser.getBalance());

       Card newCard;
       try {
             newCard = (Card) cardToBuy.clone();
       } catch (CloneNotSupportedException e) {
             throw new RuntimeException("not expected");
       }

       newCard.setOwner(currentUser.getUsername());
       newCard.setInSale(false);

       // owner field is the hash key of the Card table, so we must delete the
       // old card and save the new card
       cardRepository.delete(cardToBuy);

       cardRepository.save(newCard);

       // transfer the price of the card from buyer to seller
       double newBalance = currentUser.getBalance() - cardToBuy.getPrice();

       currentUser.setBalance(newBalance);
       userRepository.save(currentUser);

       User seller = userRepository.findOne(cardToBuy.getOwner());

       seller.setBalance(seller.getBalance() + cardToBuy.getPrice());
       userRepository.save(seller);

       return new BuyResult(newBalance);
}

Now we are developed the server side of the application. To complete the application we should develop the web interfaces also. These interfaces are Login, Register User forms and the dashboard page. You can develop these interfaces according to the controller paths and entity attributes. The code can be found at GitHub.

Deploying the application on AWS

Before deploying the application to AWS, we must prepare IAM role the EC2 instances use. In my previous posts EC2 instances used an IAM role that have only S3 access policy. For this post we should give DynamoDB access to the IAM role we use. We can use AWS Console to create an IAM role named CardStoreRole that have AmazonS3FullAccess and AmazonDynamoDBFullAccess policies.

You can create a load balancer and auto scaling group to launch the application using the instructions on my previous posts.

Once the application launched you can use the application to register two different users. Then you can create one card with first user and buy the card with second user as shown in the pictures below.






Summary

In this post, I have shown how to use Amazon DynamoDB to provide object persistence in a Spring Boot application. The code can be found at my GitHub repository.

In my next post, I will show how to use Amazon Simple Queue Service and Amazon Simple Email Service to provide email activation for user registration.



38 comments:

  1. Nice post. You pointed on very important facts by this post. This is really very informative and useful information. Thanks for sharing this post.apply aws jobs in hyderabad.

    ReplyDelete




  2. It is really a great work and the way in which you are sharing the knowledge is excellent.Amazon Web service Training in Velachery

    ReplyDelete
  3. Super blog, very easy to understand. Thanks for providing such a nice information. Very useful to the users, for more updates on AWS AWS Online Training

    ReplyDelete
  4. Hi,

    Thank you so much for taking effort to share such a useful information. I will definitely share your articles to my online portal DynamoDB Development blog.

    Ecommerce Website Development
    Aapthi Technologies
    Web Development Company Dubai

    ReplyDelete
  5. Hi, when I am trying to findByInSale I get this error:

    java.lang.NullPointerException
    at org.socialsignin.spring.data.dynamodb.repository.support.DynamoDBEntityMetadataSupport.getPropertyNameForAccessorMethod(DynamoDBEntityMetadataSupport.java:316)
    at org.socialsignin.spring.data.dynamodb.repository.support.DynamoDBHashAndRangeKeyExtractingEntityMetadataImpl.getRangeKeyPropertyName(DynamoDBHashAndRangeKeyExtractingEntityMetadataImpl.java:78)
    at org.socialsignin.spring.data.dynamodb.repository.support.DynamoDBIdIsHashAndRangeKeyEntityInformationImpl.getRangeKeyPropertyName(DynamoDBIdIsHashAndRangeKeyEntityInformationImpl.java:81)
    at org.socialsignin.spring.data.dynamodb.repository.query.DynamoDBEntityWithHashAndRangeKeyCriteria.(DynamoDBEntityWithHashAndRangeKeyCriteria.java:74)
    at org.socialsignin.spring.data.dynamodb.repository.query.AbstractDynamoDBQueryCreator.create(AbstractDynamoDBQueryCreator.java:64)
    at org.socialsignin.spring.data.dynamodb.repository.query.AbstractDynamoDBQueryCreator.create(AbstractDynamoDBQueryCreator.java:41)
    at org.springframework.data.repository.query.parser.AbstractQueryCreator.createCriteria(AbstractQueryCreator.java:119)
    at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:95)
    at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:81)
    at org.socialsignin.spring.data.dynamodb.repository.query.PartTreeDynamoDBQuery.doCreateQuery(PartTreeDynamoDBQuery.java:58)
    at org.socialsignin.spring.data.dynamodb.repository.query.AbstractDynamoDBQuery.doCreateQueryWithPermissions(AbstractDynamoDBQuery.java:79)
    at org.socialsignin.spring.data.dynamodb.repository.query.AbstractDynamoDBQuery$CollectionExecution.execute(AbstractDynamoDBQuery.java:101)
    at org.socialsignin.spring.data.dynamodb.repository.query.AbstractDynamoDBQuery.execute(AbstractDynamoDBQuery.java:303)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:590)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:578)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:59)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
    at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
    at com.sun.proxy.$Proxy86.findByInSale(Unknown Source)

    ReplyDelete
  6. Thanks for proving such a wonderful content.
    Thank you so much for the post it is very helpful, Keep posting such type of articles.

    Fashion bloggers in India

    ReplyDelete
  7. Those guidelines additionally worked to become a good way to recognize that other people online have identical fervor like mine to grasp a great deal more around this condition. and I could assume you are an expert on this subject. Same as your blog i found another one Amazon Master Class .Actually I was looking for the same information on internet for Amazon Master Class and came across your blog. I am impressed by the information that you have on this blog. Thanks a million and please keep up the gratifying work.

    ReplyDelete
  8. Such an interesting article here.I was searching for something like that for quite a long time and at last I have found it here. Quicksilver Leather Jacket

    ReplyDelete
  9. Hi , Thank you so much for writing such an informational blog. If you are Searching for latest Jackets, Coats and Vests, for more info click on given link-Puscifer Leather Jacket

    ReplyDelete
  10. thank you for this informative article.<a href="https: https://www.credosystemz.com/courses/azure-training/ Azure Training in Chennai</a>

    ReplyDelete
  11. Thank you for this interesting article<a href="https: https://www.credosystemz.com/courses/azure-training/ Azure Training in Chennai</a>

    ReplyDelete
  12. Thanks for this. This is the simplest explanation I can understand given the tons of Explanation here. Chris Redfield Coat

    ReplyDelete
  13. Our share market trading focus on investing and fundamentals, Choose us for the best share market training classes in Chennai. Enroll for the
    best Courses in Chennai

    ReplyDelete
  14. Nice post. Thank you to provide us this useful information. Tom Cruise The Mummy Jacket

    ReplyDelete
  15. Fantastic blog i have never ever read this type of amazing information. sabrina red coat

    ReplyDelete
  16. This comment has been removed by the author.

    ReplyDelete
  17. pure Crack Cocaine Online 98%

    Cocaine Online Vendor, Best Cocaine Online Vendor, Fishscale cocaine online shop, where to buy Fishscale cocaine, blow drug, Bolivian Cocaine Canada, Bolivian Cocaine for sale, Bolivian Cocaine Online, Buy Peruvian Pink Cocaine, cocaina no flour, cocaine for sale, How can I buy Peruvian Cocaine, How to buy Peruvian Cocaine, Order peruvian cocaine, order pure cocaine online, Peruvian Cocaine buy, Peruvian Cocaine buy online, Peruvian cocaine for sale, Peruvian flake, peruvian pink cocaine, pink cocaine, Pink Cocaine for sale online, pink peruvian coke, powder cocaine, Powder Cocaine for sale online, Purchase Powder Cocaine Online, Pure Bolivian Cocaine Online, strawberry cocaine, Where can I buy Peruvian Cocaine, Where to buy Peruvian Cocaine, Where to Buy Peruvian Pink Cocaine online, Where to buy real Peruvian Pink Cocaine Online

    Wholesale Cocaine Online Vendor
    Wholesale Bolivian Cocaine Online Vendor
    Wholesale Uncut Cocaine Online Vendor
    Wholesale Colombian Cocaine Online Vendor
    Wholesale Black, Brown & china Heroin Online Vendor
    Wholesale Kilocaine Powder Online Vendor
    Wholesale Peruvian Cocaine Online Vendor
    Wholesale Volkswagen Cocaine Online Vendor
    whatsApp number : +15024936152
    wickr:movecokee

    ReplyDelete
  18. travisscottshop Its very interesting blog and well explained.

    ReplyDelete
  19. Great website you have got here. https://dreammerchofficial.com/
    Keep up the good work and thanks for sharing your blog site it really help a lot.

    ReplyDelete
  20. Thank you for this amazing blog.The Article which you have shared is very informative..인천출장마사지
    세종출장마사지
    서귀포출장마사지
    제주출장마사지
    김포출장마사지
    안양출장마사지
    안성출장마사지
    Imperial Money is a dedicated company that provides personalized services for wealth creation. It is an all-around choice to go for to induce your monetary assets at ease with multiple innovative prospects that add more value to your profile. The services and ideas include innovative products,

    ReplyDelete
  21. It’s clearly an exceptional and very useful fact. Thanks for sharing this helpful data with us. CEA Aviation is one of the DGCA Ground Classes in delhi . It gives admission to all of the aspirants seeking to start with Ground classes. You can enrol for Best Ground training for DGCA checks with us. Once you're clear with DGCA exam Counsellor Desk will assist you in choosing the best flight academy overseas.

    ReplyDelete
  22. Charm Windows offers a large group of answers for upvc windows manufacturers gurugram which incorporate - Commotion dropping windows, Robber safe windows, sun based control windows, Security glass windows and modified benefit windows. Anything that the plan you have at the top of the priority list, anything that your practical need, Joy Windows has a superior exhibition answer for you.

    ReplyDelete
  23. This comment has been removed by the author.

    ReplyDelete
  24. Thanks for sharing a knowledgeable blog that helps us a lot.
    pathology lab in Gurgaon

    ReplyDelete
  25. This comment has been removed by the author.

    ReplyDelete