Java筆記 - Spring - 訪問數據庫

  • 使用JDBC雖然簡單, 但代碼比較繁瑣. Spring簡化了數據庫訪問:
    • 提供了簡化的JDBC的模板類, 不必動手釋放資源;
    • 提供了一個統一的DAO類以實現Data Access Object模式;
    • SQLException封裝爲DataAccessException, 這個異常是一個RuntimeException, 並且能夠讓我們能區分SQL異常的原因;
    • 能方便地集成Hibernate, JPA和MyBatis這些數據庫訪問框架

使用JDBC

  • java使用JDBC訪問數據庫步驟:

    1. 創建全局DataSource實例, 表示數據庫連接池
    2. 通過Connection實例創建PreparedStatement實例
    3. 執行SQL語句, 如果是查詢, 則通過ResultSet讀取結果集, 如果是修改, 獲取int結果
  • 關鍵使用try...finally...釋放資源, 涉及到事務的代碼需要正確提交或回滾事物

  • 在Spring使用JDBC

    1. 首先通過IoC容器創建並管理一個DataSource實例
    2. 然後Spring提供了一個JdbcTemplate, 可以方便地讓我們操作JDBC
    3. 通常情況下, 我們會實例化一個JdbcTemplate. 主要使用了Template模式
@Component
public class UserService {
  @Autowired
  JdbcTemplate jdbcTemplate;

  // 提供了jdbc的`Connection`使用
  public User getUserById(long id) {
    // 傳入ConnectionCallback
    return jdbcTemplate.execute((Connection conn) -> {
      // 可以直接使用Connection實例, 不要釋放, 回調結束後JdbcTemplate自動釋放:
      // 內部手動創建的PreparedStatement, ResultSet必須用try(...)釋放:
      try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
        ps.setObject(1, id);
        try (ResultSet rs = ps.executeQuery()) {
          if (rs.next()) {
            return new User(rs.getLong("id"), rs.getString("email"), rs.getString("password"), rs.getString("name"));
          }
          throw new RuntimeException("user not found by id");
        }
      }
    });
  }

  public User getUserByName(String name) {
    // 需要傳入SQL語句, 以及PreparedStatementCallback
    return jdbcTemplate.execute("SELECT * FROM users WHERE name = ?", (PreparedStatement ps) -> {
      // PreparedStatement實例已經由JdbcTemplate創建, 並在回調後自動釋放:
      ps.setObject(1, name);
      try (ResultSet rs = ps.executeQuery()) {
        if (rs.next()) {
          return new User(rs.getLong("id"), rs.getString("email"), rs.getString("password"), rs.getString("name"));
        }
        throw new RuntimeException("user not found by id");
      }
    });
  }

  public User getUserByEmail(String email) {
    // 傳入SQL, 參數, 和RowMapper實例
    // RowMapper可以返回任何Java對象
    return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email = ?", new Object[] { email },
        (ResultSet rs, int rowNum) -> {
          return new User(rs.getLong("id"), rs.getString("email"), rs.getString("password"), rs.getString("name"));
        });
  }

  // 返回多行記錄
  public List<User> getUsers(int pageIndex) {
    int limit = 100;
    int offset = limit * (pageIndex - 1);
    return jdbcTemplate.query("SELECT * FROM users LIMIT ? OFFSET ?", new Object[] { limit, offset },
        new BeanPropertyRowMapper<>(User.class) // 數據庫結構恰好類似, 可以把一行記錄按照列名轉換爲JavaBean
    );
  }

  // 插入, 更新, 刪除, 需要使用`update()`方法
  public void updateUser(User user) {
    // 傳入SQL, SQL參數, 返回更新的行數
    if (1 != jdbcTemplate.update("UPDATE user SET name = ? WHERE id = ?", user.getName(), user.getId())) {
      throw new RuntimeException("User not found by id");
    }
  }

  // `INSERT`操作比較特殊
  // 如果某一列是自增列, 通常, 需要獲取插入後的自增值.
  // 提供了一個`KeyHolder`簡化操作
  public User register(String email, String password, String name) {
    // 創建一個KeyHolder
    KeyHolder holder = new GeneratedKeyHolder();
    if (1 != jdbcTemplate.update(
        // 參數1: PrepareStatementCreator
        (conn) -> {
          // 創建PreparedStatement時, 必須指定RETURN_GENERATED_KEYS:
          PreparedStatement ps = conn.prepareStatement("INSERT INFO users(email, password, name) VALUES()",
              Statement.RETURN_GENERATED_KEYS);
          ps.setObject(1, email);
          ps.setObject(2, password);
          ps.setObject(3, name);
          return ps;
        },
        // 參數2: KeyHolder
        holder)) {
      throw new RuntimeException("Insert failed.");
    }
    return new User(holder.getKey().longValue(), email, password, name);
  }
}
  • JdbcTemplate還有許多重載方法.

  • 本質是對JDBC操作的一個簡單封裝.

  • 目的:

    1. 減少手動編寫try(resource) {...}
    2. 通過RowMapper實現了JDBC結果集到Java對象的轉換
  • 用法:

    1. 針對簡單查詢, 優選query()queryForObject(), 因爲只需要提供SQL語句, 參數和RowMapper
    2. 針對更新操作, 優選使用update(), 因爲只需要提供SQL語句和參數;
    3. 任何複雜的操作, 最終可以通過execute(ConnectionCallback)實現, 因爲拿到Connection就可以做任何JDBC操作
  • 在設計表結構時, 能夠和JavaBean的屬性一一對應, 直接使用BeanPropertyRowMapper會很方便.

  • 操作時候遇到了一個最大的問題, 就是數據庫有兩條數據, 因爲設置了不唯一主鍵, 插入的時候, 一直衝突, 需要添加一條刪除表的語句

@Component
public class DatabaseInitializer {
  @Autowired
  JdbcTemplate jdbcTemplate;

  @PostConstruct
  public void init() {
    jdbcTemplate.update(" DROP TABLE IF EXISTS users;"
    + "CREATE TABLE IF NOT EXISTS users ( "
    + "id BIGINT IDENTITY NOT NULL PRIMARY KEY, "
    + "email VARCHAR(100) NOT NULL, "
    + "password VARCHAR(100) NOT NULL, "
    + "name VARCHAR(100) NOT NULL, "
    + "UNIQUE (email))"
    );
  }
}

使用聲明式事務

  • Spring提供了一個PlatformTransactionManager表示事務管理器.
  • TransactionStatus表示事務.
    TransactionStatus tx = null;
    try {
      // 開啓事務
      tx = txManager.getTransaction(new DefaultTransactionDefinition());
      // 相關jdbc操作
      jdbcTemplate.update("...");
      jdbcTemplate.update("...");
      // 提交事務
      txManager.commit(tx);
    } catch (Exception e) {
      // 回滾事務
      txManager.rollback(tx);
      throw e;
    }
  • 抽象PlatformTransactionManagerTransactionStatus是爲了支持分佈式事務

  • 分佈式事務指多個數據源(多個數據庫, 多個消息系統)要在分佈式環境下實現事務的時候.

  • 通過一個分佈式事務管理器實現兩階段提交, 但本身數據庫事務就不快, 基於數據庫事務實現的分佈式事務就非常慢, 使用率不高.

  • Spring爲了同時支持JDBC和JTA兩種事務模型, 就抽象出PlatformTransactionManager.

  • Spring使用AOP代理, 即通過自動創建Bean的Proxy實現: 對一個聲明式事務方法的事務支持

  • 聲明瞭@EnableTransactionManager後, 不必額外添加@EnableAspectJAutoProxy

事務回滾

  • 發生了RuntimeException, Spring的聲明式事務將自動回滾.
  • 在一個事務中, 如果程序判斷需要回滾事務, 只需要拋出RuntimeException
@Transactional(rollbackFor = {RuntimeException.class, IoException.class})
public buyProducts(long productId, int num) throws IOException{
  ...
  if (store < num) {
    // 庫存不夠, 購買失效
    throw new IllegalArgumentException("No enough products");
  }
  ...
}
  • 強烈建議業務異常體系從RuntimeException中派生, 這樣就不必聲明任何特殊異常即可讓Spring的聲明式事務正常工作

事務邊界

  • 在使用事務的時候, 明確事務邊界非常重要.
  • 如果一個事務內部, 又調用其他的事務方法, 在回滾的時候, 可能會造成一起回滾的現象.

事務傳播

  • 解決事務邊界問題, 定義事務的傳播類型.

  • Spring的聲明式事務爲事務傳播定義了幾個級別, 默認的傳播級別是REQUIRED.

  • 如果當前沒有事務, 就創建一個新事務, 如果當前有事務, 就加入到當前事務中執行.

  • 這樣整個事務邊界就清晰了: 只有一個事務, 就是UserService.register().

  • 這樣每個事務就都是單獨且清晰的.

  • 事務傳播級別:

    • REQUIRED: 默認, 沒有事務, 就創建一個, 有, 就加入
    • SUPPORTS: 如果有事務, 就加入, 沒有, 自己也不開啓事務執行. 一般用在查詢方法
    • MANDATORY
    • REQUIRES_NEW: 不管當前有沒有, 都必須開啓一個新的事務執行. 如果當前有事務, 那麼當前事務會掛起, 等新事物完成後, 再恢復執行;
    • NOT_SUPPORTED
    • NEVER
    • NOT_SUPPORTED
    • NESTED
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Product createProduct() {
}

Spring如何傳播事務

// jdbc中事務寫法
Connection con = openConnection();
try {
  // 關閉自動提交
  con.setAutoCommit(false)
  // 執行多條SQL語句
  insert();
  update();
  delete();
  // 提交事務
  con.commit();
} catch (SQLException e) {
  // 回滾事務
  con.rollback();
} finally {
  con.setAutoCommit(true)
  con.close();
}
  • 使用ThreadLocal

  • Spring總把JDBC相關的ConnectionTransactionStatus實例綁定到ThreadLocal

  • 如果一個事務方法從ThreadLocal中未取到事務, 那麼它會打開一個新的JDBC鏈接, 同時開啓一個事務.

  • 否則, 就直接從ThreadLocal獲取JDBC鏈接以及TransactionStatus

  • 因此事務支取之前的前提是, 方法調用是在一個線程內執行.

@Transactional
public User register(String email, String password, String name) { // BEGIN TX-A
  User user = jdbcTemplate.insert("...");
  new Thread(() -> {
    // BEGIN TX-B
    bonusService.addBuns(user.id, 100)
    // END TX-B
  }).start();
} // END TX-A
  • 事務只能在當前線程傳播, 無法跨躍線程傳播

使用DAO

  • 傳統的多層應用程序中, 通常是Web層調用業務層, 業務層調用數據訪問層.

  • 業務層負責處理各種業務邏輯, 數據訪問層只負責對數據進行增刪改查.

  • 實現數據訪問層就是用JdbcTemplate實現對數據庫的操作.

  • DAO: Data Access Object

public class AbstractDao<T> extends JdbcDaoSupport{
  private String table;
  private Class<T> entityClass;
  private RowMapper<T> rowMapper;

  @Autowired
  private JdbcTemplate jdbcTemplate;

  @PostConstruct
  public void init() {
    super.setJdbcTemplate(jdbcTemplate);
  }

  public AbstractDao() {
    // 獲取當前類型的泛型類型
    this.entityClass = getParameterizedType();
    this.table = this.entityClass.getSimpleName().toLowerCase() + "s";
    this.rowMapper = new BeanPropertyRowMapper<>(entityClass);
  }

  public T getById(long id) {
    return getJdbcTemplate().queryForObject(
      "SELECT * FROM " + table + " WHERE id = ?",
      this.rowMapper,
      id
    );
  }

  public List<T> getAll(int pageIndex) {
    int limit = 100;
    int offset = limit * (pageIndex - 1);
    return getJdbcTemplate().query(
      "SELECT * FROM " + table + " LIMIT ? OFFSET ?",
      new Object[] {limit, offset},
      this.rowMapper
    );
  }

  public void deleteById(long id) {
    getJdbcTemplate().update("DELETE FROM " + table + " WHERE id = ? ", id);
  }

  public RowMapper<T> getRowMapper() {
    return this.rowMapper;
  }

  private Class<T> getParameterizedType() {
    ...
  }
}

  • 這樣每個子類都會有了這些通用方法
@Component
@Transactional
public class UserDao extends AbstractDao<User> {
  // 已經有了:
  // User getUserById(long)
  // List<User> getAll(int)
  // void deleteById(long)
}

@Component
@Transactional
public class BookDao extends AbstractDao<Book> {
  // 已經有了:
  // Book getById(long)
  // List<Book> getAll(int)
  // void deleteById(long)
}
  • DAO模式是一種簡單的數據訪問模式, 根據實際情況, 是否使用DAO.
  • 直接在Service層操作數據庫也是完全沒有問題的.

集成Hibernate

  • 使用JdbcTemplate的時候, 我們用的最多的方法就是List<T> query(String sql, Object[] args, RowMapper rowMapper)

  • RowMapper的作用: 把ResultSet的一行記錄映射爲Java Bean.

  • 這種關係數據庫的表記錄映射爲Java對象的過程就是ORM: Object-Relational Mapping.

  • ORM可以把記錄轉換爲Java對象, 也可以把Java對下個轉換爲行記錄.

  • Hibernate作爲ORM框架, 可以替代JdbcTemplate, 但仍然需要JDBC驅動.

  • 所以我們需要引入JDBC驅動, 連接池, 已經Hibernate本身.

  • 使用Hibernate時, 不要使用基本類型的屬性, 總是使用包裝類型, 如Long或Integer

  • 使用Spring集成Hibernate, 配合JPA註解, 無需任何額外的XML配置

  • 抽象一層, 可以直接注入通用屬性

@MappedSuperclass // 表示用於繼承
public abstract class AbstractEntity {
  private Long id;
  private Long createdAt;

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(nullable = false, updatable = false)
  public Long getId() {
    return id;
  }

  @Column(nullable = false, updatable = false)
  public Long getCreatedAt() {
    return createdAt;
  }

  @Transient // 表示虛擬屬性, 不從數據庫讀取
  public ZonedDateTime getCreatedDateTime() {
    return Instant.ofEpochMilli(this.createdAt).atZone(ZoneId.systemDefault());
  }

  @PrePersist // 表示JavaBean持久化到數據庫之前(INSERT), 會先執行這個方法.
  public void preInsert() {
    setCreatedAt(System.currentTimeMillis());
  }

  public void setCreatedAt(Long createdAt) {
    this.createdAt = createdAt;
  }

  public void setId(Long id) {
    this.id = id;
  }
}

@Entity
public class Book extends AbstractEntity {
  private String title;

  @Column(nullable = false, updatable = false)
  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  }
}

插入

  public User register(String email, String password, String name) {
    // 創建一個對象
    User user = new User();
    // 設置好屬性
    user.setEmail(email);
    user.setPassword(password);
    user.setName(name);
    // 不用設置id, 因爲設置了自增主鍵, 保存到數據庫
    System.out.print(hibernateTemplate);
    hibernateTemplate.save(user);
    // 現在已經自動獲得了id;
    System.out.println(user.getId());
    return user;
  }

刪除

  public boolean deleteUser(Long id) {
    // 先根據主鍵加載記錄
    // get: 返回null
    // load: 拋出異常
    User user = hibernateTemplate.get(User.class, id);
    if (user != null) {
      hibernateTemplate.delete(user);
      return true;
    }
    return false;
  }

更新

  public void updateUser(Long id, String name) {
    User user = hibernateTemplate.load(User.class, id);
    user.setName(name);
    hibernateTemplate.update(user);
  }

查詢

  • findByExample
  • criteria: 可以實現任意複雜的查詢
  • HQL:
  public User login(String email, String password) {
    User example = new User();
    example.setEmail(email);
    example.setPassword(password);
    List<User> list = hibernateTemplate.findByExample(example);
    // 在使用findByExample時, 基本類型字段總會加入到WHERE條件.
    return list.isEmpty() ? null : list.get(0);
  }

  public User login(String email, String password) {
    DetachedCriteria criteria = DetachedCriteria.forClass(User.class);
    criteria.add(Restrictions.eq("email", email));
    criteria.add(Restrictions.eq("password", password));
    List<User> list = (List<User>) hibernateTemplate.findByCriteria(criteria);
    return list.isEmpty() ? null : list.get(0);
  }


@NamedQueries(
  @NamedQuery(
    name = "login",
    query = "SELECT u FROM User u WHERE u.email=?0 AND u.password=?1"
  )
)
public class User extends AbstractEntity{
  ...
}

  public User login(String email, String password) {
    List<User> list = (List<User>) hibernateTemplate.findByNamedQuery("login", email, password);
    return list.isEmpty() ? null : list.get(0);
  }

使用Hibernate原生接口

  • 原生接口總是從SessionFactory出發, 通常用全局變量存儲.
  • HibernateTemplate中以成員變量注入.
void operation() {
  Session session = null;
  boolean isNew = false;
  // 獲取當前Session或者打開新的Session
  try {
    session = this.sessionFactory.getCurrentSession();
  } catch (HibernateException e) {
    session = this.sessionFactory.openSession();
    isNew = true;
  }
  // 操作Session
  try {
    User user = session.load(User.class, 123L);
  }
  finally {
    // 關閉新打開的Session
    if (isNew) {
      session.close();
    }
  }
}

集成JPA

  • JPA: Java Persistence API, 是ORM標準
  • 如果使用JPA, 引用: javax.persistence, 不再是org.hibernate第三方包
  • JPA只是一個接口, 需要一個實現產品, 例如Hibernate
@Bean
  LocalContainerEntityManagerFactoryBean createEntityManagerFactory(@Autowired DataSource dataSource) {
    LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
    // 設置DataSource
    entityManagerFactoryBean.setDataSource(dataSource);
    // 掃面package
    entityManagerFactoryBean.setPackagesToScan("com.zhangrh.spring.entity");
    // 指定JPA的提供商是Hibernate:
    JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter);
    // 設定特定提供商自己的配置
    Properties props = new Properties();
    props.setProperty("hibernate.hbm2ddl.auto", "update");
    props.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
    props.setProperty("hibernate.show_sql", "true");
    entityManagerFactoryBean.setJpaProperties(props);
    return entityManagerFactoryBean;
  }

  @Bean
  PlatformTransactionManager createTxManager(@Autowired EntityManagerFactory entityManagerFactory) {
    return new JpaTransactionManager(entityManagerFactory);
  }
  • 使用Spring + Hibernate作爲API的實現, 無需任何配置文件.

  • Entity Bean的配置和上一節完全相同, 全部採用Annotation標註.

  • JDBC, Hibernate, JPA關係

    • DataSource SessionFactory EntityManagerFactory
    • Connection Session EntityManager
  • @PersistenceContext // Spring會自動注入EntityManager代理, 該代理類會在必要的時候自動打開EnetityManager

  • 多線程引用的EntityManager雖然是一個代理類, 但該代理類內部針對不同線程會創建不同的EntityManager實例

  • @Persistence的EntityManager可以多線程安全的共享

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<User> q = cb.createQuery(User.class);
    Root<User> r = q.from(User.class);
    q.where(cb.equal(r.get("email"), cb.parameter(String.class, "e")));
    TypedQuery<User> query = em.createQuery(q);
    // 綁定參數
    query.setParameter("e", email);
    // 執行查詢
    List<User> list = query.getResultList();
    return list.isEmpty() ? null : list.get(0);
    // JPQL查詢
    TypedQuery<User> query = em.createQuery("SELECT u FROM User u WHERE u.email = :e", User.class);
    query.setParameter("e", email);
    List<User> list = query.getResultList();
    if (list.isEmpty()) {
      throw new RuntimeException("User not found by email");
    }
    return list.get(0);
  public User login(String email, String password) {
    TypedQuery<User> query = em.createNamedQuery("login", User.class);
    query.setParameter("e", email);
    query.setParameter("p", password);
    List<User> list = query.getResultList();
    return list.isEmpty() ? null : list.get(0);
  }

  public User register(String email, String password, String name) {
    User user = new User();
    user.setEmail(email);
    user.setPassword(password);
    user.setName(name);
    em.persist(user);
    return user;
  }

  public void updateUser(Long id, String name) {
    User user = getUserById(id);
    user.setName(name);
    em.refresh(user);
  }

  public void deleteUser(Long id) {
    User user = getUserById(id);
    em.remove(user);
  }

集成MyBatis

  • ORM框架的主要工作就是把ResultSet的每一行編程Java Bean.
  • 或者把Java Bean自動轉換到INSERT或UPDATE語句的參數中去, 從而實現ORM
  • 因爲我們在Java Bean的屬性上給了足夠的註解作爲元數據
  • ORM獲取Java Bean的註解之後, 知道如何進行映射
  • 通過Proxy模式, 對每個setter方法進行覆寫, 達到update()目的
public class UserProxy extends User{
  Session _session;
  boolean _isNamedChanged;

  public void setName(String name) {
    super.setName(name);
    _isNamedChanged = true;
  }

  // 獲取User對象關聯的Address對象
  public Address getAddress() {
    Query q = _session.createQuery("from Address where userId = :userId");
    q.setParameter("userId", this.getId());
    List<Address> list = query.list();
    return list.isEmpty() ? null : list(0);
  }
}

  • Proxy必須保持當前的Session, 事務提交後, Session自動關閉, 要麼無法訪問, 要麼數據不一致.

  • ORM總是引入Attached/Detached, 表示此Java Bean到底是在Session的範圍內, 還是脫離了Session編程了一個"遊離對象".

  • ORM提供了緩存

    • 一級緩存: 指在一個Session範圍內的緩存, 例如根據主鍵查詢時候, 兩次查詢返回同一個實例
    • 二級緩存: 跨Session緩存, 默認關閉. 二級緩存極大的增加了數據的不一致性
  • JdbcTemplate和ORM相比:

    • 查詢後需要手動提供Mapper實例, 以便把ResultSet的每一行變爲Java對象
    • 增刪改操作所需參數列表, 需要手動傳入, 即把User實例變爲[user.id, user.name, user.email]這樣的列表, 比較麻煩
  • jdbcTemplate

    • 優勢: 確定性, 每次讀取數據庫一定是數據庫操作, 而不是緩存, 所執行的SQL是完全確定的.
    • 缺點: 代碼比較繁瑣, 構造INSERT INTO users VALUES(?,?,?)更加複雜
  • 半自動ORM框架: MyBatis:

    • 只負責ResultSet自動映射到Java Bean
    • 自動填充Java Bean參數
    • 需要自己寫出SQL
  • JDBC | Hibernate | JPA | MyBatis

  • DataSource | SessionFactory | EntityManagerFactory | SqlSessionFactory

  • Connection | Session | EntityManager | SqlSession

  • MyBatis使用Mapper來實現映射.

public interface UserMapper {
  @Select("SELECT * FROM users WHERE id = #{id}")
  User getById(@Param("id") long id);

  @Select("SELECT * FROM users LIMIT #{offset}, #{maxResults}")
  List<User> getAll(@Param("offset") int offset, @Param("maxResults") int maxResults);
}
  • MyBatis執行查詢後, 將根據方法的返回類型自動把ResultSet的每一行轉換爲User實例
  • 轉換規則按照列名和屬性名對應
  • 如果對應不成, 改寫sql語句:
-- 列名: created_time; 屬性名: createdAt
SELECT id, name, email, created_time AS createdAt FROM users
@MapperScan("com.zhangrh.spring.mapper") //自動創建所有mapper的實現類
public class AppConfig {
  // ...
}
public class UserService {
  @Autowired
  UserMapper userMapper;

  public User getUserById(long id) {
    User user = userMapper.getById(id);
    if (user == null) {
      throw new RuntimeException("User not found by id");
    }
    return user;
  }
}

XML配置方式

  • xml可以動態組裝輸出sql, 但是配置繁瑣, 不推薦使用

  • 使用MyBatis最大的問題: 所有的sql全部需要手寫

  • 優點: sql是我們自己寫的, 優化簡單, 可以編寫任意負責sql

  • 切換數據庫不太方便, 但是大部分項目沒有切換數據庫的需求

設計ORM

  • ORM: 建立在JDBC的基礎上, 通過ResultSet到JavaBean的映射, 實現各種查詢.

設計ORM接口

// todo: 不再看了, 暫時達成能用就成. 後面補上.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章