DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Low-Code Development: Leverage low and no code to streamline your workflow so that you can focus on higher priorities.

DZone Security Research: Tell us your top security strategies in 2024, influence our research, and enter for a chance to win $!

Launch your software development career: Dive head first into the SDLC and learn how to build high-quality software and teams.

Open Source Migration Practices and Patterns: Explore key traits of migrating open-source software and its impact on software development.

Related

  • Custom Health Checks in Spring Boot
  • Fine-Tuning Performance, Resolving Common Issues in FinTech Application With MySQL
  • Navigating the Maze: Evolving Projects and Database Dilemmas
  • The Beginner's Guide To Understanding Graph Databases

Trending

  • Empowering Citizen Developers With Low- and No-Code Tools: Changing Developer Workflows and Empowering Non-Technical Employees to Build Apps
  • Unlocking Potential With Mobile App Performance Testing
  • Maintain Chat History in Generative AI Apps With Valkey
  • Packages for Store Routines in MariaDB 11.4
  1. DZone
  2. Data Engineering
  3. Databases
  4. Hidden Gems: Event-Driven Change Notifications in Relational Databases

Hidden Gems: Event-Driven Change Notifications in Relational Databases

An introduction to event-driven change notifications via JDBC with examples written in Groovy for the PostgreSQL, Oracle, and H2 relational databases.

By 
Thomas Fuller user avatar
Thomas Fuller
·
Updated Sep. 21, 20 · Tutorial
Like (6)
Save
Tweet
Share
17.7K Views

Join the DZone community and get the full member experience.

Join For Free

Introduction

Wouldn’t it be great if we could receive event-driven change notifications (EDCN) when data changes directly from the database without having to poll for updates?

This feature is, in fact, available in some relational databases, but not all, as it’s non-standard functionality and not part of any SQL specification.

In the three examples covered in this article, this functionality is expressed via the implementation of an interface that is then registered with the JDBC driver directly. This opens the door to a myriad of potential use cases that can be expressed without the need to poll and which do not require the developer to write infrastructure code to deal with changes in data and notifying interested parties. Instead, we can interface with the driver directly and listen for changes and, when they occur, execute whatever workflow we have in an event-driven fashion. A few examples where this could be helpful include:

  • Caching (more on this when we cover PostgreSQL, see also Good Candidates for CQN)
  • Honeypots for database tables — also see poison records
  • Debugging problems
  • Logging changes
  • Analytics and reporting

There are, of course, some consequences when relying on this functionality. The most obvious implication is that it’s a non-standard feature that ties the application directly to the database.

I was speaking with Michael Dürgner on LinkedIn about an example implementation as it pertains to PostgreSQL, and he commented that:

“[W]hile it’s definitely a great way to do this, one of the big drawbacks is that you move application logic into the RDBMS. Not saying you shouldn’t do it but make sure that you have people with deep understanding of the RDBMS you use on board since it’ll be rather unlikely your average software will be able to trouble shoot. Another huge challenge with this approach is continuous delivery since your RDBMS needs to be deeply integrated with your delivery pipeline.”

I agree with Michael’s position, and keeping business logic out of the database tends to be a good practice.

Projects that rely on object-relational mapping (ORM) tools such as the Java Persistence API (JPA) to generate the database schema directly from one or more object models immediately lose portability and simplicity when developers are required to add logic in the database tier which probably belongs in the application itself. If developers are not careful, they’ll end up having to use the same database for testing as used in production and this could easily lead to pain and regret.

I proffer the following question to any engineer considering using EDCNs via the JDBC driver: can the application still function as intended without the inclusion of whatever it is that you’re building that relies on this functionality? If the answer is “yes” then what you’re doing is likely fine; on the contrary, if the answer is “no”, then this is a strike against using EDCNs and alternatives may need to be considered.

Finally, this feature on its own is not a substitute for well-engineered message-oriented middleware (MOM), which typically provides out-of-the-box solutions for guaranteed delivery, message persistence, at-least-once/exactly-once delivery, delivery via queues and topics, strategies for flow control (see also: backpressure), and addresses fault tolerance and scalability concerns. The presence of these requirements could be a strong indicator that an approach relying on EDCNs needs to be reconsidered.

Below we explore this functionality as it exists in the PostgreSQL, Oracle, and H2 databases; we also include some general comments on MySQL and its fork, MariaDB.

Throughout this article, we rely on Java 13.0.2 and Groovy. 3.0.4 and include links to the various scripts on GitHub which contain extra notes on how to set up the required dependencies and any other preconditions necessary to run the examples.


PostgreSQL

The PostgreSQL (Postgres) database is the first example we’ll examine.

The Postgres API includes the PGNotificationListener interface which must be implemented and then registered with the database connection. Note that there are two implementations of this available: the Postgres [default] JDBC driver and the Impossibl JDBC driver implementation. We do not want to use the Postgres driver, as that implementation will poll the database for changes. Instead, we will rely on the Impossibl implementation, which delivers true event-driven notifications.

I had the opportunity to speak with Erik Brandsberg, CTO of Heimdall Data about this, and Erik remarked that:

“The PG notify interface is one of the hidden gems in PG vs. other databases. We can use it to provide cache invalidation messages between proxies instead of using a distinct pub/sub interface such as on Redis.”

Heimdall Data delivers a sophisticated caching solution for applications that use Amazon Relational Database Service (Amazon RDS) and other databases and this is one real-world use case that demonstrates how important this functionality can be.

In the following example, the trigger and function script must be executed inside Postgres as a precondition to running the Groovy script. The notify_change function will send events to any registered listeners that are listening on the examplechannel channel — pay particular attention to the warning below, as channel names are case sensitive.

SQL
 




xxxxxxxxxx
1
22


 
1
CREATE OR REPLACE FUNCTION notify_change() RETURNS TRIGGER AS $$
2
  BEGIN
3
    --
4
    -- WARNING: Case is VERY IMPORTANT here! If we use 'exampleChannel' PG converts this to
5
    --          examplechannel and no events will be received!!
6
    --
7
    --  UPDATE: [to be confirmed] Case can be handled in PostgreSQL by using double quotes.
8
    --
9
    --          In theory, if you had the following line as the listener, it would work in camelCase.
10
    --
11
    --          statement.execute('LISTEN "exampleChannel"');
12
    --
13
    --          The same applies to any identifier in Postgres.
14
    --
15
    PERFORM pg_notify('examplechannel', NEW.phrase);
16
    RETURN NEW;
17
  END;
18
$$ LANGUAGE plpgsql;
19

           
20
CREATE TRIGGER table_change
21
  AFTER INSERT OR UPDATE OR DELETE ON example
22
  FOR EACH ROW EXECUTE PROCEDURE notify_change();


A working example implementation of the com.impossibl.postgres.api.jdbc. PGNotificationListener using the PostgreSQL database is included next. The PGNotificationListener interface requires that the developer implement just one method:

void notification(int processId, String channelName, String payload)

We can see this on line #18 below.

Groovy
 




xxxxxxxxxx
1
44
1
44


 
1
@Grapes(
2
    @Grab(group='com.impossibl.pgjdbc-ng', module='pgjdbc-ng', version='0.8.4')
3
)
4
import com.impossibl.postgres.api.jdbc.PGConnection
5
import com.impossibl.postgres.api.jdbc.PGNotificationListener
6
import com.impossibl.postgres.jdbc.PGDataSource
7

           
8
PGDataSource dataSource = new PGDataSource();
9
dataSource.setHost("0.0.0.0")
10
dataSource.setPort(5432)
11
dataSource.setDatabaseName("testdb")
12
dataSource.setUser("postgres")
13
dataSource.setPassword("password")
14

           
15
final def pgNotificationListener = new PGNotificationListener () { 
16

           
17
  @Override
18
  public void notification(int processId, String channelName, String payload) {
19
    println("processId $processId, channelName: $channelName, payload: $payload")
20
  }
21
}
22

           
23
final def connection = (PGConnection) dataSource.getConnection()
24

           
25
connection.addNotificationListener(pgNotificationListener)
26

           
27
final def statement = connection.createStatement()
28

           
29
statement.execute("LISTEN examplechannel")
30
statement.close()
31

           
32
def time = 60 * 60 * 1000
33

           
34
println "Will sleep for $time milliseconds..."
35

           
36
try {
37
  Thread.sleep (time)
38
} catch (Throwable thrown) {
39
  thrown.printStackTrace (System.err)
40
} finally {
41
  connection.close ()
42
}
43

           
44
print "...done!


We can see an example of this script being executed along with an explanation and the output which appears in the GroovyConsole in the following image.

An explanation of the PostgreSQL example running in the Groovy Console.

The next example we’ll cover includes event-driven change notification functionality as it applies to the Oracle Database.

Oracle

The next example we’ll cover in this article will focus on the Oracle Database (Oracle). Below we detail the steps required to configure event-driven change notifications via the JDBC driver along with preconditions required to run the example.

The following two gists are required preconditions for this example. It is instructive to note that Docker was running on another machine which, in this case, uses the Ubuntu operating system. See the warning regarding running Oracle in Docker locally in the DatabaseChangeListenerInOracleDatabaseExample.groovy script for complete details.

Shell
 




xxxxxxxxxx
1
11


 
1
#
2
# In this example Docker is running on another machine so assume that I've ssh'd into that box and
3
# am running the following on the remote machine.
4
#
5
docker run -d -p 1521:1521 oracleinanutshell/oracle-xe-11g
6
docker exec -it [container id] /bin/sh
7
su
8
#
9
# Username: system, password: oracle
10
#
11
/u01/app/oracle/product/11.2.0/xe/bin/sqlplus


In SQL*Plus, we can now run the following configuration script. Keep in mind that once the example table has been created, see line #8, the Groovy script in the next section can be started, and any insert, update, or delete operations on the target table will result in an event being sent to the Groovy script and then printed to the console output.

SQL
 




xxxxxxxxxx
1
26


 
1
--
2
-- This is required otherwise notifications won't be sent to the JDBC driver.
3
--
4
grant change notification to system;
5

           
6
commit;
7

           
8
CREATE TABLE example(
9
  example_id NUMBER(10) PRIMARY KEY,
10
  phrase VARCHAR2(120) NOT NULL
11
);
12

           
13
commit;
14

           
15
insert into example values (1, 'one');
16
insert into example values (2, 'two');
17
insert into example values (3, 'three');
18
insert into example values (4, 'four');
19
insert into example values (5, 'five');
20
commit;
21

           
22
--
23
-- Then once the DatabaseChangeListenerInOracleDatabaseExample.groovy is running
24
-- execute the following and an update should appear in the Groovy console:
25
--
26
update example set phrase = 'one / 1' where example_id = 1;


Here we have an example of the complete DatabaseChangeListenerInOracleDatabaseExample.groovy script. Note that the developer must implement one method:

void onDatabaseChangeNotification(DatabaseChangeEvent databaseChangeEvent)

We can see this implementation on line #55 below.

Groovy
 




x


 
1
@GrabConfig(systemClassLoader=true)
2

           
3
//
4
// https://docs.oracle.com/cd/E11882_01/appdev.112/e13995/index.html?oracle/jdbc/dcn/DatabaseChangeRegistration.html
5
//
6
// https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc6
7
@Grapes(
8
    @Grab(group='com.oracle.database.jdbc', module='ojdbc6', version='11.2.0.4')
9
)
10
import oracle.jdbc.dcn.DatabaseChangeListener
11
import oracle.jdbc.dcn.DatabaseChangeEvent
12
import oracle.jdbc.driver.OracleConnection
13
import oracle.jdbc.dcn.DatabaseChangeRegistration
14
import oracle.jdbc.OracleStatement
15

           
16
import java.sql.DriverManager
17

           
18
import java.util.Properties
19

           
20
//
21
// Note that the thin driver supports this example.
22
//
23
//
24
// SEE THE WARNING BELOW ABOUT RUNNING THIS SCRIPT ON LOCALHOST WITH ORACLE DB IN DOCKER, ALSO ON LOCALHOST.
25
//
26
final def connection = DriverManager.getConnection("jdbc:oracle:thin:@192.168.1.232:1521:xe", "system", "oracle")
27

           
28
def databaseProperties = new Properties ()
29

           
30
/*
31
 * [6] When the notification type is OCN, any DML transaction that changes one or more registered objects generates
32
 *     one notification for each object when it commits.
33
 *
34
 *     When the notification type is QRCN, any DML transaction that changes the result of one or more registered
35
 *     queries generates a notification when it commits. The notification includes the query IDs of the queries whose
36
 *     results changed.
37
 *
38
 *     For either notification type, the notification includes:
39
 *
40
 *     Name of each changed table
41
 *
42
 *     Operation type (INSERT, UPDATE, or DELETE)
43
 *
44
 *     ROWID of each changed row, if the registration was created with the ROWID option and the number of modified rows
45
 *     was not too large. For more information, see ROWID Option."
46
 */
47
databaseProperties.setProperty(OracleConnection.DCN_NOTIFY_ROWIDS, "true")
48
databaseProperties.setProperty(OracleConnection.DCN_QUERY_CHANGE_NOTIFICATION, "true")
49

           
50
final def databaseChangeRegistration = connection.registerDatabaseChangeNotification(databaseProperties)
51

           
52
public class ExampleDatabaseChangeListener implements DatabaseChangeListener {
53

           
54
    @Override
55
    public void onDatabaseChangeNotification(DatabaseChangeEvent databaseChangeEvent) {
56
        println ("***** databaseChangeEvent: $databaseChangeEvent")
57
        println ("***** databaseChangeEvent.source: ${databaseChangeEvent.source}")
58
        println ("***** databaseChangeEvent.queryChangeDescription: ${databaseChangeEvent.queryChangeDescription}")
59
        println ("***** databaseChangeEvent.tableChangeDescription: ${databaseChangeEvent.tableChangeDescription.each {println '\n  - nextTableChangeDescription: $it' } }")
60
    }
61
}
62

           
63
databaseChangeRegistration.addListener(new ExampleDatabaseChangeListener ())
64

           
65
final def statement = connection.createStatement()
66

           
67
statement.setDatabaseChangeRegistration(databaseChangeRegistration)
68

           
69
try {
70

           
71
  resultSet = statement.executeQuery("select * from example")
72

           
73
  while (resultSet.next())
74
    {} // println "resultSet.phrase: ${resultSet.getString('phrase')}"
75

           
76
} catch (Throwable thrown) {
77
  thrown.printStackTrace (System.err)
78
}
79

           
80
println "databaseChangeRegistration.userName: ${databaseChangeRegistration.userName}"
81

           
82
databaseChangeRegistration.tables.each {
83
    println "tables: $it"
84
}
85

           
86
final def time = 60 * 60 * 1000
87

           
88
println "Will sleep for $time milliseconds..."
89

           
90
try {
91
  Thread.sleep (time)
92
} catch (Throwable thrown) {
93
  thrown.printStackTrace (System.err)
94
} finally {
95
  statement.close ()
96
  connection.close ()
97
}
98

           
99
println "...done!"
100

           
101
/* WARNING: I'm copy-pasting the below message because this is important when running Oracle in Docker and then
102
 *          running this script on localhost. This caused me a few hours of time trying to figure out why the
103
 *          notification wasn't being received and ONLY APPLIES IF YOU'RE RUNNING DOCKER ON THE SAME MACHINE AS THIS
104
 *          SCRIPT IS BEING EXECUTED ON! In fact, I'm not bothering with this at the moment and am running Docker with
105
 *          Oracle on another machine entirely.
106
 *
107
 *          Note also that I've not been able to get this running ON THE SAME MACHINE using:
108
 *
109
 *          docker run -d -p 1521:1521 -p [47632:47632] oracleinanutshell/oracle-xe-11g
110
 *
111
 * FROM:
112
 *
113
 * https://stackoverflow.com/questions/26003506/databasechangeregistration-in-remote-server
114
 *
115
 * "You can check active listeners in the Oracle database running the following query:
116
 *
117
 * Select * FROM USER_CHANGE_NOTIFICATION_REGS
118
 * I the query does not return any rows probably the database server can't access the jdbc driver listener port.
119
 *
120
 * By default the Oracle JDBC driver listens at port 47632 for notifications. You will need to ensure that it is possible to connect to that port from the database server. You may need to add a rule in the firewall to accept incoming requests to that port.
121
 *
122
 * This port can be changed with the NTF_LOCAL_TCP_PORT option:
123
 *
124
 * prop.setProperty(OracleConnection.NTF_LOCAL_TCP_PORT, "15000");"
125
 *
126
 */


The following image goes into deeper detail regarding what each step is doing, along with some notes explaining the output.

A deeper explanation regarding what the example script, as it pertains to the Oracle database, is doing, including notes explaining the output.

Lastly, the following image demonstrates that when we perform five inserts in a row and then commit the changes, only a single event is emitted, which includes these five inserts. Events are only ever emitted whenever commit has returned successfully.

Five inserts are executed in SQL*Plus followed by a single commit and we can see that this event includes these five insert operations.

The final example we’ll cover in this article includes the H2 database.

H2 Database

The H2 database is an open-source, lightweight, and very powerful relational database written entirely in Java. It supports a long list of features and ships as a single 2.2mb jar file. H2 is frequently used when testing Java applications and works well as an embedded database and can be used with object-relational mapping tools such as the Java Persistence API (JPA). H2 is also embedded in the JBoss Wildfly application server (JBoss) and has been in use in JBoss as an embedded database for a very long time.

H2 delivers event notifications via the org.h2.api.DatabaseEventListener interface. The DatabaseEventListener interface offers limited functionality when compared with the Postgres and Oracle listener specifications covered earlier. The methods required to implement the interface are:

void closingDatabase ()
void exceptionThrown(SQLException sqlException, String sql)
void init (String url)
void opened ()
void setProgress (String state, String name, int x, int max)

A working example implementation of the org.h2.api.DatabaseEventListener using the H2 database can be found on GitHub and is also included in the gist below followed by an image with pointers explaining how this works.

In this example, H2 is running in embedded mode — that is, H2 is running entirely in memory in the same virtual machine that the Groovy script is running in.

Groovy
 




xxxxxxxxxx
1
46


 
1
@GrabConfig(systemClassLoader=true)
2

           
3
@Grapes(
4
  @Grab(group="com.h2database", module="h2", version="1.4.200")
5
)
6
import org.h2.api.DatabaseEventListener
7
import java.sql.SQLException
8
import java.sql.DriverManager
9

           
10
public class ExampleDatabaseEventListener implements DatabaseEventListener {
11

           
12
  public void closingDatabase () {
13
    println "closingDatabase: method invoked."
14
  }
15

           
16
  public void exceptionThrown (SQLException sqle, String sql) {
17
    println "exceptionThrown: method invoked; sqle: $sqle, sql: $sql"
18
  }
19

           
20
  public void init (String url) {
21
    println "init: method invoked; url: $url"
22
  }
23

           
24
  public void opened () {
25
    println "opened: method invoked."
26
  }
27

           
28
  public void setProgress (int state, String name, int x, int max) {
29
    println "setProgress: method invoked; state: $state, name: $name, x: $x, max: $max"
30
  }
31
}
32

           
33
//
34
// Note the event listener has been added as a parameter in the connection URL below.
35
//
36
def cxn = DriverManager.getConnection("jdbc:h2:mem:EventListenerInH2DatabaseExampleDB;DB_CLOSE_DELAY=-1;DATABASE_EVENT_LISTENER='ExampleDatabaseEventListener';")
37
def stm = cxn.createStatement()
38
def resultSet = stm.executeQuery("SELECT 1+1")
39

           
40
if (resultSet.next()) {
41
  println("next: ${resultSet.getInt(1)}")
42
}
43

           
44
cxn.close ()
45

           
46
println "...Done!"


In the following image, we can see an example of this script being executed, along with the output in the GroovyConsole.

An example of the H2 DatabaseEventListener running in the Groovy Console including output.

It’s somewhat disappointing that the H2 DatabaseEventListener doesn’t offer similar functionality to that found in the PostgreSQL interface. As a result, I submitted a new feature request on the H2 Database repository on GitHub and may try to implement this myself, time permitting.

MySQL/MariaDB

Event-driven change notifications via the JDBC driver do not appear to be supported by either MySQL or MariaDB databases and so engineers will have to consider alternative solutions if this functionality is required.

We will not cover triggers and user-defined functions (UDF) as they pertain to MySQL and MariaDB to call a web service endpoint, which is one such alternative. Brief research on this subject suggests that triggers and UDFs can be used to accomplish this; however, they come with potentially significant security and performance implications that must be considered when taking this approach.

If you’ve used triggers and UDFs or some other solution to accomplish this within MySQL and/or MariaDB please feel free to elaborate on what you needed to do, the approach you took, and how well it worked out in the comments. Finally, If there’s a better solution available in MySQL and MariaDB, please explain further.

Conclusion

Have you used event-driven change notifications in relational databases in a project you were on? If so, I encourage you to discuss any of the following in the comments:

  • What was it needed for?
  • What database was used?
  • What language was the solution written in?
  • Can you do it with Microsoft SQL Server or another database not covered here?
  • Were any problems encountered, and how were they addressed?
  • Was this option explored and determined to not be an appropriate solution for the problem at hand?
  • Were you forced to use an alternative and, if so, what did the implementation look like and why?
  • Any other thoughts you might have, including any questions I might have left out here.

While EDCNs offers powerful functionality in supported relational databases, we hope that, in addition to providing some working examples, it has also been made clear that there’s a cost associated with relying on this feature and that you must give due consideration before using it.

Happy coding!

Database Relational database

Published at DZone with permission of Thomas Fuller. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Custom Health Checks in Spring Boot
  • Fine-Tuning Performance, Resolving Common Issues in FinTech Application With MySQL
  • Navigating the Maze: Evolving Projects and Database Dilemmas
  • The Beginner's Guide To Understanding Graph Databases

Partner Resources


Comments

ABOUT US

  • About DZone
  • Send feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: