我在 Java 应用程序(使用 H2)中的自动增量列方面遇到了一些问题。主要是,我无法找出从 DAO 访问 ID 的最佳方式:
相关代码部分是: 我有一个表
PEOPLE
,其中包含自动递增 ID
列和字符串 NAME
列:
create table PEOPLE (
ID INTEGER auto_increment,
NAME CHARACTER VARYING(50)
);
PeopleDAO.java
public class PeopleDAO {
public static ObservableList<People> getPeople() throws SQLException, ClassNotFoundException {
String sql = "SELECT * FROM PEOPLE";
try {
PreparedStatement statement = getConnection().prepareStatement(sql);
ResultSet rs = DBHelpers.executeQuerry(statement);
return getPeopleList(rs);
} catch (SQLException e {
throw e
}
}
public static ObservableList<Person> getPeopleList(ResultSet rs) throws SQLException, ClassNotFoundException {
ObservableList<Person> peopleList = FXCollections.observableArrayList();
while (rs.next()) {
Person person = new Person(
rs.getString("NAME");
peopleList.add(person);
}
return peopleList;
}
public static void insertPerson(String name) throws SQLException, ClassNotFoundException {
String sql = "INSERT INTO PEOPLE (NAME) VALUES (?)";
try {
PreparedStatement statement = getConnection().prepareStatement(sql);
statement.setString(1, name);
DBHelpers.executeUpdate(statement);
} catch (SQLException | ClassNotFoundException e) {
throw e;
}
}
}
DBHelpers.java 这有我的驱动程序、URL、连接等。
getConnection()
返回 Connection 对象,并且 executeQuery()
和 exeucteUpdate()
执行他们所说的操作(分别是statement.executeQuery() 和statement.executeUpdate())
Person.java
public class Person {
String name;
public Person(String name) {
this.name = name;
}
}
我的问题是如何从
ID
对象访问 Person
列?我尝试添加该字段
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
int ID;
to
Person
,并为它创建一个 getter,但它为第一个对象返回 0,而不是 1,我认为这与它是一个零索引的对象列表有关。如果它是一个非递增列,我只需在 getPeopleList()
中创建 Person 对象时添加 ID,但由于 ID 是自动分配的,我不确定如何执行此操作。
如果需要,我可以发布更多我的代码。
首先让应用程序运行的基础知识,使用虚拟数据且没有数据库。
让我们定义
Person
类,其中包含两个字段 id
和 name
。
如果类的主要目的是透明地通信浅层不可变数据,请将类定义为记录。
public record Person( int id , String name ) { }
让我们使用常见的 Repository 模式。我们定义一个接口,定义用于存储和检索对象的方法。
public interface Repository
{
public Collection < Person > allPeople ( );
public Optional < Person > addPerson ( String name );
}
进行一个虚假的实现,只是为了让我们的前几个部分正常工作。
public class RepositoryBogus implements Repository
{
private Collection < Person > persons =
new ArrayList <>(
List.of(
new Person( 1 , "Alice" ) ,
new Person( 2 , "Bob" ) ,
new Person( 3 , "Carol" )
) );
@Override
public List < Person > allPeople ( )
{
return List.copyOf( this.persons );
}
@Override
public Optional < Person > addPerson ( final String name )
{
Person person = new Person( ( this.persons.size( ) + 1 ) , name );
this.persons.add( person );
return Optional.of( person );
}
}
编写一个应用程序来练习这些课程。
public class App
{
public static void main ( String[] args )
{
App app = new App( );
app.demo( );
}
private void demo ( )
{
Repository repository = new RepositoryBogus( );
// Before
Collection < Person > persons = repository.allPeople( );
System.out.println( "persons = " + persons );
// After
Optional < Person > optionalDavis = repository.addPerson( "Davis" );
System.out.println( "optionalDavis = " + optionalDavis ); // Verify that Davis got an id value.
System.out.println( "repository.allPeople() = " + repository.allPeople( ) ); // Verify that Davis and his id were stored in database successfully.
}
}
我们的
addPerson
方法会在失败时返回 Optional
。我们必须测试 Optional
来验证是否成功。
运行时,我们看到成功。我们添加“Davis”作为我们的第四个人,返回一个对象,其
id
字段具有预期的值 4
。
persons = [Person[id=1, name=Alice], Person[id=2, name=Bob], Person[id=3, name=Carol]]
optionalDavis = Optional[Person[id=4, name=Davis]]
repository.allPeople() = [Person[id=1, name=Alice], Person[id=2, name=Bob], Person[id=3, name=Carol], Person[id=4, name=Davis]]
现在我们可以正确实现
Repository
来使用 H2 数据库引擎 数据库。
我们最初的实施是最小的:
public class RepositoryH2 implements Repository
{
@Override
public Collection < Person > allPeople ( )
{
return List.of( );
}
@Override
public Optional < Person > addPerson ( final String name )
{
return Optional.empty( );
}
}
再次运行我们相同的应用程序代码。
persons = []
optionalDavis = Optional.empty
repository.allPeople() = []
很好,符合预期。
DataSource
实现来保存我们的数据库凭据(用户名、密码、服务器地址、数据库产品特定设置等)。在实际工作中,我们将通过 JNDI 从命名/目录服务器获取一个。在我们的示例中,我们在与 H2 捆绑的简单实现的对象中伪造了硬编码值:org.h2.jdbcx.JdbcDataSource
。
我们需要在数据库中创建一个表,然后填充它。在实际工作中,我们会使用数据库迁移工具,例如Flyway或Liquibase。在我们的示例中,我们通过在 JDBC 代码中硬编码一些 SQL 来伪造它。
我们在名为
DataSource
的类上的一些静态方法中进行 DatabaseUtils
实例化和表创建/填充。我们还编写了一个方法将表转储到控制台以进行调试。
package work.basil.example.db.autoincrement;
import org.h2.jdbcx.JdbcDataSource;
import org.intellij.lang.annotations.Language;
import javax.sql.DataSource;
import java.sql.*;
import java.util.List;
// The code here is a hack to keep our example simple.
// In real work you would obtain a `DataSource` object via JNDI from a naming/directory server.
// In real work you would create tables by way of a database migration tool such as Flyway or Liquibase.
public class DatabaseUtils
{
public static DataSource makeDatabase ( )
{
DataSource dataSource = DatabaseUtils.fetchDataSource( );
DatabaseUtils.createPersonTable( dataSource );
DatabaseUtils.populatePersonTable( dataSource );
DatabaseUtils.dumpPersonTable( dataSource );
return dataSource;
}
private static DataSource fetchDataSource ( )
{
JdbcDataSource dataSource = new JdbcDataSource( ); // Implementation of `DataSource` bundled with H2.
dataSource.setURL( "jdbc:h2:mem:h2_identity_example_db;DB_CLOSE_DELAY=-1" ); // Set `DB_CLOSE_DELAY` to `-1` to keep in-memory database in existence after connection closes.
dataSource.setUser( "scott" );
dataSource.setPassword( "tiger" );
return dataSource;
}
private static void createPersonTable ( final DataSource dataSource )
{
@Language ( "SQL" )
String sql = """
CREATE TABLE person_
(
id_ INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- ⬅ `identity` = auto-incrementing integer number.
name_ VARCHAR NOT NULL
)
;
""";
try (
Connection connection = dataSource.getConnection( ) ;
Statement statement = connection.createStatement( ) ;
)
{
statement.execute( sql );
} catch ( SQLException e )
{
throw new RuntimeException( e );
}
}
private static void populatePersonTable ( final DataSource dataSource )
{
@Language ( "SQL" )
String sql = """
INSERT INTO person_ ( name_ )
VALUES ( ? )
;
""";
try (
Connection connection = dataSource.getConnection( ) ;
PreparedStatement preparedStatement = connection.prepareStatement( sql ) ;
)
{
for ( String name : List.of( "Alice" , "Bob" , "Carol" ) )
{
preparedStatement.setString( 1 , name );
int rowCount = preparedStatement.executeUpdate( );
if ( rowCount != 1 ) throw new IllegalStateException( "Failed to insert " + name + " into database" );
}
} catch ( SQLException e )
{
throw new RuntimeException( e );
}
}
public static void dumpPersonTable ( final DataSource dataSource )
{
@Language ( "SQL" )
String sql = """
SELECT *
FROM person_
;
""";
try (
Connection connection = dataSource.getConnection( ) ;
Statement statement = connection.createStatement( ) ;
ResultSet resultSet = statement.executeQuery( sql ) ;
)
{
System.out.println( "---------| Start dump of table person_ |--------------------" );
while ( resultSet.next( ) )
{
System.out.println( new Person( resultSet.getInt( "id_" ) , resultSet.getString( "name_" ) ) );
}
System.out.println( "---------| End dump of table person_ |--------------------" );
} catch ( SQLException e )
{
throw new RuntimeException( e );
}
}
}
因此,有了
DataSource
和数据库表,我们就可以充实我们的 Repository
实现。
allPeople
方法与上面看到的dumpPersonTable
方法基本相同。
addPerson
方法是问题的核心:如何通过 JDBC 从 Java 访问数据库引擎中生成的密钥。要实现这一点,请要求 PreparedStatement
将所有生成的密钥返回给 Java。通过在调用 Statement.RETURN_GENERATED_KEYS
准备语句时传递可选参数 Connection#prepareStatement
来实现此目的。
PreparedStatement preparedStatement = connection.prepareStatement( sql , Statement.RETURN_GENERATED_KEYS ) ;
所以这是我们的
RepositoryH2
类,实现我们的 Repository
接口。
package work.basil.example.db.autoincrement;
import org.h2.jdbcx.JdbcDataSource;
import org.intellij.lang.annotations.Language;
import javax.sql.DataSource;
import java.sql.*;
import java.util.*;
public class RepositoryH2 implements Repository
{
// Member fields
private final DataSource dataSource;
// Constructors
public RepositoryH2 ( final DataSource dataSource )
{
this.dataSource = Objects.requireNonNull( dataSource );
}
//------------| Implement Repository |-----------------------------------
@Override
public Collection < Person > allPeople ( )
{
@Language ( "SQL" )
String sql = """
SELECT *
FROM person_
;
""";
try (
Connection connection = dataSource.getConnection( ) ;
Statement statement = connection.createStatement( ) ;
ResultSet resultSet = statement.executeQuery( sql ) ;
)
{
Collection < Person > persons = new ArrayList <>( );
while ( resultSet.next( ) )
{
Person person = new Person( resultSet.getInt( "id_" ) , resultSet.getString( "name_" ) );
persons.add( person );
}
return persons;
} catch ( SQLException e )
{
throw new RuntimeException( e );
}
}
@Override
public Optional < Person > addPerson ( final String name )
{
@Language ( "SQL" )
String sql = """
INSERT INTO person_ ( name_ )
VALUES ( ? )
;
""";
Optional < Person > personOptional = Optional.empty( );
try (
Connection connection = dataSource.getConnection( ) ;
PreparedStatement preparedStatement = connection.prepareStatement( sql , Statement.RETURN_GENERATED_KEYS ) ;
)
{
preparedStatement.setString( 1 , name );
preparedStatement.executeUpdate( );
try (
ResultSet rs = preparedStatement.getGeneratedKeys( ) ;
)
{
while ( rs.next( ) )
{
int id = rs.getInt( 1 ); // Generated key.
Person person = new Person( id , name );
personOptional = Optional.of( person );
}
}
} catch ( SQLException e )
{
throw new RuntimeException( e );
}
return personOptional;
}
}