generateAccountList(int size){
40 | return range(0, size)
41 | .mapToObj(i -> generateAccount())
42 | .collect(toList());
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/jdbc-account-data/src/main/java/com/bobocode/exception/JdbcAccountUtilException.java:
--------------------------------------------------------------------------------
1 | package com.bobocode.exception;
2 |
3 | public class JdbcAccountUtilException extends RuntimeException {
4 | public JdbcAccountUtilException(String message) {
5 | super(message);
6 | }
7 |
8 | public JdbcAccountUtilException(String message, Throwable cause) {
9 | super(message, cause);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/jdbc-account-data/src/main/java/com/bobocode/model/Account.java:
--------------------------------------------------------------------------------
1 | package com.bobocode.model;
2 |
3 | import lombok.*;
4 |
5 | import java.math.BigDecimal;
6 | import java.time.LocalDate;
7 | import java.time.LocalDateTime;
8 |
9 | @NoArgsConstructor
10 | @Getter
11 | @Setter
12 | @ToString
13 | @EqualsAndHashCode(of = "id")
14 | public class Account {
15 | private Long id;
16 | private String firstName;
17 | private String lastName;
18 | private String email;
19 | private LocalDate birthday;
20 | private Gender gender;
21 | private LocalDateTime creationTime;
22 | private BigDecimal balance = BigDecimal.ZERO;
23 | }
24 |
--------------------------------------------------------------------------------
/jdbc-account-data/src/main/java/com/bobocode/model/Gender.java:
--------------------------------------------------------------------------------
1 | package com.bobocode.model;
2 |
3 | public enum Gender {
4 | UNKNOWN (0),
5 | MALE(1),
6 | FEMALE(2);
7 |
8 | private final int value;
9 |
10 | Gender(int value) {
11 | this.value = value;
12 | }
13 |
14 | public int getValue() {
15 | return value;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/jdbc-account-data/src/main/java/com/bobocode/util/JdbcAccountUtil.java:
--------------------------------------------------------------------------------
1 | package com.bobocode.util;
2 |
3 | import com.bobocode.exception.JdbcAccountUtilException;
4 |
5 | import javax.sql.DataSource;
6 | import java.sql.Connection;
7 | import java.sql.SQLException;
8 | import java.sql.Statement;
9 |
10 | public class JdbcAccountUtil {
11 | private final static String CREATE_ACCOUNT_TABLE_SQL =
12 | "CREATE TABLE IF NOT EXISTS account (\n" +
13 | " id SERIAL NOT NULL,\n" +
14 | " first_name VARCHAR(255) NOT NULL,\n" +
15 | " last_name VARCHAR(255) NOT NULL,\n" +
16 | " email VARCHAR(255) NOT NULL,\n" +
17 | " birthday TIMESTAMP NOT NULL,\n" +
18 | " sex SMALLINT NOT NULL DEFAULT 0 CHECK (sex >= 0 AND sex <=2) ,\n" +
19 | " balance DECIMAL(19, 4),\n" +
20 | " creation_time TIMESTAMP NOT NULL DEFAULT now(),\n" +
21 | " CONSTRAINT account_pk PRIMARY KEY (id)\n" +
22 | ");";
23 |
24 | public static void createAccountTable(DataSource dataSource) {
25 | try (Connection connection = dataSource.getConnection()) {
26 | Statement createTableStatement = connection.createStatement();
27 | createTableStatement.execute(CREATE_ACCOUNT_TABLE_SQL);
28 | } catch (SQLException e) {
29 | throw new JdbcAccountUtilException("Cannot create account table", e);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/jdbc-basics/README.md:
--------------------------------------------------------------------------------
1 | #
JDBC API basics tutorial
2 |
3 | The tutorial on JDBC API essential features and basic configurations
4 |
5 | ### Pre-conditions :heavy_exclamation_mark:
6 | You're supposed to be familiar with SQL and relational databases, have basic knowledge of JDK, and be able to write Java code.
7 | ##
8 | ***JDBC API*** is the only part of *JDK* that provides an ability to **connect to a relational database from Java.**
9 | Since it's just an API, in order to call a real database, you need a specific implementation of that API for each database.
10 | *JDBC API* implementation is called **JDBC Driver**. Each driver is provided by its database vendor.
11 |
12 | The basic flow of working with database is **getting connection, performing SQL query, and getting results.**
13 | Here's the list of most important *JDBC API* classes needed for calling db and getting results:
14 |
15 | JDBC API class | Description
16 | --- | ---
17 | `DataSource` | Represents a concrete database server
18 | `Connection` | Represents a real physical network connection to the db
19 | `Statement` | Represents a SQL query
20 | `ResultSet` | Represents a query result received from the db
21 |
22 | Check out the `SimpleJdbcExample.java` class to see the real working example that uses all of the classes listed above.
23 |
24 | ### Best practices
25 | * use *try-with-resources* to handle database connection
26 | * prefer `PreparedStatement` for *SQL* queries with parameters
27 | * avoid mixing SQL queries with Java code withing one method
28 | * avoid returning JDBC API classes (like `Connection`, or `ResultSet`) form `public` methods
29 |
--------------------------------------------------------------------------------
/jdbc-basics/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | jdbc-api-tutorial
7 | com.bobocode
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | jdbc-basics
13 |
14 |
15 | com.bobocode
16 | jdbc-util
17 | 1.0-SNAPSHOT
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/jdbc-basics/src/main/java/com.bobocode/SimpleJdbcExample.java:
--------------------------------------------------------------------------------
1 | package com.bobocode;
2 |
3 | import com.bobocode.util.JdbcUtil;
4 |
5 | import javax.sql.DataSource;
6 | import java.sql.*;
7 |
8 | public class SimpleJdbcExample {
9 | private static final String CREATE_TABLE_SQL = "CREATE TABLE message(" +
10 | "body VARCHAR(255)," +
11 | "creation_date TIMESTAMP DEFAULT now()" +
12 | ");";
13 | private static final String INSERT_SQL = "INSERT INTO message(body) VALUES (?)";
14 | private static final String SELECT_ALL_SQL = "SELECT * FROM message";
15 |
16 |
17 | private static DataSource dataSource;
18 |
19 | public static void main(String[] args) throws SQLException { // exception handling is omitted
20 | init();
21 | createMessageTable();
22 | saveSomeMessagesIntoDB();
23 | printMessagesFromDB();
24 | }
25 |
26 | private static void init() {
27 | dataSource = JdbcUtil.createDefaultInMemoryH2DataSource();
28 | }
29 |
30 | private static void createMessageTable() throws SQLException {
31 | try (Connection connection = dataSource.getConnection()) {
32 | Statement statement = connection.createStatement();
33 | statement.execute(CREATE_TABLE_SQL);
34 | }
35 |
36 | }
37 |
38 | private static void saveSomeMessagesIntoDB() throws SQLException {
39 | try (Connection connection = dataSource.getConnection()) {
40 | PreparedStatement insertStatement = connection.prepareStatement(INSERT_SQL);
41 | insertSomeMessages(insertStatement);
42 | }
43 | }
44 |
45 | private static void insertSomeMessages(PreparedStatement insertStatement) throws SQLException {
46 | insertStatement.setString(1, "Hello!");
47 | insertStatement.executeUpdate();
48 |
49 | insertStatement.setString(1, "How are you?");
50 | insertStatement.executeUpdate();
51 | }
52 |
53 | private static void printMessagesFromDB() throws SQLException {
54 | //try-with-resource will automatically close Connection resource
55 | try (Connection connection = dataSource.getConnection()) {
56 | Statement statement = connection.createStatement();
57 | ResultSet resultSet = statement.executeQuery(SELECT_ALL_SQL);
58 | printAllMessages(resultSet);
59 | }
60 | }
61 |
62 | private static void printAllMessages(ResultSet resultSet) throws SQLException {
63 | while (resultSet.next()) {
64 | String messageText = resultSet.getString(1);
65 | Timestamp timestamp = resultSet.getTimestamp(2);
66 | System.out.println(" - " + messageText + " [" + timestamp + "]");
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/jdbc-batch-insert/README.md:
--------------------------------------------------------------------------------
1 | #
BATCH INSERT tutorial
2 |
3 | The tutorial on SQL BATCH INSERT using JDBC API
4 |
5 | ### Pre-conditions :heavy_exclamation_mark:
6 | You're supposed to be familiar with SQL and relational databases, and be able to write Java code.
7 | ##
8 |
9 | **Batch insert** it's an operation that allows to **insert more than one row using one INSERT statement.** :star:
10 |
11 | #### SQL
12 | A typical insert sql query looks like this:
13 |
14 | ```sql
15 | INSERT INTO products(name, producer) VALUES('Snickers', 'Mars Inc');
16 | ```
17 |
18 | We usually perform one INSERT statement to store one row. However, in some cases such approach is not efficient :-1:
19 |
20 | **Suppose you need to store large amount of products** at the same time. Like you need to store 100 000 products. It means
21 | that you need to call database 100 000 which is super inefficient, because each SQL query needs to at least go through
22 | the network from the server to the database, and it needs to do it 100 000 times :scream_cat:
23 |
24 | In order to make such operation more efficient relational database and SQL provide an ability to **insert multiple rows
25 | in one INSERT statement.** :thumbsup: The sql query looks like the following:
26 |
27 | ```sql
28 | INSERT INTO products(name, producer) VALUES ('Snickers', 'Mars Inc'), ('Fanta', 'The Coca-Cola company'), ('Bueno', 'Ferrero S.p.A.');
29 | ```
30 |
31 | This approach allow to tremendously reduce the amount of database calls :+1:
32 |
33 | Using batch INSERT you can split all products, and insert them using batches. In case a **batch size is 1000**, the **number
34 | of database calls would be only 100** :smiley_cat:
35 |
36 | #### JDBC API
37 | JDBC API provides two basic methods to perform batch insert:
38 | - `PreparedStatement#addBatch()` that adds new row data to the existing batch
39 | - `Statement#executeBatch()` that calls the database to perform an SQL and insert all data stored in the batch
40 |
41 | Instead of executing query each time
42 | ```java
43 | for (int i = 0; i < PRODUCTS_NUMBER; i++) {
44 | // get prepared statement
45 | // get product
46 | // set prepared statemnt parameters
47 | preparedStatemnt.executeUpdate(); // calls the database
48 | }
49 | ```
50 |
51 | You can add new data to the batch:
52 | ```java
53 | for (int i = 0; i < PRODUCTS_NUMBER; i++) {
54 | // get prepared statement
55 | // get product
56 | // set prepared statemnt parameters
57 | preparedStatemnt.addBatch(); // doesn't call the database
58 |
59 | if (i % BATCH_SIZE == 0) {
60 | preparedStatemnt.executeBatch(); // calls the database
61 | }
62 | }
63 |
64 | // in case batch in not empty at the end of the for loop
65 | if (i % BATCH_SIZE == 0) {
66 | preparedStatemnt.executeBatch(); // calls the database
67 | }
68 | ```
69 |
70 | ### Best practices
71 | * use batch INSERT for saving large amount of data
72 | * always measure performance
73 | * change batch size depending on situation
--------------------------------------------------------------------------------
/jdbc-batch-insert/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | jdbc-api-tutorial
7 | com.bobocode
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | jdbc-batch-insert
13 |
14 |
15 |
16 | com.bobocode
17 | jdbc-account-data
18 | 1.0-SNAPSHOT
19 |
20 |
21 | com.bobocode
22 | jdbc-util
23 | 1.0-SNAPSHOT
24 |
25 |
26 | com.bobocode
27 | jdbc-dao
28 | 1.0-SNAPSHOT
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/jdbc-batch-insert/src/main/java/com/bobocode/AccountBatchInsertExample.java:
--------------------------------------------------------------------------------
1 | package com.bobocode;
2 |
3 | import com.bobocode.exception.AccountBatchInsertException;
4 | import com.bobocode.model.Account;
5 | import com.bobocode.model.Gender;
6 | import com.bobocode.util.JdbcAccountUtil;
7 | import com.bobocode.util.JdbcUtil;
8 | import org.apache.commons.lang3.RandomStringUtils;
9 | import org.apache.commons.lang3.RandomUtils;
10 |
11 | import javax.sql.DataSource;
12 | import java.math.BigDecimal;
13 | import java.sql.Connection;
14 | import java.sql.Date;
15 | import java.sql.PreparedStatement;
16 | import java.sql.SQLException;
17 | import java.time.LocalDate;
18 |
19 | /**
20 | * {@link AccountBatchInsertExample} provides an example of BATCH INSERT using JDBC API.
21 | *
22 | * {@link AccountBatchInsertExample#init()} creates in-memory database.
23 | *
24 | * Then there are two methods that do completely the
25 | * same. Both methods store generated accounts into the database. The first one {@link AccountBatchInsertExample#saveAccountsUsingRegularInsert()}
26 | * is a simple insert that call the database each time to perform INSERT query for each account.
27 | *
28 | * The second one {@link AccountBatchInsertExample#saveAccountsUsingBatchInsert()} stores generated accounts using
29 | * BATCH INSERT. E.g. it creates batch insert query, and sends it to the database every each {@link AccountBatchInsertExample#BATCH_SIZE}
30 | * accounts.
31 | *
32 | * It means that if ACCOUNT_NUMBER = 100_000, and BATCH_SIZE = 1000,
33 | * regular insert will execute 100_000 SQL queries,
34 | * batch insert will execute 100 SQL queries
35 | */
36 | public class AccountBatchInsertExample {
37 | private static DataSource dataSource;
38 | private static final int ACCOUNT_NUMBER = 100_000;
39 | private static final int BATCH_SIZE = 1000;
40 | private static final String ACCOUNT_INSERT_SQL =
41 | "INSERT INTO account(first_name, last_name, email, birthday, sex, balance) VALUES(?,?,?,?,?,?);";
42 |
43 | public static void main(String[] args) throws SQLException {
44 | init();
45 | saveAccountsUsingRegularInsert();
46 | saveAccountsUsingBatchInsert();
47 | }
48 |
49 | public static void init() {
50 | dataSource = JdbcUtil.createDefaultInMemoryH2DataSource();
51 | JdbcAccountUtil.createAccountTable(dataSource);
52 | }
53 |
54 | private static void saveAccountsUsingRegularInsert() throws SQLException {
55 | try (Connection connection = dataSource.getConnection()) {
56 | System.out.printf("Insert %d accounts into the database using regular INSERT: ", ACCOUNT_NUMBER);
57 | Runnable saveAccountUsingRegularInsert = saveAccountsUsingRegularInsertRunnable(connection);
58 | long millis = performCountingTimeInMillis(saveAccountUsingRegularInsert);
59 | System.out.printf("%d ms%n", millis);
60 | }
61 | }
62 |
63 | /**
64 | * Creates a {@link Runnable} that holds the logic of generating and saving accounts using regular insert.
65 | * We need {@link Runnable} to pass it to {@link AccountBatchInsertExample#performCountingTimeInMillis(Runnable)}
66 | * because we want to calculate the execution time.
67 | *
68 | * @return runnable object that does regular insert
69 | */
70 | private static Runnable saveAccountsUsingRegularInsertRunnable(Connection connection) {
71 | return () -> {
72 | try {
73 | PreparedStatement insertStatement = connection.prepareStatement(ACCOUNT_INSERT_SQL);
74 | performRegularInsert(insertStatement);
75 | } catch (SQLException e) {
76 | throw new AccountBatchInsertException("Cannot perform regular account insert", e);
77 | }
78 |
79 | };
80 | }
81 |
82 | private static long performCountingTimeInMillis(Runnable runnable) {
83 | long startingTime = System.nanoTime();
84 | runnable.run();
85 | return (System.nanoTime() - startingTime) / 1_000_000;
86 | }
87 |
88 | private static void performRegularInsert(PreparedStatement insertStatement) throws SQLException {
89 | for (int i = 0; i < ACCOUNT_NUMBER; i++) {
90 | Account account = generateAccount();
91 | fillStatementParameters(insertStatement, account);
92 | insertStatement.executeUpdate(); // on each step we call the database sending INSERT query
93 | }
94 | }
95 |
96 | private static Account generateAccount() {
97 | Account fakeAccount = new Account();
98 | fakeAccount.setFirstName(RandomStringUtils.randomAlphabetic(20));
99 | fakeAccount.setLastName(RandomStringUtils.randomAlphabetic(20));
100 | fakeAccount.setEmail(RandomStringUtils.randomAlphabetic(20));
101 | fakeAccount.setGender(Gender.values()[RandomUtils.nextInt(1, 3)]);
102 | fakeAccount.setBalance(BigDecimal.valueOf(RandomUtils.nextInt(500, 200_000)));
103 | fakeAccount.setBirthday(LocalDate.now().minusDays(RandomUtils.nextInt(6000, 18000)));
104 | return fakeAccount;
105 | }
106 |
107 | private static void fillStatementParameters(PreparedStatement ps, Account account) throws SQLException {
108 | ps.setString(1, account.getFirstName());
109 | ps.setString(2, account.getLastName());
110 | ps.setString(3, account.getEmail());
111 | ps.setDate(4, Date.valueOf(account.getBirthday()));
112 | ps.setInt(5, account.getGender().getValue());
113 | ps.setBigDecimal(6, account.getBalance());
114 | }
115 |
116 | private static void saveAccountsUsingBatchInsert() throws SQLException {
117 | try (Connection connection = dataSource.getConnection()) {
118 | System.out.printf("Insert %d accounts into the database using BATCH INSERT: ", ACCOUNT_NUMBER);
119 | Runnable saveAccountUsingBatch = saveAccountsUsingBatchInsertRunnable(connection);
120 | long millis = performCountingTimeInMillis(saveAccountUsingBatch);
121 | System.out.printf("%d ms%n", millis);
122 | }
123 |
124 | }
125 |
126 | /**
127 | * Creates a {@link Runnable} that holds the logic of generating and saving accounts using batch insert.
128 | * We need {@link Runnable} to pass it to {@link AccountBatchInsertExample#performCountingTimeInMillis(Runnable)}
129 | * because we want to calculate the execution time.
130 | *
131 | * @return runnable object that does regular insert
132 | */
133 | private static Runnable saveAccountsUsingBatchInsertRunnable(Connection connection) {
134 | return () -> {
135 | try {
136 | PreparedStatement insertStatement = connection.prepareStatement(ACCOUNT_INSERT_SQL);
137 | performBatchInsert(insertStatement);
138 | } catch (SQLException e) {
139 | throw new AccountBatchInsertException("Cannot perform account batch insert", e);
140 | }
141 | };
142 | }
143 |
144 | private static void performBatchInsert(PreparedStatement insertStatement) throws SQLException {
145 | for (int i = 1; i <= ACCOUNT_NUMBER; i++) {
146 | Account account = generateAccount();
147 | fillStatementParameters(insertStatement, account);
148 | insertStatement.addBatch(); // on each step we add a new account to the batch (IT DOESN'T CALL THE DATABASE)
149 |
150 | if (i % BATCH_SIZE == 0) { // every BATCH_SIZE accounts
151 | insertStatement.executeBatch(); // we perform the real SQL query to insert all account to the database
152 | }
153 | }
154 | executeRemaining(insertStatement);
155 | }
156 |
157 | private static void executeRemaining(PreparedStatement insertStatement) throws SQLException {
158 | if (ACCOUNT_NUMBER % BATCH_SIZE != 0) {
159 | insertStatement.executeBatch();
160 | }
161 | }
162 |
163 | }
164 |
--------------------------------------------------------------------------------
/jdbc-batch-insert/src/main/java/com/bobocode/exception/AccountBatchInsertException.java:
--------------------------------------------------------------------------------
1 | package com.bobocode.exception;
2 |
3 | public class AccountBatchInsertException extends RuntimeException {
4 | public AccountBatchInsertException(String message) {
5 | super(message);
6 | }
7 |
8 | public AccountBatchInsertException(String message, Throwable cause) {
9 | super(message, cause);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/jdbc-dao/README.md:
--------------------------------------------------------------------------------
1 | #
Data Access Objects (DAO) tutorial
2 |
3 | The tutorial on JDBC API and Data Access Objects
4 |
5 | ### Pre-conditions :heavy_exclamation_mark:
6 | You're supposed to be familiar with SQL and relational databases, have basic knowledge of JDK and JUnit, and be able to write Java code.
7 | ### Related exercises :muscle:
8 | * [Product DAO](https://github.com/bobocode-projects/jdbc-api-exercises/tree/master/product-dao)
9 | ### See also :point_down:
10 | * [Tutorial on JDBC API basics](https://github.com/bobocode-projects/jdbc-api-tutorial/tree/master/jdbc-basics)
11 | ##
12 | ***Data Access Object (DAO)*** is an object that encapsulates all **database configuration, data access and manipulation logic.**
13 | It hides all database queries, and provides a convenient API based on object-oriented model. So you can access and manipulate
14 | data in you business logic using your business objects (models).
15 |
16 | ***A model (entity)*** is a class that represents a **business entity**, **stores data** and **does not contain any business logic.**
17 |
18 |
19 | ### Best practices
20 | * create a separate *DAO* for each *entity (model)*
21 | * separate declaration (interface) and its implementation
22 | * keep all database-related details behind the *DAO*
23 | * wrap database-related exception with custom ones, providing more data and meaningful messages
24 | * avoid mixing *Java* code with *SQL*
25 |
--------------------------------------------------------------------------------
/jdbc-dao/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | jdbc-api-tutorial
7 | com.bobocode
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | jdbc-dao
13 |
14 |
15 |
16 | com.bobocode
17 | jdbc-util
18 | 1.0-SNAPSHOT
19 |
20 |
21 | com.bobocode
22 | jdbc-account-data
23 | 1.0-SNAPSHOT
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/jdbc-dao/src/main/java/com/bobocode/dao/AccountDao.java:
--------------------------------------------------------------------------------
1 | package com.bobocode.dao;
2 |
3 | import com.bobocode.model.Account;
4 |
5 | import java.util.List;
6 |
7 | public interface AccountDao {
8 | void save(Account account);
9 |
10 | Account findOne(Long id);
11 |
12 | List findAll();
13 |
14 | void update(Account account);
15 | }
16 |
--------------------------------------------------------------------------------
/jdbc-dao/src/main/java/com/bobocode/dao/AccountDaoImpl.java:
--------------------------------------------------------------------------------
1 | package com.bobocode.dao;
2 |
3 | import com.bobocode.exception.DaoOperationException;
4 | import com.bobocode.model.Account;
5 | import com.bobocode.model.Gender;
6 |
7 | import javax.sql.DataSource;
8 | import java.sql.*;
9 | import java.util.ArrayList;
10 | import java.util.List;
11 |
12 | public class AccountDaoImpl implements AccountDao {
13 | private final static String INSERT_ACCOUNT_SQL = "INSERT INTO account(first_name, last_name, email, birthday, sex, balance) VALUES(?,?,?,?,?,?);";
14 | private final static String SELECT_ACCOUNT_BY_ID_SQL = "SELECT * FROM account WHERE account.id = ?;";
15 | private final static String SELECT_ALL_ACCOUNTS_SQL = "SELECT * FROM account;";
16 | private final static String UPDATE_ACCOUNT_SQL = "UPDATE account SET first_name =?, last_name = ?, email = ?, birthday = ?, sex = ?, balance = ? WHERE id = ?;";
17 | private DataSource dataSource;
18 |
19 | public AccountDaoImpl(DataSource dataSource) {
20 | this.dataSource = dataSource;
21 | }
22 |
23 | @Override
24 | public void save(Account account) {
25 | try (Connection connection = dataSource.getConnection()) {
26 | saveAccount(account, connection);
27 | } catch (SQLException e) {
28 | throw new DaoOperationException(e.getMessage(), e);
29 | }
30 | }
31 |
32 | private void saveAccount(Account account, Connection connection) throws SQLException {
33 | PreparedStatement insertStatement = prepareInsertStatement(connection, account);
34 | executeUpdate(insertStatement, "Account was not created");
35 | Long id = fetchGeneratedId(insertStatement);
36 | account.setId(id);
37 | }
38 |
39 | private PreparedStatement prepareInsertStatement(Connection connection, Account account) {
40 | try {
41 | PreparedStatement insertStatement = connection.prepareStatement(INSERT_ACCOUNT_SQL, PreparedStatement.RETURN_GENERATED_KEYS);
42 | return fillStatementWithAccountData(insertStatement, account);
43 | } catch (SQLException e) {
44 | throw new DaoOperationException("Cannot prepare statement to insert account", e);
45 | }
46 | }
47 |
48 | private PreparedStatement fillStatementWithAccountData(PreparedStatement insertStatement, Account account)
49 | throws SQLException {
50 | insertStatement.setString(1, account.getFirstName());
51 | insertStatement.setString(2, account.getLastName());
52 | insertStatement.setString(3, account.getEmail());
53 | insertStatement.setDate(4, Date.valueOf(account.getBirthday()));
54 | insertStatement.setInt(5, account.getGender().getValue());
55 | insertStatement.setBigDecimal(6, account.getBalance());
56 | return insertStatement;
57 | }
58 |
59 | private void executeUpdate(PreparedStatement insertStatement, String errorMessage) throws SQLException {
60 | int rowsAffected = insertStatement.executeUpdate();
61 | if (rowsAffected == 0) {
62 | throw new DaoOperationException(errorMessage);
63 | }
64 | }
65 |
66 | private Long fetchGeneratedId(PreparedStatement insertStatement) throws SQLException {
67 | ResultSet generatedKeys = insertStatement.getGeneratedKeys();
68 |
69 | if (generatedKeys.next()) {
70 | return generatedKeys.getLong(1);
71 | } else {
72 | throw new DaoOperationException("Can not obtain an account ID");
73 | }
74 | }
75 |
76 | @Override
77 | public Account findOne(Long id) {
78 | try (Connection connection = dataSource.getConnection()) {
79 | return findAccountById(id, connection);
80 | } catch (SQLException e) {
81 | throw new DaoOperationException(String.format("Cannot find Account by id = %d", id), e);
82 | }
83 | }
84 |
85 | private Account findAccountById(Long id, Connection connection) throws SQLException {
86 | PreparedStatement selectByIdStatement = prepareSelectByIdStatement(id, connection);
87 | ResultSet resultSet = selectByIdStatement.executeQuery();
88 | resultSet.next();
89 | return parseRow(resultSet);
90 | }
91 |
92 | private PreparedStatement prepareSelectByIdStatement(Long id, Connection connection) {
93 | try {
94 | PreparedStatement selectByIdStatement = connection.prepareStatement(SELECT_ACCOUNT_BY_ID_SQL);
95 | selectByIdStatement.setLong(1, id);
96 | return selectByIdStatement;
97 | } catch (SQLException e) {
98 | throw new DaoOperationException("Cannot prepare statement to select account by id", e);
99 | }
100 | }
101 |
102 | private Account parseRow(ResultSet rs) throws SQLException {
103 | Account account = new Account();
104 | account.setId(rs.getLong(1));
105 | account.setFirstName(rs.getString(2));
106 | account.setLastName(rs.getString(3));
107 | account.setEmail(rs.getString(4));
108 | account.setBirthday(rs.getDate(5).toLocalDate());
109 | account.setGender(Gender.values()[rs.getInt(6)]);
110 | account.setBalance(rs.getBigDecimal(7));
111 | account.setCreationTime(rs.getTimestamp(8).toLocalDateTime());
112 | return account;
113 | }
114 |
115 | @Override
116 | public List findAll() {
117 | try (Connection connection = dataSource.getConnection()) {
118 | Statement statement = connection.createStatement();
119 | ResultSet rs = statement.executeQuery(SELECT_ALL_ACCOUNTS_SQL);
120 | return collectToList(rs);
121 | } catch (SQLException e) {
122 | throw new DaoOperationException(e.getMessage());
123 | }
124 | }
125 |
126 | @Override
127 | public void update(Account account) {
128 | try (Connection connection = dataSource.getConnection()) {
129 | PreparedStatement updateStatement = prepareUpdateStatement(account, connection);
130 | executeUpdate(updateStatement, "Account was not updated");
131 | } catch (SQLException e) {
132 | throw new DaoOperationException(String.format("Cannot update Account with id = %d", account.getId()), e);
133 | }
134 | }
135 |
136 | private PreparedStatement prepareUpdateStatement(Account account, Connection connection) {
137 | try {
138 | PreparedStatement updateStatement = connection.prepareStatement(UPDATE_ACCOUNT_SQL);
139 | fillStatementWithAccountData(updateStatement, account);
140 | updateStatement.setLong(7, account.getId());
141 | return updateStatement;
142 | } catch (SQLException e) {
143 | throw new DaoOperationException(String.format("Cannot prepare update statement for account id = %d", account.getId()), e);
144 | }
145 | }
146 |
147 | private List collectToList(ResultSet rs) throws SQLException {
148 | List accountList = new ArrayList<>();
149 | while (rs.next()) {
150 | Account account = parseRow(rs);
151 | accountList.add(account);
152 | }
153 |
154 | return accountList;
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/jdbc-dao/src/main/java/com/bobocode/exception/DaoOperationException.java:
--------------------------------------------------------------------------------
1 | package com.bobocode.exception;
2 |
3 | public class DaoOperationException extends RuntimeException {
4 | public DaoOperationException(String message) {
5 | super(message);
6 | }
7 |
8 | public DaoOperationException(String message, Throwable cause) {
9 | super(message, cause);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/jdbc-dao/src/main/resources/account.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS account (
2 | id SERIAL NOT NULL,
3 | first_name VARCHAR(255) NOT NULL,
4 | last_name VARCHAR(255) NOT NULL,
5 | email VARCHAR(255) NOT NULL,
6 | birthday TIMESTAMP NOT NULL,
7 | balance DECIMAL(19, 4),
8 | creation_time TIMESTAMP NOT NULL DEFAULT now(),
9 |
10 | CONSTRAINT account_pk PRIMARY KEY (id)
11 | );
12 |
13 |
--------------------------------------------------------------------------------
/jdbc-dao/src/test/java/com/bobocode/AccountDaoTest.java:
--------------------------------------------------------------------------------
1 | package com.bobocode;
2 |
3 | import com.bobocode.dao.AccountDao;
4 | import com.bobocode.dao.AccountDaoImpl;
5 | import com.bobocode.data.Accounts;
6 | import com.bobocode.model.Account;
7 | import com.bobocode.util.JdbcUtil;
8 | import org.junit.BeforeClass;
9 | import org.junit.Test;
10 | import org.junit.runner.RunWith;
11 | import org.junit.runners.JUnit4;
12 |
13 | import javax.sql.DataSource;
14 | import java.math.BigDecimal;
15 | import java.sql.Connection;
16 | import java.sql.SQLException;
17 | import java.sql.Statement;
18 | import java.util.List;
19 |
20 | import static org.junit.Assert.assertEquals;
21 | import static org.junit.Assert.assertNotNull;
22 | import static org.junit.Assert.assertTrue;
23 |
24 | @RunWith(JUnit4.class)
25 | public class AccountDaoTest {
26 |
27 | private static AccountDao accountDao;
28 |
29 | @BeforeClass
30 | public static void init() throws SQLException {
31 | DataSource h2DataSource = JdbcUtil.createDefaultInMemoryH2DataSource();
32 | createAccountTable(h2DataSource);
33 | accountDao = new AccountDaoImpl(h2DataSource);
34 | }
35 |
36 | private static void createAccountTable(DataSource dataSource) throws SQLException {
37 | try (Connection connection = dataSource.getConnection()) {
38 | Statement createTableStatement = connection.createStatement();
39 | createTableStatement.execute("CREATE TABLE IF NOT EXISTS account (\n" +
40 | " id SERIAL NOT NULL,\n" +
41 | " first_name VARCHAR(255) NOT NULL,\n" +
42 | " last_name VARCHAR(255) NOT NULL,\n" +
43 | " email VARCHAR(255) NOT NULL,\n" +
44 | " birthday TIMESTAMP NOT NULL,\n" +
45 | " sex TINYINT NOT NULL DEFAULT 0 CHECK (sex >= 0 AND sex <=2) ,\n" +
46 | " balance DECIMAL(19, 4),\n" +
47 | " creation_time TIMESTAMP NOT NULL DEFAULT now(),\n" +
48 | "\n" +
49 | " CONSTRAINT account_pk PRIMARY KEY (id)\n" +
50 | ");\n" +
51 | "\n");
52 | }
53 | }
54 |
55 | @Test
56 | public void testSave() {
57 | Account account = Accounts.generateAccount();
58 |
59 | int accountsCountBeforeInsert = accountDao.findAll().size();
60 | accountDao.save(account);
61 | List accountList = accountDao.findAll();
62 |
63 | assertNotNull(account.getId());
64 | assertEquals(accountsCountBeforeInsert + 1, accountList.size());
65 | assertTrue(accountList.contains(account));
66 | }
67 |
68 | @Test
69 | public void testFindAll() {
70 | List generatedAccounts = Accounts.generateAccountList(10);
71 | generatedAccounts.stream().forEach(accountDao::save);
72 |
73 | List accountList = accountDao.findAll();
74 |
75 | assertTrue(accountList.containsAll(generatedAccounts));
76 | }
77 |
78 | @Test
79 | public void testFindById() {
80 | Account generatedAccount = Accounts.generateAccount();
81 | accountDao.save(generatedAccount);
82 |
83 | Account account = accountDao.findOne(generatedAccount.getId());
84 |
85 | assertEquals(generatedAccount, account);
86 | }
87 |
88 | @Test
89 | public void testUpdate() {
90 | Account generateAccount = Accounts.generateAccount();
91 | accountDao.save(generateAccount);
92 | BigDecimal balanceBeforeUpdate = generateAccount.getBalance();
93 | BigDecimal newBalance = balanceBeforeUpdate.add(BigDecimal.valueOf(15000)).setScale(2);
94 |
95 | generateAccount.setBalance(newBalance);
96 | accountDao.update(generateAccount);
97 | Account account = accountDao.findOne(generateAccount.getId());
98 |
99 | assertEquals(newBalance, generateAccount.getBalance().setScale(2));
100 | assertEquals(newBalance, account.getBalance().setScale(2));
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/jdbc-transaction/README.md:
--------------------------------------------------------------------------------
1 | #
JDBC transaction tutorial
2 |
3 | The tutorial on JDBC API transaction management features
4 |
5 | ### Pre-conditions :heavy_exclamation_mark:
6 | You're supposed to be familiar with SQL and relational databases, have basic knowledge of JDK, and be able to write Java code.
7 | ##
8 |
9 | #### SQL
10 | Relational databases provide different commands to handle transactions. For isntance, PostgreSQL provides three self-descriptive SQL commands:
11 |
12 | ```sql
13 | START TRANSACTION
14 | ```
15 | ```sql
16 | COMMIT
17 | ```
18 | ```sql
19 | ROLLBACK
20 | ```
21 |
22 | **All SQL queries that you perform after transaction started and before it was closed, is done in the scope of one transaction.**
23 |
24 | #### JDBC API
25 | JDBC encapsulates transaction management in class `java.sql.Connection`. **By default, it uses auto-commit mode.** E.g.
26 | all changes are committed automatically when you execute your statement.
27 |
28 | In order, to handle transaction manually you need to **turn off auto-commit mode**.
29 | ```java
30 | connection.setAutoCommit(false);
31 | ```
32 | it will start new transaction, and all following statements will be executed in the scope of that transaction,
33 | until you call one of the following methods:
34 |
35 | ```java
36 | connection.commit();
37 | ```
38 | ```java
39 | connection.rollback();
40 | ```
41 |
42 | ### Best practices
43 | * prefer *try-with-resources* to `final` in order to close the resource
44 | * avoid using `auto-commit` mode
45 | * always catch exception and `rollback` a transaction as early as possible in case of error
46 | * avoid mixing transaction management with other logic
47 |
48 |
--------------------------------------------------------------------------------
/jdbc-transaction/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | jdbc-api-tutorial
7 | com.bobocode
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | jdbc-transaction
13 |
14 |
15 |
16 | com.bobocode
17 | jdbc-util
18 | 1.0-SNAPSHOT
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/jdbc-transaction/src/main/java/com/bobocode/JdbcTransactionManagementExample.java:
--------------------------------------------------------------------------------
1 | package com.bobocode;
2 |
3 | import com.bobocode.exception.JdbcTransactionExampleException;
4 | import com.bobocode.util.JdbcUtil;
5 | import org.apache.commons.lang3.RandomStringUtils;
6 |
7 | import javax.sql.DataSource;
8 | import java.sql.*;
9 |
10 | public class JdbcTransactionManagementExample {
11 | private static final String CREATE_POST_TABLE_SQL = "CREATE TABLE post(id SERIAL PRIMARY KEY, message VARCHAR(255) NOT NULL)";
12 | private static final String INSERT_POST_SQL = "INSERT INTO post(message) VALUES (?)";
13 | private static final String COUNT_POSTS_SQL = "SELECT count(*) FROM post";
14 | private static final int POSTS_TO_INSERT_PER_OPERATION = 10;
15 | private static DataSource dataSource;
16 |
17 | public static void main(String[] args) {
18 | init();
19 | savePostsWithCommit(); // will generate posts insert them into the db, and commit changes
20 | savePostsWithRollBack(); // will generate posts insert them into the db, and revert changes
21 | printNumberOfPostsInThDb();
22 | }
23 |
24 | /**
25 | * Creates default in-memory H2 database and creates post table
26 | */
27 | private static void init() {
28 | dataSource = JdbcUtil.createDefaultInMemoryH2DataSource();
29 | createPostTable();
30 | }
31 |
32 | private static void createPostTable() {
33 | try (Connection connection = dataSource.getConnection()) {
34 | Statement statement = connection.createStatement();
35 | statement.execute(CREATE_POST_TABLE_SQL);
36 | } catch (SQLException e) {
37 | throw new JdbcTransactionExampleException("Error creating post database", e);
38 | }
39 | }
40 |
41 | /**
42 | * Generates posts and inserts it to the database. It turns of auto-commit mode at the beginning. It means that each
43 | * insert statement is performed in the scope of a single transaction. When all statements are executed it commits
44 | * the transaction
45 | */
46 | private static void savePostsWithCommit() {
47 | try (Connection connection = dataSource.getConnection()) {
48 | connection.setAutoCommit(false);
49 | try (PreparedStatement insertStatement = connection.prepareStatement(INSERT_POST_SQL)) {
50 | insertRandomPosts(insertStatement);
51 | }
52 | connection.commit();
53 | connection.setAutoCommit(true);
54 | } catch (SQLException e) {
55 | throw new JdbcTransactionExampleException("Error saving random posts with commit", e);
56 | }
57 | }
58 |
59 | private static void insertRandomPosts(PreparedStatement insertStatement) throws SQLException {
60 | for (int i = 0; i < POSTS_TO_INSERT_PER_OPERATION; i++) {
61 | String randomText = RandomStringUtils.randomAlphabetic(20);
62 | insertStatement.setString(1, randomText);
63 | insertStatement.executeUpdate();
64 | }
65 | }
66 |
67 | /**
68 | * Generates posts and inserts it to the database. It turns of auto-commit mode at the beginning. It means that each
69 | * insert statement is performed in the scope of a single transaction. When all statements are executed it rollbacks
70 | * the transaction. The rollback will revert all changes made in the scope of this transaction, so no posts will
71 | * be stored.
72 | */
73 | private static void savePostsWithRollBack() {
74 | try (Connection connection = dataSource.getConnection()) {
75 | connection.setAutoCommit(false);
76 | try (PreparedStatement insertStatement = connection.prepareStatement(INSERT_POST_SQL)) {
77 | insertRandomPosts(insertStatement);
78 | }
79 | connection.rollback();
80 | } catch (SQLException e) {
81 | throw new JdbcTransactionExampleException("Error saving random posts with rollback", e);
82 | }
83 | }
84 |
85 | /**
86 | * Call the database to get the number of records in the post table. Prints the number of posts
87 | */
88 | private static void printNumberOfPostsInThDb() {
89 | try (Connection connection = dataSource.getConnection()) {
90 | int postsCount = countPosts(connection);
91 | System.out.printf("Number of posts in the database is %d%n", postsCount);
92 | } catch (SQLException e) {
93 | throw new JdbcTransactionExampleException("Error selecting number of posts in the database", e);
94 | }
95 | }
96 |
97 | private static int countPosts(Connection connection) throws SQLException {
98 | Statement statement = connection.createStatement();
99 | ResultSet countResultSet = statement.executeQuery(COUNT_POSTS_SQL);
100 | countResultSet.next();
101 | return countResultSet.getInt(1);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/jdbc-transaction/src/main/java/com/bobocode/exception/JdbcTransactionExampleException.java:
--------------------------------------------------------------------------------
1 | package com.bobocode.exception;
2 |
3 | public class JdbcTransactionExampleException extends RuntimeException {
4 | public JdbcTransactionExampleException(String message) {
5 | super(message);
6 | }
7 |
8 | public JdbcTransactionExampleException(String message, Throwable cause) {
9 | super(message, cause);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/jdbc-util/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | jdbc-api-tutorial
7 | com.bobocode
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | jdbc-util
13 |
14 |
15 |
16 | org.slf4j
17 | slf4j-simple
18 | 1.7.12
19 |
20 |
21 |
--------------------------------------------------------------------------------
/jdbc-util/src/main/java/com/bobocode/util/JdbcUtil.java:
--------------------------------------------------------------------------------
1 | package com.bobocode.util;
2 |
3 | import org.h2.jdbcx.JdbcDataSource;
4 | import org.postgresql.ds.PGSimpleDataSource;
5 |
6 | import javax.sql.DataSource;
7 | import java.util.Map;
8 |
9 | public class JdbcUtil {
10 | static String DEFAULT_DATABASE_NAME = "bobocode_db";
11 | static String DEFAULT_USERNAME = "bobouser";
12 | static String DEFAULT_PASSWORD = "bobodpass";
13 |
14 | public static DataSource createDefaultInMemoryH2DataSource() {
15 | String url = formatH2ImMemoryDbUrl(DEFAULT_DATABASE_NAME);
16 | return createInMemoryH2DataSource(url, DEFAULT_USERNAME, DEFAULT_PASSWORD);
17 | }
18 |
19 | public static DataSource createInMemoryH2DataSource(String url, String username, String pass) {
20 | JdbcDataSource h2DataSource = new JdbcDataSource();
21 | h2DataSource.setUser(username);
22 | h2DataSource.setPassword(pass);
23 | h2DataSource.setUrl(url);
24 |
25 | return h2DataSource;
26 | }
27 |
28 | private static String formatH2ImMemoryDbUrl(String databaseName) {
29 | return String.format("jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false", databaseName);
30 | }
31 |
32 | public static DataSource createDefaultPostgresDataSource() {
33 | String url = formatPostgresDbUrl(DEFAULT_DATABASE_NAME);
34 | return createPostgresDataSource(url, DEFAULT_USERNAME, DEFAULT_PASSWORD);
35 | }
36 |
37 | public static DataSource createPostgresDataSource(String url, String username, String pass) {
38 | PGSimpleDataSource dataSource = new PGSimpleDataSource();
39 | dataSource.setUrl(url);
40 | dataSource.setUser(username);
41 | dataSource.setPassword(pass);
42 | return dataSource;
43 | }
44 |
45 | private static String formatPostgresDbUrl(String databaseName) {
46 | return String.format("jdbc:postgresql://localhost:5432/%s", databaseName);
47 | }
48 |
49 | public static Map getInMemoryDbPropertiesMap() {
50 | return Map.of(
51 | "url", String.format("jdbc:h2:mem:%s", DEFAULT_DATABASE_NAME),
52 | "username", DEFAULT_USERNAME,
53 | "password", DEFAULT_PASSWORD);
54 | }
55 |
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.bobocode
8 | jdbc-api-tutorial
9 | pom
10 | 1.0-SNAPSHOT
11 |
12 | jdbc-basics
13 | jdbc-dao
14 | jdbc-util
15 | jdbc-account-data
16 | jdbc-batch-insert
17 | jdbc-transaction
18 |
19 |
20 |
21 | 1.10
22 | 1.10
23 |
24 |
25 |
26 |
27 | org.projectlombok
28 | lombok
29 | 1.18.0
30 |
31 |
32 | org.postgresql
33 | postgresql
34 | 9.4-1202-jdbc4
35 |
36 |
37 | com.h2database
38 | h2
39 | 1.4.197
40 |
41 |
42 | org.slf4j
43 | slf4j-simple
44 | 1.7.24
45 |
46 |
47 | junit
48 | junit
49 | 4.12
50 |
51 |
52 | org.apache.commons
53 | commons-lang3
54 | 3.8
55 |
56 |
57 | net.ttddyy
58 | datasource-proxy
59 | 1.4.9
60 |
61 |
62 |
--------------------------------------------------------------------------------