ActiveJDBC is a Java library implements Active record design pattern. It was inspired by ActiveRecord ORM from Ruby on Rails. ActiveJDBC can be used as an alternative to Hibernate in many projects. As long as there is little to no business logic involved. In this article, I demonstrate a comprehensive example of how to use ActiveJDBC with the conjunction of Spring Boot and MySQL 8.
The example of my choice is a music streaming platform where users can create a Playlist
Songs
User
Song
Platlist
Keep in mind that we will not have service
Without further ado, let’s get started.
Adding ActiveJDBC dependencies
First thing first, we need to add ActiveJDBC dependency to our project and then do some configurations to make it running.
<dependencies>
<dependency>
<groupId>org.javalite</groupId>
<artifactId>activejdbc</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.javalite</groupId>
<artifactId>activejdbc-instrumentation</artifactId>
<version>2.2</version>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>instrument</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.javalite</groupId>
<artifactId>db-migrator-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<configFile>${project.basedir}/src/main/resources/database.properties</configFile>
<environments>development</environments>
</configuration>
<executions>
<execution>
<id>dev_migrations</id>
<phase>validate</phase>
<goals>
<goal>migrate</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
The first paragraph of the above snippet is rather simple. We just added ActiveJDBC and MySQL connector as the project dependency. But most of the magic comes in the second part of the code where we need to have two maven plugins.
The first plugin generates models and adds them in the classpath. This is necessary, otherwise, the application throws run time exception if does not find instruments
process-classes
every
And the second one handles the database migration or evolution which I configured what environmentdevelopment
schema_version
Unlike the previous plugin, database plugin is optional and can be omitted. But then all the database tables and changes should be done manually.
Database schema
For the music streaming example, we need to create at least four tables, users
, songs
, playlists
, and playlist_song
. The last table is required in order to have many to many relationship between song
and playlists
. As one song can appear in many playlists and one playlist can have many songs.
The final database schema is look like below:
CREATE TABLE IF NOT EXISTS users (
id INTEGER DEFAULT NULL AUTO_INCREMENT PRIMARY KEY,
email_address VARCHAR(512) NOT NULL,
first_name VARCHAR(512) NOT NULL,
last_name VARCHAR(512) NOT NULL,
CONSTRAINT uc_user_email_address UNIQUE(email_address),
INDEX idx_user_email_address (email_address)
) engine=InnoDB;
CREATE TABLE IF NOT EXISTS songs (
id INTEGER AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(512) NOT NULL,
artist VARCHAR(512) NOT NULL,
user_id INTEGER NOT NULL,
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
) engine=InnoDB;
CREATE TABLE IF NOT EXISTS playlists (
id INTEGER AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(512) NOT NULL,
user_id INTEGER NOT NULL,
CONSTRAINT uc_playlist_name UNIQUE(name),
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
) engine=InnoDB;
CREATE TABLE IF NOT EXISTS playlist_song (
id INTEGER AUTO_INCREMENT PRIMARY KEY,
playlist_id INTEGER NOT NULL,
song_id INTEGER NOT NULL,
CONSTRAINT fk_playlist_song_playlist_id FOREIGN KEY (playlist_id) REFERENCES playlists (id),
CONSTRAINT fk_playlist_song_song_id FOREIGN KEY (song_id) REFERENCES songs (id)
) engine=InnoDB;
To utilize ActiveJDBC database migrator, we need to place the SQL file inside of migrations
src
In order for ActiveJDBC migrator to pick up the script, we need to follow a certain file naming convention such as 20190503131110_create_tables.sql
. The format is Year Month Day Hour Minute Second_any description
. I’ve tried other format and none has worked. Hence, I assume this is the only format to follow.
Integrating ActiveJDBC with Spring Boot ecosystem
Before starting to implement the code, we need to do one last configuration. And that’s integrating ActiveJDBC with Spring Boot. All we need to do is to create a Servlet filter that opens the database connection before hitting any endpoint and closes after that. This essentially is an optional requirement to reduce code complications. Otherwise, we need to open and close the connection each time manually.
The filter is as follows:
package com.madadipouya.example.activejdbc.activejdbcexample.filters;
import org.javalite.activejdbc.Base;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;
@Component
public class ActiveJdbcFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(ActiveJdbcFilter.class);
@Override
public void init(FilterConfig config) {
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
long before = System.currentTimeMillis();
try {
Base.open();
Base.openTransaction();
chain.doFilter(req, resp);
Base.commitTransaction();
} catch (IOException | ServletException e) {
Base.rollbackTransaction();
throw e;
} finally {
Base.close();
}
logger.info("Processing took: {} milliseconds", System.currentTimeMillis() - before);
}
@Override
public void destroy() {
}
}
Keep in mind the Servlet filter is not the only option. The same results can be achieved by defining aspects or HandlerInterceptor
Creating models
Now that we have done with the configurations, it’s time to create models for our database tables. Unlike Hibernate, we don’t need to define any property in the models. Solely, extending activejdbc.Model
suffices to have a functioning entity.
Keep in mind all the validations need to be done in the model static
We define our music streaming table model as below:
package com.madadipouya.example.activejdbc.activejdbcexample.model;
import org.javalite.activejdbc.Model;
import org.javalite.activejdbc.annotations.Table;
@Table("users")
public class User extends Model {
private static final String FIRST_NAME_FIELD = "first_name";
private static final String LAST_NAME_FIELD = "last_name";
private static final String EMAIL_ADDRESS_FIELD = "email_address";
static {
validatePresenceOf(FIRST_NAME_FIELD, LAST_NAME_FIELD, EMAIL_ADDRESS_FIELD);
validateEmailOf(EMAIL_ADDRESS_FIELD);
}
public User() {
}
public User(String firstName, String lastName, String emailAddress) {
set(FIRST_NAME_FIELD, firstName, LAST_NAME_FIELD, lastName, EMAIL_ADDRESS_FIELD, emailAddress);
}
public User merge(String firstName, String lastName, String emailAddress) {
return set(FIRST_NAME_FIELD, firstName, LAST_NAME_FIELD, lastName, EMAIL_ADDRESS_FIELD, emailAddress);
}
}
package com.madadipouya.example.activejdbc.activejdbcexample.model;
import org.javalite.activejdbc.Model;
import org.javalite.activejdbc.annotations.Many2Many;
import org.javalite.activejdbc.annotations.Table;
@Many2Many(other = Playlist.class, join = "playlist_song", sourceFKName = "song_id", targetFKName = "playlist_id")
@Table("songs")
public class Song extends Model {
private static final String TITLE_FIELD = "title";
private static final String ARTIST_FIELD = "artist";
static {
validatePresenceOf(TITLE_FIELD, ARTIST_FIELD);
}
public Song() {
}
public Song(String title, String artist) {
set(TITLE_FIELD, title, ARTIST_FIELD, artist);
}
public Song merge(String title, String artist) {
return set(TITLE_FIELD, title, ARTIST_FIELD, artist);
}
}
package com.madadipouya.example.activejdbc.activejdbcexample.model;
import org.javalite.activejdbc.Model;
import org.javalite.activejdbc.annotations.Table;
@Table("playlists")
public class Playlist extends Model {
private static final String PLAYLIST_NAME_FIELD = "name";
static {
validatePresenceOf(PLAYLIST_NAME_FIELD);
}
public Playlist() {
}
public Playlist(String name) {
set(PLAYLIST_NAME_FIELD, name);
}
public Playlist merge(String name) {
return set(PLAYLIST_NAME_FIELD, name);
}
}
As described before, all the validations
The interesting method here is the merge
method which I coded for enabling flawless updates which in reality is no difference than create.
It is important to know that Many to Many
Song
model by annotating the class with:
@Many2Many(other = Playlist.class, join = "playlist_song", sourceFKName = "song_id", targetFKName = "playlist_id")
Many to Many
table name_table name
playlist_song
playlists_songs
Implementing the Spring Boots controllers
The next which is the last step is to implement the endpoints. These endpoints are not different than normal controllers just they might have some additional logic. One shortcoming of ActiveJDBC is by default it didn’t ship with many validators. One way to compensate that is to use Javax validators in DTOs to disallow end users entering undesirable inputs.
The following is the implementation of our music stream controllers:
package com.madadipouya.example.activejdbc.activejdbcexample.controller;
import com.google.common.collect.ImmutableMap;
import com.madadipouya.example.activejdbc.activejdbcexample.controller.exceptions.UserNotFoundException;
import com.madadipouya.example.activejdbc.activejdbcexample.model.Playlist;
import com.madadipouya.example.activejdbc.activejdbcexample.model.Song;
import com.madadipouya.example.activejdbc.activejdbcexample.model.User;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RequestMapping("/v1/users")
@RestController
public class UserController {
@GetMapping
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public List<Map<String, Object>> getUsers() {
return User.findAll().toMaps();
}
@GetMapping("/details")
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public List<Map<?, ?>> getUsersWithDetails() {
return User.findAll().stream().map(user -> ImmutableMap.builder().putAll(user.toMap())
.put("songs", user.getAll(Song.class).toMaps()).put("playlists", user.getAll(Playlist.class).toMaps())
.build()).collect(Collectors.toList());
}
@ResponseStatus(HttpStatus.CREATED)
@PostMapping
public Map<String, Object> createUser(@Valid @RequestBody UserDto userDto) {
User user = new User(userDto.firstName, userDto.lastName, userDto.emailAddress);
if (user.saveIt()) {
return user.toMap();
}
return null;
}
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping(value = "/{userId}")
public void deleteUser(@PathVariable int userId) throws UserNotFoundException {
User user = User.findById(userId);
if (user != null) {
user.deleteCascade();
} else {
throw new UserNotFoundException(String.format("There is no user associated with id: %s", userId));
}
}
@PutMapping(value = "/{userId}")
@ResponseStatus(HttpStatus.OK)
public Map<String, Object> updateUser(@PathVariable int userId, @RequestBody UserDto userDto) throws UserNotFoundException {
User user = User.findById(userId);
if (user != null) {
user.merge(userDto.firstName, userDto.lastName, userDto.emailAddress);
if (user.saveIt()) {
return user.toMap();
}
return null;
}
throw new UserNotFoundException(String.format("There is no user associated with id: %s", userId));
}
public static class UserDto {
@Email
@NotBlank
private String emailAddress;
@NotBlank
private String firstName;
@NotBlank
private String lastName;
public String getEmailAddress() {
return emailAddress;
}
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
}
package com.madadipouya.example.activejdbc.activejdbcexample.controller;
import com.madadipouya.example.activejdbc.activejdbcexample.controller.exceptions.SongNotFoundException;
import com.madadipouya.example.activejdbc.activejdbcexample.controller.exceptions.UserNotFoundException;
import com.madadipouya.example.activejdbc.activejdbcexample.model.Song;
import com.madadipouya.example.activejdbc.activejdbcexample.model.User;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.util.List;
import java.util.Map;
@RequestMapping("/v1/songs")
@RestController
public class SongController {
@GetMapping(value = "/{userId}")
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public List<Map<String, Object>> getSongs(@PathVariable int userId) throws UserNotFoundException {
User user = User.findById(userId);
if (user != null) {
return user.getAll(Song.class).toMaps();
}
throw new UserNotFoundException(String.format("There is no user associated with id: %s", userId));
}
@ResponseStatus(HttpStatus.CREATED)
@PostMapping(value = "/{userId}")
public Map<String, Object> createSong(@PathVariable int userId, @Valid @RequestBody SongDto songDto) throws UserNotFoundException {
User user = User.findById(userId);
if (user != null) {
Song song = new Song(songDto.title, songDto.artist);
user.add(song);
return song.toMap();
}
throw new UserNotFoundException(String.format("There is no user associated with id: %s", userId));
}
@ResponseStatus(HttpStatus.OK)
@PutMapping(value = "/{songId}")
public Map<String, Object> updateSong(@PathVariable int songId, @Valid @RequestBody SongDto songDto) throws SongNotFoundException {
Song song = Song.findById(songId);
if (song != null) {
song.merge(songDto.title, songDto.artist);
if (song.saveIt()) {
return song.toMap();
}
return null;
}
throw new SongNotFoundException(String.format("There is no song associated with id: %s", songId));
}
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping(value = "/{songId}")
public void deleteSong(@PathVariable int songId) throws SongNotFoundException {
Song song = Song.findById(songId);
if (song != null) {
song.deleteCascadeShallow();
} else {
throw new SongNotFoundException(String.format("There is no song associated with id: %s", songId));
}
}
public static class SongDto {
@NotBlank
private String title;
@NotBlank
private String artist;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getArtist() {
return artist;
}
public void setArtist(String artist) {
this.artist = artist;
}
}
}
package com.madadipouya.example.activejdbc.activejdbcexample.controller;
import com.google.common.collect.ImmutableMap;
import com.madadipouya.example.activejdbc.activejdbcexample.controller.exceptions.PlaylistNotFoundException;
import com.madadipouya.example.activejdbc.activejdbcexample.controller.exceptions.UserNotFoundException;
import com.madadipouya.example.activejdbc.activejdbcexample.model.Playlist;
import com.madadipouya.example.activejdbc.activejdbcexample.model.Song;
import com.madadipouya.example.activejdbc.activejdbcexample.model.User;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RequestMapping("/v1/playlists")
@RestController
public class PlaylistController {
@GetMapping(value = "/{userId}")
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public List<Map<String, Object>> getPlaylists(@PathVariable int userId) throws UserNotFoundException {
User user = User.findById(userId);
if (user != null) {
return user.getAll(Playlist.class).toMaps();
}
throw new UserNotFoundException(String.format("There is no user associated with id: %s", userId));
}
@GetMapping(value = "/{userId}/details")
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public List<Map<?, Object>> getPlaylistWithSongs(@PathVariable int userId) throws UserNotFoundException {
User user = User.findById(userId);
if (user != null) {
return createPlaylistWithSongsResponse(user);
}
throw new UserNotFoundException(String.format("There is no user associated with id: %s", userId));
}
@ResponseStatus(HttpStatus.CREATED)
@PostMapping(value = "/{userId}")
public Map<String, Object> createPlaylist(@PathVariable int userId, @Valid @RequestBody PlaylistDto playlistDto) throws UserNotFoundException {
User user = User.findById(userId);
if (user != null) {
Playlist playlist = new Playlist(playlistDto.name);
user.add(playlist);
addSongsToPlaylist(playlist, filterInvalidSongs(user, playlistDto));
return playlist.toMap();
}
throw new UserNotFoundException(String.format("There is no user associated with id: %s", userId));
}
@ResponseStatus(HttpStatus.OK)
@PutMapping(value = "/{playlistId}")
public List<Map<?, Object>> updatePlaylist(@PathVariable int playlistId, @Valid @RequestBody PlaylistDto playlistDto) throws PlaylistNotFoundException {
Playlist playlist = Playlist.findById(playlistId);
if (playlist != null) {
User user = User.findById(playlist.get("user_id"));
playlist.merge(playlistDto.name);
if (playlist.saveIt()) {
updatePlaylist(playlist, filterInvalidSongs(user, playlistDto));
}
return createPlaylistWithSongsResponse(user);
}
throw new PlaylistNotFoundException(String.format("There is no playlist associated with id: %s", playlistId));
}
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping(value = "/{playlistId}")
public void deletePlaylist(@PathVariable int playlistId) throws PlaylistNotFoundException {
Playlist playlist = Playlist.findById(playlistId);
if (playlist != null) {
playlist.deleteCascadeShallow();
} else {
throw new PlaylistNotFoundException(String.format("There is no playlist associated with id: %s", playlistId));
}
}
private void addSongsToPlaylist(Playlist playlist, List<Song> songs) {
songs.forEach(playlist::add);
}
private void updatePlaylist(Playlist playlist, List<Song> songs) {
songs.forEach(playlist::remove);
addSongsToPlaylist(playlist, songs);
}
private List<Song> filterInvalidSongs(User user, PlaylistDto playlistDto) {
return user.getAll(Song.class).stream().filter(song -> playlistDto.songIds.contains(song.getId()))
.collect(Collectors.toList());
}
private List<Map<?, Object>> createPlaylistWithSongsResponse(User user) {
return user.getAll(Playlist.class).stream().map(playlist -> ImmutableMap.builder().putAll(playlist.toMap())
.put("songs", playlist.getAll(Song.class).toMaps()).build()).collect(Collectors.toList());
}
public static class PlaylistDto {
@NotBlank
private String name;
@NotNull
private List<Integer> songIds;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Integer> getSongIds() {
return songIds;
}
public void setSongIds(List<Integer> songIds) {
this.songIds = songIds;
}
}
}
Before finishing this post, I’d like to touch down on delete methods in ActiveJDBC. In our example we used three types of delete and each has a different functionality as below:
delete
: deletes the sole row of the table only. Such as deleting a user or a song.deleteCascade
: deletes the row with all its associations entirely. For example, deleting a user row results in removing all the user’s songs and playlists.deleteCascadeShallow
: deletes the row and its associated foreign keys. For example, deleting a song results in deleting the foreign key of the songfrom playlist_song
table
The example source code is available at GitHub at this link: https://github.com/kasramp/active-jdbc-example
All the instructions on how to run the project are provided in the readme file.
Well, that’s all about the ActiveJDBC. Of course, the example presented here was a simple one. You can do much more with ActiveJDBC and use it as a full-blown ORM. In the next article, I’ll discuss the Active Record pattern.
If you are interested about high performance data insertion with Spring Boot and Hibernete have a look at my previous tutorial at below link:
https://www.geekyhacker.com/high-performance-data-fetching-using-spring-data-jpa-stream/