Enterprise Java

MongoDB: Add A Counter With Spring Data

In my blog app, you can view any user’s profile, for example, my profile page will be http://www.jiwhiz.com/profile/user1, and ‘user1’ is my user Id in the system. In MongoDB, every document object will have a unique identifier, and often we store it as String, so I have a BaseEntity class for that:
 
 
 
 
 
 
 

@Document
@SuppressWarnings('serial')
public abstract class BaseEntity implements Serializable {
    @Id
    private String id;
…
}

But the system generated id usually is very long, and I want to generate my own userId in my UserAccount class:

@Document(collection = 'UserAccount')
public class UserAccount extends BaseEntity implements SocialUserDetails {
    @Indexed
    private String userId;

    private UserRoleType[] roles;

    private String email;

    private String displayName;

    private String imageUrl;

    private String webSite;
...
}

The generated userId is very simple, just ‘user’ with a sequence number, for example, I’m the first user, so my userId is ‘User1’, and the next signed up user will be ‘User2’, etc. I want a sequence number generator from MongoDB to give me unique sequence numbers. The operations are to return current sequence number and also increase the sequence number in the database. In MongoDB, command findAndModify automatically modifies and returns a single document. So we can use this command to query the sequence number and increase it by $inc function.

First we create a Counter class to store sequence numbers for different purposes, like userId:

@SuppressWarnings('serial')
@Document(collection = 'Counter')
public class Counter extends BaseEntity{

    private String name;

    private long sequence;

...
}

Since we will use counter in a special way, there is no need to have a repository. I just create a CounterService with the method to return the next user id:

public interface CounterService {
    long getNextUserIdSequence();
}

The implementation will use findAndModify to get next sequence:

public class CounterServiceImpl implements CounterService {
    public static final String USER_ID_SEQUENCE_NAME = 'user_id';

    private final MongoTemplate mongoTemplate;

    @Inject
    public CounterServiceImpl(MongoTemplate mongoTemplate){
        this.mongoTemplate = mongoTemplate;
    }

    @Override
    public long getNextUserIdSequence() {
        return increaseCounter(USER_ID_SEQUENCE_NAME);
    }

    private long increaseCounter(String counterName){
        Query query = new Query(Criteria.where('name').is(counterName));
        Update update = new Update().inc('sequence', 1);
        Counter counter = mongoTemplate.findAndModify(query, update, Counter.class); // return old Counter object
        return counter.getSequence();
    }
}

Using this approach, you can add as many sequence as you want, just create a name for it. For example, you can record visits to your web site, so add a method like logVisit(), which calls the private method increaseCounter() with a name like ‘visit_num’. In this example, we don’t use Spring Data Repository for Counter document, but instead use MongoTemplate directly. From my MongoConfig class, which extends AbstractMongoConfiguration, which exposes MongoTemplate bean, we can easily inject MongoTemplate into other config bean, like CounterService:

@Configuration
class MainAppConfig {
...
    @Bean
    public CounterService counterService(MongoTemplate mongoTemplate) {
        return new CounterServiceImpl(mongoTemplate);
    }
...
}

Before you start running your app in any environment, you have to set up a Counter document first. Just type the following script in MongoDB shell:

db.Counter.insert({ 'name' : 'user_id', sequence : 1})

OK, those are the steps to prepare a user id sequence generator. But how can we use it when we want to add a new user to our system? It becomes very easy now. We will have an AccountService, which has createUserAccount method, to create a new UserAccount when the user sign in for the first time.

public interface AccountService extends SocialUserDetailsService, UserDetailsService, UserIdExtractor {
    UserAccount findByUserId(String userId);

    List<UserAccount> getAllUsers();

    List<UserSocialConnection> getConnectionsByUserId(String userId);

    UserAccount createUserAccount(ConnectionData data);
}

In our implementation class AccountServiceImpl, we can use CounterService, see highlighted code below:

public class AccountServiceImpl implements AccountService {
    private final UserAccountRepository accountRepository;
    private final UserSocialConnectionRepository userSocialConnectionRepository;
    private final CounterService counterService;

    @Inject
    public AccountServiceImpl(UserAccountRepository accountRepository, UserSocialConnectionRepository userSocialConnectionRepository, CounterService counterService) {
        this.accountRepository = accountRepository;
        this.userSocialConnectionRepository = userSocialConnectionRepository;
        this.counterService = counterService;
    }

    @Override
    public UserAccount findByUserId(String userId) {
        return accountRepository.findByUserId(userId);
    }

    @Override
    public List<UserAccount> getAllUsers() {
        return accountRepository.findAll();
    }

    @Override
    public List<UserSocialConnection> getConnectionsByUserId(String userId){
        return this.userSocialConnectionRepository.findByUserId(userId);
    }

    @Override
    public UserAccount createUserAccount(ConnectionData data) {
        UserAccount account = new UserAccount();
        account.setUserId('user' + this.counterService.getNextUserIdSequence());
        account.setDisplayName(data.getDisplayName());
        account.setImageUrl(data.getImageUrl());
        account.setRoles(new UserRoleType[] { UserRoleType.ROLE_USER });
        this.accountRepository.save(account);
        return account;
    }

    @Override
    public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException, DataAccessException {
        UserAccount account = findByUserId(userId);
        if (account == null) {
            throw new UsernameNotFoundException('Cannot find user by userId ' + userId);
        }
        return account;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return loadUserByUserId(username);
    }

    @Override
    public String extractUserId(Authentication authentication) {
        if (authentication instanceof SocialAuthenticationToken) {
            SocialAuthenticationToken token = (SocialAuthenticationToken) authentication;
            if (token.getPrincipal() instanceof SocialUserDetails) {
                return ((SocialUserDetails) token.getPrincipal()).getUserId();
            }
        }
        return null;
    }

}

The Java config code to glue them together for AccountService:

@Configuration
class MainAppConfig {
...
    @Bean
    public AccountService accountService(MongoTemplate mongoTemplate, UserAccountRepository accountRepository,
            UserSocialConnectionRepository userSocialConnectionRepository) {
        AccountServiceImpl service = new AccountServiceImpl(accountRepository, userSocialConnectionRepository,
                counterService(mongoTemplate));
        return service;
    }
...
}

When do we call AccountService.createUserAccount()? At the time when a first time user tries to sign in, and the system cannot find an existing UserAccount, so the ConnectionSignUp bean plugged into MongoUsersConnectionRepository will be called. (See my previous post for other spring social connection related code.) So ConnectionSignUp will pass ConnectionData to AccountService.createUserAccount():

public class AutoConnectionSignUp implements ConnectionSignUp{
    private final AccountService accountService;

    @Inject
    public AutoConnectionSignUp(AccountService accountService){
        this.accountService = accountService;
    }

    public String execute(Connection<?> connection) {
        ConnectionData data = connection.createData();

        UserAccount account = this.accountService.createUserAccount(data);

        return account.getUserId();
    }
}

My experience with Spring Data MongoDB is very positive. It is very powerful in providing basic CRUD functions as well as abundant query functions, and you don’t need to write any implementation code. If you have to use a special command of MongoDB, MongoTemplate is flexible enough to meet your requirements.
 

Reference: MongoDB: Add A CounterWithSpring Data from our JCG partner Yuan Ji at the Jiwhiz blog.

Yuan Ji

Yuan is a passionate Java programmer and open source evangelist. He is eager to learn new technologies and loves clean and beautiful application design. He lives in Edmonton, Canada as an independent consultant and contractor.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button