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.