Mitch Pronschinske is the Lead Research Analyst at DZone. Researching and compiling content for DZone's research guides is his primary job. He likes to make his own ringtones, watches cartoons/anime, enjoys card and board games, and plays the accordion. Mitch is a DZone Zone Leader and has posted 2576 posts at DZone. You can read more from them at their website. View Full User Profile

Sharding in Grails

08.02.2010
| 10785 views |
  • submit to reddit
Its important to build apps that have a system for scaling horizontally from the start.  Sharding is a good solution for this case since it allows developers to define multiple database 'shards' to scale data across multiple database schema and servers.  Developers can easily modify the database targeted by the Domain Classes with a simple service call.  We'll look at how sharding is achieved in Grails (based on a user account) with a relatively new plugin.

To install the plugin run the install-plugin script "sharding" (Must be using Grails 1.2.1 or higher):
grails install-plugin sharding
Grails uses convention over configuration, but for the sharding plugin developers will need to provide configuration in some cases.  The plugin uses an index database that holds a minimum of two tables.  The plugin creates one table containing a record of every shard along with the capacity and usage field.  This table is queried when a new object (User) is created.  The object is assigned to the shard with the lowest usage capacity ratio.  The other table is provided by the application.  This table maps the object onto the shard and the application will query this table to retrieve the right shard for the plugin to use.

Here a description of the DSL used to configure the sharding plugin.  The configuration is contained in grails-app/conf/Shards.groovy:

index - This section contains data about the Index Database
  • domainClass - The name of the domain class used to represent the object used to segment the data.
  • shardNameFieldName - The name of the domain class field that will hold the shard the object is assigned to.
  • name - The name used to represent the SessionFactory and DataSource for this Index Database
  • user - The username used to connect to the Index database
  • password - The password used to connect to the Index database
  • driverClass - The name of the driver class used to talk to the Index Database
  • jdbcUrl - The jdbc connection string used to connect to the Index database
  • dialect - The hibernate dialect used to talk to the Index database
shards - This section has a set of entries for each shard in the system
shard_XX - Container for a shard, XX should be a two digit number that increments and is unique
  • name - The string used to represent this shard.
  • user - The username used to connect to the shard database
  • password - The password used to connect to the shard database
  • driverClass - The name of the driver class used to talk to the shard Database
  • jdbcUrl - The jdbc connection string used to connect to the shard database
  • capacity - The number of objecs that can be assigned to the shard database before it is considered full, please note this is only used to calculate a percentage full. This does not stop objects being assigned if the percentage exceeds 100%.

This does not stop objects being assigned if the percentage exceeds 100%.

Lets look at Jeff Rick's (the plugin's creator) example:

First, create two domain classes:
grails create-domain-class UserIndex
grails create-domain-class Comment
Then some properties for the domain objects:

grails-app/domain/UserIndex.groovy
class UserIndex {

String userName

String shard

static constraints = {
}
}
grails-app/domain/Comment.groovy
class Comment {

Integer userIndexId

String comment

static constraints = {
}
}
The UserIndex associates a userName with a shard so the app can identify which shard owns what user, and then switch to that shard.  The 'Comment' object is a sample of some data you might store with a user.  

Next, we'll create a definition of the databases holding the application data.  This is a basic example with two Shard databases and an Index database.  You may need to change your connection settings in the config file below, which defines the databases:
grails-app/conf/Shards.groovy:
index = {
domainClass('UserIndex')
shardNameFieldName('shard')

name('shardINDEX')
user('root')
password('PASSWORD')
driverClass('com.mysql.jdbc.Driver')
jdbcUrl('jdbc:mysql://localhost:3306/shardINDEX')
dialect(org.hibernate.dialect.MySQL5InnoDBDialect)
}
shards = {
shard_01 {
name('shard1001')
user('root')
password('PASSWORD')
driverClass('com.mysql.jdbc.Driver')
capacity(1000)
jdbcUrl('jdbc:mysql://localhost:3306/shard1001')
}
shard_02 {
name('shard1002')
user('root')
password('PASSWORD')
driverClass('com.mysql.jdbc.Driver')
capacity(1000)
jdbcUrl('jdbc:mysql://localhost:3306/shard1002')
}
}
Create three schema in the database (example uses MySQL):
create schema shardINDEX;
create schema shard1001;
create schema shard1002;
Add a dependency for your DB driver (MySQL in this case) to the grails-app/conf/BuildConfig.groovy dependencies section.
dependencies {
runtime 'mysql:mysql-connector-java:5.1.5'
}
And create default templates for UserIndex and Comment.
grails generate-all UserIndex
grails generate-all Comment
Add these closures to grails-app/controllers/UserIndexController.groovy:
def login = {
session.userName = params.userName
render "User logged in."
}

def logout = {
session.userName = null
render "User logged out"
}
And modify the grails-app.controllers/controllers/CommentController.groovy (which will use the logged-in user's name and switch to the appropriate shard):
import UserIndex

class CommentController {
def shardService

static allowedMethods = [save: "POST", update: "POST", delete: "POST"]

def index = {
redirect(action: "list", params: params)
}

def list = {
def user = UserIndex.findByUserName(session.userName)
shardService.changeByObject(user)

params.max = Math.min(params.max ? params.int('max') : 10, 100)
[commentInstanceList: Comment.list(params), commentInstanceTotal: Comment.count()]
}

def create = {
def user = UserIndex.findByUserName(session.userName)
shardService.changeByObject(user)

def commentInstance = new Comment()
commentInstance.properties = params
return [commentInstance: commentInstance]
}

def save = {
def user = UserIndex.findByUserName(session.userName)
shardService.changeByObject(user)

def commentInstance = new Comment(params)
if (commentInstance.save(flush: true)) {
flash.message = "${message(code: 'default.created.message', args: [message(code: 'comment.label', default: 'Comment'), commentInstance.id])}"
redirect(action: "show", id: commentInstance.id)
}
else {
render(view: "create", model: [commentInstance: commentInstance])
}
}

def show = {
def user = UserIndex.findByUserName(session.userName)
shardService.changeByObject(user)

def commentInstance = Comment.get(params.id)
if (!commentInstance) {
flash.message = "${message(code: 'default.not.found.message', args: [message(code: 'comment.label', default: 'Comment'), params.id])}"
redirect(action: "list")
}
else {
[commentInstance: commentInstance]
}
}

def edit = {
def user = UserIndex.findByUserName(session.userName)
shardService.changeByObject(user)

def commentInstance = Comment.get(params.id)
if (!commentInstance) {
flash.message = "${message(code: 'default.not.found.message', args: [message(code: 'comment.label', default: 'Comment'), params.id])}"
redirect(action: "list")
}
else {
return [commentInstance: commentInstance]
}
}

def update = {
def user = UserIndex.findByUserName(session.userName)
shardService.changeByObject(user)

def commentInstance = Comment.get(params.id)
if (commentInstance) {
if (params.version) {
def version = params.version.toLong()
if (commentInstance.version > version) {

commentInstance.errors.rejectValue("version", "default.optimistic.locking.failure", [message(code: 'comment.label', default: 'Comment')] as Object[], "Another user has updated this Comment while you were editing")
render(view: "edit", model: [commentInstance: commentInstance])
return
}
}
commentInstance.properties = params
if (!commentInstance.hasErrors() && commentInstance.save(flush: true)) {
flash.message = "${message(code: 'default.updated.message', args: [message(code: 'comment.label', default: 'Comment'), commentInstance.id])}"
redirect(action: "show", id: commentInstance.id)
}
else {
render(view: "edit", model: [commentInstance: commentInstance])
}
}
else {
flash.message = "${message(code: 'default.not.found.message', args: [message(code: 'comment.label', default: 'Comment'), params.id])}"
redirect(action: "list")
}
}

def delete = {
def user = UserIndex.findByUserName(session.userName)
shardService.changeByObject(user)

def commentInstance = Comment.get(params.id)
if (commentInstance) {
try {
commentInstance.delete(flush: true)
flash.message = "${message(code: 'default.deleted.message', args: [message(code: 'comment.label', default: 'Comment'), params.id])}"
redirect(action: "list")
}
catch (org.springframework.dao.DataIntegrityViolationException e) {
flash.message = "${message(code: 'default.not.deleted.message', args: [message(code: 'comment.label', default: 'Comment'), params.id])}"
redirect(action: "show", id: params.id)
}
}
else {
flash.message = "${message(code: 'default.not.found.message', args: [message(code: 'comment.label', default: 'Comment'), params.id])}"
redirect(action: "list")
}
}
}
Now you Run the application! (grails run-app)

Finally you'll navigate to http://localhost:8080/ShardingExample/userIndex and create a few users, login, create comments, and then do the same for the second user to see that the shards are separated.

The sample application described above can be obtained here.

Comments

Liran Zelkha replied on Fri, 2011/08/05 - 9:23am

It's a great post. However, sharding here is a bit simple - what happens if you need to run multi-shard join operations or aggregate functions? What will happen to your backup or reporting solution when you shard? It's best to try an off-the-shelf solution for these. You can try out ScaleBase to see how easy database sharding can be (disclaimer - I work there).

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.