Java Development with Microsoft SQL Server
By: Gary A. Stafford
Tags: Microsoft Sql Server, Java, Development, Programming, Microsoft

Java Development with Microsoft SQL Server

Calling Microsoft SQL Server Stored Procedures from Java Applications Using JDBC

Introduction

Enterprise software solutions often combine multiple technology platforms. Accessing an Oracle database via a Microsoft .NET application and vice-versa, accessing Microsoft SQL Server from a Java-based application is common. In this post, we will explore the use of the JDBC (Java Database Connectivity) API to call stored procedures from a Microsoft SQL Server 2017 database and return data to a Java 11-based console application.

View of the post’s Java project from JetBrains’ IntelliJ IDE

The objectives of this post include:

Demonstrate the differences between using static SQL statements and stored procedures to return data.

Demonstrate three types of JDBC statements to return data: Statement , PreparedStatement , and CallableStatement .

, , and . Demonstrate how to call stored procedures with input and output parameters.

Demonstrate how to return single values and a result set from a database using stored procedures.

Why Stored Procedures?

To access data, many enterprise software organizations require their developers to call stored procedures within their code as opposed to executing static T-SQL (Transact-SQL) statements against the database. There are several reasons stored procedures are preferred:

Optimization: Stored procedures are often written by database administrators (DBAs) or database developers who specialize in database development. They understand the best way to construct queries for optimal performance and minimal load on the database server. Think of it as a developer using an API to interact with the database.

Safety and Security: Stored procedures are considered safer and more secure than static SQL statements. The stored procedure provides tight control over the content of the queries, preventing malicious or unintentionally destructive code from being executed against the database.

Error Handling: Stored procedures can contain logic for handling errors before they bubble up to the application layer and possibly to the end-user.

AdventureWorks 2017 Database

For brevity, I will use an existing and well-known Microsoft SQL Server database, AdventureWorks. The AdventureWorks database was originally published by Microsoft for SQL Server 2008. Although a bit dated architecturally, the database comes prepopulated with plenty of data for demonstration purposes.

The HumanResources schema, one of five schemas within the AdventureWorks database

For the demonstration, I have created an Amazon RDS for SQL Server 2017 Express Edition instance on AWS. You have several options for deploying SQL Server, including AWS, Microsoft Azure, Google Cloud, or installed on your local workstation.

There are many methods to deploy the AdventureWorks database to Microsoft SQL Server. For this post’s demonstration, I used the AdventureWorks2017.bak backup file, which I copied to Amazon S3. I then enabled and configured the native backup and restore feature of Amazon RDS for SQL Server to import and install the backup.

DROP DATABASE IF EXISTS AdventureWorks;

GO



EXECUTE msdb.dbo.rds_restore_database

@restore_db_name='AdventureWorks',

@s3_arn_to_restore_from='arn:aws:s3:::my-bucket/AdventureWorks2017.bak',

@type='FULL',

@with_norecovery=0;



-- get task_id from output (e.g. 1)



EXECUTE msdb.dbo.rds_task_status

@db_name='AdventureWorks',

@task_id=1;

Install Stored Procedures

For the demonstration, I have added four stored procedures to the AdventureWorks database to use in this post. To follow along, you will need to install these stored procedures, which are included in the GitHub project.

View of the new stored procedures from JetBrains’ IntelliJ IDE Database tab

Data Sources, Connections, and Properties

Using the latest Microsoft JDBC Driver 8.4 for SQL Server (version 8.4.1.jre11), we create a SQL Server data source, com.microsoft.sqlserver.jdbc.SQLServerDataSource , and database connection, java.sql.Connection . There are several patterns for creating and working with JDBC data sources and connections. This post does not necessarily focus on the best practices for creating or using either. In this example, the application instantiates a connection class, SqlConnection.java , which in turn instantiates the java.sql.Connection and com.microsoft.sqlserver.jdbc.SQLServerDataSource objects. The data source’s properties are supplied from an instance of a singleton class, ProjectProperties.java . This class instantiates the java.util.Properties class, which reads values from a configuration properties file, config.properties . On startup, the application creates the database connection, calls each of the example methods, and then closes the connection.

Examples

For each example, I will show the stored procedure, if applicable, followed by the Java method that calls the procedure or executes the static SQL statement. For brevity, I have left out the data source and connection code in the article. Again, a complete copy of all the code for this article is available on GitHub, including Java source code, SQL statements, helper SQL scripts, and a set of basic JUnit tests. Use the following command to git clone a local copy of the project.



https://github.com/garystafford/mssql-sp-java-jdbc-2020.git git clone --branch master --single-branch --depth 1 --no-tags \

To run the JUnit unit tests, using Gradle, which the project is based on, use the ./gradlew cleanTest test --warning-mode none command.

A successful run of the JUnit tests

To build and run the application, using Gradle, which the project is based on, use the ./gradlew run --warning-mode none command.

The output of the Java console application

Example 1: SQL Statement

Before jumping into stored procedures, we will start with a simple static SQL statement. This example’s method, getAverageProductWeightST , uses the java.sql.Statement class. According to Oracle’s JDBC documentation, the Statement object is used for executing a static SQL statement and returning the results it produces. This SQL statement calculates the average weight of all products in the AdventureWorks database. It returns a solitary double numeric value. This example demonstrates one of the simplest methods for returning data from SQL Server.

/**

* Statement example, no parameters, returns Integer

*

* @return Average weight of all products

*/

public double getAverageProductWeightST() {

double averageWeight = 0;

Statement stmt = null;

ResultSet rs = null; try {

stmt = connection.getConnection().createStatement();

String sql = "WITH Weights_CTE(AverageWeight) AS" +

"(" +

" SELECT [Weight] AS [AverageWeight]" +

" FROM [Production].[Product]" +

" WHERE [Weight] > 0" +

" AND [WeightUnitMeasureCode] = 'LB'" +

" UNION" +

" SELECT [Weight] * 0.00220462262185 AS [AverageWeight]" +

" FROM [Production].[Product]" +

" WHERE [Weight] > 0" +

" AND [WeightUnitMeasureCode] = 'G')" +

"SELECT ROUND(AVG([AverageWeight]), 2)" +

"FROM [Weights_CTE];";

rs = stmt.executeQuery(sql);

if (rs.next()) {

averageWeight = rs.getDouble(1);

}

} catch (Exception ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.SEVERE, null, ex);

} finally {

if (rs != null) {

try {

rs.close();

} catch (SQLException ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.WARNING, null, ex);

}

}

if (stmt != null) {

try {

stmt.close();

} catch (SQLException ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.WARNING, null, ex);

}

}

}

return averageWeight;

}

Example 2: Prepared Statement

Next, we will execute almost the same static SQL statement as in Example 1. The only change is the addition of the column name, averageWeight . This allows us to parse the results by column name, making the code easier to understand as opposed to using the numeric index of the column as in Example 1.

Also, instead of using the java.sql.Statement class, we use the java.sql.PreparedStatement class. According to Oracle’s documentation, a SQL statement is precompiled and stored in a PreparedStatement object. This object can then be used to execute this statement multiple times efficiently.

/**

* PreparedStatement example, no parameters, returns Integer

*

* @return Average weight of all products

*/

public double getAverageProductWeightPS() {

double averageWeight = 0;

PreparedStatement pstmt = null;

ResultSet rs = null; try {

String sql = "WITH Weights_CTE(averageWeight) AS" +

"(" +

" SELECT [Weight] AS [AverageWeight]" +

" FROM [Production].[Product]" +

" WHERE [Weight] > 0" +

" AND [WeightUnitMeasureCode] = 'LB'" +

" UNION" +

" SELECT [Weight] * 0.00220462262185 AS [AverageWeight]" +

" FROM [Production].[Product]" +

" WHERE [Weight] > 0" +

" AND [WeightUnitMeasureCode] = 'G')" +

"SELECT ROUND(AVG([AverageWeight]), 2) AS [averageWeight]" +

"FROM [Weights_CTE];";

pstmt = connection.getConnection().prepareStatement(sql);

rs = pstmt.executeQuery();

if (rs.next()) {

averageWeight = rs.getDouble("averageWeight");

}

} catch (Exception ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.SEVERE, null, ex);

} finally {

if (rs != null) {

try {

rs.close();

} catch (SQLException ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.WARNING, null, ex);

}

}

if (pstmt != null) {

try {

pstmt.close();

} catch (SQLException ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.WARNING, null, ex);

}

}

}

return averageWeight;

}

Example 3: Callable Statement

In this example, the average product weight query has been moved into a stored procedure. The procedure is identical in functionality to the static statement in the first two examples. To call the stored procedure, we use the java.sql.CallableStatement class. According to Oracle’s documentation, the CallableStatement extends PreparedStatement . It is the interface used to execute SQL stored procedures. The CallableStatement accepts both input and output parameters; however, this simple example does not use either. Like the previous two examples, the procedure returns a double numeric value.

CREATE OR

ALTER PROCEDURE [Production].[uspGetAverageProductWeight]

AS

BEGIN

SET NOCOUNT ON; WITH

Weights_CTE(AverageWeight)

AS

(

SELECT [Weight] AS [AverageWeight]

FROM [Production].[Product]

WHERE [Weight] > 0

AND [WeightUnitMeasureCode] = 'LB'

UNION

SELECT [Weight] * 0.00220462262185 AS [AverageWeight]

FROM [Production].[Product]

WHERE [Weight] > 0

AND [WeightUnitMeasureCode] = 'G'

)

SELECT ROUND(AVG([AverageWeight]), 2)

FROM [Weights_CTE];

END GO

The calling Java method is shown below.

/**

* CallableStatement, no parameters, returns Integer

*

* @return Average weight of all products

*/

public double getAverageProductWeightCS() {

CallableStatement cstmt = null;

double averageWeight = 0;

ResultSet rs = null;

try {

cstmt = connection.getConnection().prepareCall(

"{call [Production].[uspGetAverageProductWeight]}");

cstmt.execute();

rs = cstmt.getResultSet();

if (rs.next()) {

averageWeight = rs.getDouble(1);

}

} catch (Exception ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.SEVERE, null, ex);

} finally {

if (rs != null) {

try {

rs.close();

} catch (SQLException ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.SEVERE, null, ex);

}

}

if (cstmt != null) {

try {

cstmt.close();

} catch (SQLException ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.WARNING, null, ex);

}

}

}

return averageWeight;

}

Example 4: Calling a Stored Procedure with an Output Parameter

In this example, we use almost the same stored procedure as in Example 3. The only difference is the inclusion of an output parameter. This time, instead of returning a result set with a value in a single unnamed column, the column has a name, averageWeight . We can now call that column by name when retrieving the value.

The stored procedure patterns found in Examples 3 and 4 are both commonly used. One procedure uses an output parameter, and one not, both return the same values. You can use the CallableStatement to for either type.

CREATE OR

ALTER PROCEDURE [Production].[uspGetAverageProductWeightOUT]

@averageWeight DECIMAL(8, 2) OUT

AS

BEGIN

SET NOCOUNT ON; WITH

Weights_CTE(AverageWeight)

AS

(

SELECT [Weight] AS [AverageWeight]

FROM [Production].[Product]

WHERE [Weight] > 0

AND [WeightUnitMeasureCode] = 'LB'

UNION

SELECT [Weight] * 0.00220462262185 AS [AverageWeight]

FROM [Production].[Product]

WHERE [Weight] > 0

AND [WeightUnitMeasureCode] = 'G'

)

SELECT @averageWeight = ROUND(AVG([AverageWeight]), 2)

FROM [Weights_CTE];

END GO

The calling Java method is shown below.

/**

* CallableStatement example, (1) output parameter, returns Integer

*

* @return Average weight of all products

*/

public double getAverageProductWeightOutCS() {

CallableStatement cstmt = null;

double averageWeight = 0; try {

cstmt = connection.getConnection().prepareCall(

"{call [Production].[uspGetAverageProductWeightOUT](?)}");

cstmt.registerOutParameter("averageWeight", Types.DECIMAL);

cstmt.execute();

averageWeight = cstmt.getDouble("averageWeight");

} catch (Exception ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.SEVERE, null, ex);

} finally {

if (cstmt != null) {

try {

cstmt.close();

} catch (SQLException ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.WARNING, null, ex);

}

}

}

return averageWeight;

}

Example 5: Calling a Stored Procedure with an Input Parameter

In this example, the procedure returns a result set of type java.sql.ResultSet , of employees whose last name starts with a particular sequence of characters (e.g., ‘M’ or ‘Sa’). The sequence of characters is passed as an input parameter, lastNameStartsWith , to the stored procedure using the CallableStatement .

The method making the call iterates through the rows of the result set returned by the stored procedure, concatenating multiple columns to form the employee’s full name as a string. Each full name string is then added to an ordered collection of strings, a List<String> object. The List instance is returned by the method. You will notice this procedure takes a little longer to run because of the use of the LIKE operator. The database server has to perform pattern matching on each last name value in the table to determine the result set.

CREATE OR

ALTER PROCEDURE [HumanResources].[uspGetEmployeesByLastName]

@lastNameStartsWith VARCHAR(20) = 'A'

AS

BEGIN

SET NOCOUNT ON; SELECT p.[FirstName], p.[MiddleName], p.[LastName], p.[Suffix], e.[JobTitle], m.[EmailAddress]

FROM [HumanResources].[Employee] AS e

LEFT JOIN [Person].[Person] p ON e.[BusinessEntityID] = p.[BusinessEntityID]

LEFT JOIN [Person].[EmailAddress] m ON e.[BusinessEntityID] = m.[BusinessEntityID]

WHERE e.[CurrentFlag] = 1

AND p.[PersonType] = 'EM'

AND p.[LastName] LIKE @lastNameStartsWith + '%'

ORDER BY p.[LastName], p.[FirstName], p.[MiddleName]

END GO

The calling Java method is shown below.

/**

* CallableStatement example, (1) input parameter, returns ResultSet

*

* @param lastNameStartsWith

* @return List of employee names

*/

public List<String> getEmployeesByLastNameCS(String lastNameStartsWith) { CallableStatement cstmt = null;

ResultSet rs = null;

List<String> employeeFullName = new ArrayList<>(); try {

cstmt = connection.getConnection().prepareCall(

"{call [HumanResources].[uspGetEmployeesByLastName](?)}",

ResultSet.TYPE_SCROLL_INSENSITIVE,

ResultSet.CONCUR_READ_ONLY); cstmt.setString("lastNameStartsWith", lastNameStartsWith);

boolean results = cstmt.execute();

int rowsAffected = 0; // Protects against lack of SET NOCOUNT in stored procedure

while (results || rowsAffected != -1) {

if (results) {

rs = cstmt.getResultSet();

break;

} else {

rowsAffected = cstmt.getUpdateCount();

}

results = cstmt.getMoreResults();

}

while (rs.next()) {

employeeFullName.add(

rs.getString("LastName") + ", "

+ rs.getString("FirstName") + " "

+ rs.getString("MiddleName"));

}

} catch (Exception ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.SEVERE, null, ex);

} finally {

if (rs != null) {

try {

rs.close();

} catch (SQLException ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.WARNING, null, ex);

}

}

if (cstmt != null) {

try {

cstmt.close();

} catch (SQLException ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.WARNING, null, ex);

}

}

}

return employeeFullName;

}

Example 6: Converting a Result Set to Ordered Collection of Objects

In this last example, we pass two input parameters, productColor and productSize , to a stored procedure. The stored procedure returns a result set containing several columns of product information. This time, the example’s method iterates through the result set returned by the procedure and constructs an ordered collection of products, List<Product> object. The Product objects in the list are instances of the Product.java POJO class. The method converts each results set’s row-level field value into a Product property (e.g., Product.Size , Product.Model ). Using a collection is a common method for persisting data from a result set in an application.

CREATE OR

ALTER PROCEDURE [Production].[uspGetProductsByColorAndSize]

@productColor VARCHAR(20),

@productSize INTEGER

AS

BEGIN

SET NOCOUNT ON; SELECT p.[ProductNumber], m.[Name] AS [Model], p.[Name] AS [Product], p.[Color], p.[Size]

FROM [Production].[ProductModel] AS m

INNER JOIN

[Production].[Product] AS p ON m.[ProductModelID] = p.[ProductModelID]

WHERE (p.[Color] = @productColor)

AND (p.[Size] = @productSize)

ORDER BY p.[ProductNumber], [Model], [Product]

END GO

The calling Java method is shown below.

/**

* CallableStatement example, (2) input parameters, returns ResultSet

*

* @param color

* @param size

* @return List of Product objects

*/

public List<Product> getProductsByColorAndSizeCS(String color, String size) { CallableStatement cstmt = null;

ResultSet rs = null;

List<Product> productList = new ArrayList<>(); try {

cstmt = connection.getConnection().prepareCall(

"{call [Production].[uspGetProductsByColorAndSize](?, ?)}",

ResultSet.TYPE_SCROLL_INSENSITIVE,

ResultSet.CONCUR_READ_ONLY); cstmt.setString("productColor", color);

cstmt.setString("productSize", size);

boolean results = cstmt.execute();

int rowsAffected = 0; // Protects against lack of SET NOCOUNT in stored procedure

while (results || rowsAffected != -1) {

if (results) {

rs = cstmt.getResultSet();

break;

} else {

rowsAffected = cstmt.getUpdateCount();

}

results = cstmt.getMoreResults();

} while (rs.next()) {

Product product = new Product(

rs.getString("Product"),

rs.getString("ProductNumber"),

rs.getString("Color"),

rs.getString("Size"),

rs.getString("Model"));

productList.add(product);

}

} catch (Exception ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.SEVERE, null, ex);

} finally {

if (rs != null) {

try {

rs.close();

} catch (SQLException ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.WARNING, null, ex);

}

}

if (cstmt != null) {

try {

cstmt.close();

} catch (SQLException ex) {

Logger.getLogger(RunExamples.class.getName()).

log(Level.WARNING, null, ex);

}

}

}

return productList;

}

Proper T-SQL: Schema Reference and Brackets

You will notice in all T-SQL statements, I refer to the schema as well as the table or stored procedure name (e.g., {call [Production].[uspGetAverageProductWeightOUT](?)} ). According to Microsoft, it is always good practice to refer to database objects by a schema name and the object name, separated by a period; that even includes the default schema (e.g., dbo ).

You will also notice I wrap the schema and object names in square brackets (e.g., SELECT [ProductNumber] FROM [Production].[ProductModel] ). The square brackets are to indicate that the name represents an object and not a reserved word (e.g, CURRENT or NATIONAL ). By default, SQL Server adds these to make sure the scripts it generates run correctly.

Running the Examples

The application will display the name of the method being called, a method description, the duration of time it took to retrieve the data, and any results returned by the method.

Below, we see the results.

SQL Statement Performance

This post is certainly not about SQL performance, demonstrated by the fact I am only using Amazon RDS for SQL Server 2017 Express Edition on a single, very underpowered db.t2.micro Amazon RDS instance types. However, I have added a timer feature, ProcessTimer.java class, to capture the duration of time each example takes to return data, measured in milliseconds. The ProcessTimer.java class is part of the project code. Using the timer, you should observe significant differences between the first run and proceeding runs of the application for several of the called methods. The time difference is a result of several factors, primarily pre-compilation of the SQL statements and SQL Server plan caching.

The effects of these two factors are easily demonstrated by clearing the SQL Server plan cache (see SQL script below) using DBCC (Database Console Commands) statements. and then running the application twice in a row. The second time, pre-compilation and plan caching should result in significantly faster times for the prepared statements and callable statements, in Examples 2–6. In the two random runs shown below, we see up to a 5x improvement in query time.

USE AdventureWorks; DBCC FREESYSTEMCACHE('SQL Plans');

GO CHECKPOINT;

GO -- Impossible to run with Amazon RDS for Microsoft SQL Server on AWS

-- DBCC DROPCLEANBUFFERS;

-- GO

The first run results are shown below.

SQL SERVER STATEMENT EXAMPLES

======================================

Method: GetAverageProductWeightST

Description: Statement, no parameters, returns Integer

Duration (ms): 122

Results: Average product weight (lb): 12.43

---

Method: GetAverageProductWeightPS

Description: PreparedStatement, no parameters, returns Integer

Duration (ms): 146

Results: Average product weight (lb): 12.43

---

Method: GetAverageProductWeightCS

Description: CallableStatement, no parameters, returns Integer

Duration (ms): 72

Results: Average product weight (lb): 12.43

---

Method: GetAverageProductWeightOutCS

Description: CallableStatement, (1) output parameter, returns Integer

Duration (ms): 623

Results: Average product weight (lb): 12.43

---

Method: GetEmployeesByLastNameCS

Description: CallableStatement, (1) input parameter, returns ResultSet

Duration (ms): 830

Results: Last names starting with 'Sa': 7

Last employee found: Sandberg, Mikael Q

---

Method: GetProductsByColorAndSizeCS

Description: CallableStatement, (2) input parameter, returns ResultSet

Duration (ms): 427

Results: Products found (color: 'Red', size: '44'): 7

First product: Road-650 Red, 44 (BK-R50R-44)

---

The second run results are shown below.

SQL SERVER STATEMENT EXAMPLES

======================================

Method: GetAverageProductWeightST

Description: Statement, no parameters, returns Integer

Duration (ms): 116

Results: Average product weight (lb): 12.43

---

Method: GetAverageProductWeightPS

Description: PreparedStatement, no parameters, returns Integer

Duration (ms): 89

Results: Average product weight (lb): 12.43

---

Method: GetAverageProductWeightCS

Description: CallableStatement, no parameters, returns Integer

Duration (ms): 80

Results: Average product weight (lb): 12.43

---

Method: GetAverageProductWeightOutCS

Description: CallableStatement, (1) output parameter, returns Integer

Duration (ms): 340

Results: Average product weight (lb): 12.43

---

Method: GetEmployeesByLastNameCS

Description: CallableStatement, (1) input parameter, returns ResultSet

Duration (ms): 139

Results: Last names starting with 'Sa': 7

Last employee found: Sandberg, Mikael Q

---

Method: GetProductsByColorAndSizeCS

Description: CallableStatement, (2) input parameter, returns ResultSet

Duration (ms): 208

Results: Products found (color: 'Red', size: '44'): 7

First product: Road-650 Red, 44 (BK-R50R-44)

---

Conclusion

This post has demonstrated several methods for querying and calling stored procedures from a SQL Server 2017 database using JDBC with the Microsoft JDBC Driver 8.4 for SQL Server. Although the examples are quite simple, the same patterns can be used with more complex stored procedures, with multiple input and output parameters, which not only select, but insert, update, and delete data.

There may be some limitations of the Microsoft JDBC Driver for SQL Server you should be aware of by reading the documentation. However, for most tasks that require database interaction, the Driver provides adequate functionality with SQL Server.