Guice, JDBC и управление соединениями с базой данных - PullRequest
33 голосов
/ 27 февраля 2010

Я собираюсь создать пример проекта, изучая Guice, который использует JDBC для чтения / записи в базу данных SQL. Однако, после многих лет использования Spring и отвлечения его от обработки соединений и транзакций, я изо всех сил пытаюсь сделать это концептуально.

Я хотел бы иметь службу, которая запускает и останавливает транзакцию и вызывает многочисленные репозитории, которые повторно используют одно и то же соединение и участвуют в одной транзакции. Мои вопросы:

  • Где я могу создать свой источник данных?
  • Как мне предоставить репозиториям доступ к соединению? (ThreadLocal?)
  • Лучший способ управления транзакцией (Создание перехватчика для аннотации?)

Код ниже показывает, как я буду делать это весной. JdbcOperations, внедренный в каждый репозиторий, будет иметь доступ к соединению, связанному с активной транзакцией.

Мне не удалось найти много учебных пособий, которые покрывают это, кроме тех, которые показывают создание перехватчиков для транзакций.

Я рад, что продолжаю использовать Spring, поскольку он очень хорошо работает в моих проектах, но я хотел бы знать, как это сделать в чистом Guice и JBBC (без JPA / Hibernate / Warp / Reusing Spring)

@Service
public class MyService implements MyInterface {

  @Autowired
  private RepositoryA repositoryA;
  @Autowired
  private RepositoryB repositoryB;
  @Autowired
  private RepositoryC repositoryC; 

  @Override
  @Transactional
  public void doSomeWork() {
    this.repositoryA.someInsert();
    this.repositoryB.someUpdate();
    this.repositoryC.someSelect();  
  }    
}

@Repository
public class MyRepositoryA implements RepositoryA {

  @Autowired
  private JdbcOperations jdbcOperations;

  @Override
  public void someInsert() {
    //use jdbcOperations to perform an insert
  }
}

@Repository
public class MyRepositoryB implements RepositoryB {

  @Autowired
  private JdbcOperations jdbcOperations;

  @Override
  public void someUpdate() {
    //use jdbcOperations to perform an update
  }
}

@Repository
public class MyRepositoryC implements RepositoryC {

  @Autowired
  private JdbcOperations jdbcOperations;

  @Override
  public String someSelect() {
    //use jdbcOperations to perform a select and use a RowMapper to produce results
    return "select result";
  }
}

Ответы [ 4 ]

31 голосов
/ 01 марта 2010

Если ваша база данных меняется нечасто, вы можете использовать источник данных, который поставляется с драйвером JDBC базы данных, и изолировать вызовы сторонней библиотеки в поставщике (в моем примере используется источник данных, предоставленный H2ab, но все поставщики JDBC должен иметь один). Если вы переключитесь на другую реализацию DataSource (например, c3PO, Apache DBCP или предоставленную контейнером сервера приложений), вы можете просто написать новую реализацию Provider, чтобы получить источник данных из соответствующего места. Здесь я использую одноэлементную область, чтобы позволить экземпляру DataSource быть разделенным между теми классами, которые зависят от него (необходимо для объединения).

public class DataSourceModule extends AbstractModule {

    @Override
    protected void configure() {
        Names.bindProperties(binder(), loadProperties());

        bind(DataSource.class).toProvider(H2DataSourceProvider.class).in(Scopes.SINGLETON);
        bind(MyService.class);
    }

    static class H2DataSourceProvider implements Provider<DataSource> {

        private final String url;
        private final String username;
        private final String password;

        public H2DataSourceProvider(@Named("url") final String url,
                                    @Named("username") final String username,
                                    @Named("password") final String password) {
            this.url = url;
            this.username = username;
            this.password = password;
        }

        @Override
        public DataSource get() {
            final JdbcDataSource dataSource = new JdbcDataSource();
            dataSource.setURL(url);
            dataSource.setUser(username);
            dataSource.setPassword(password);
            return dataSource;
        }
    }

    static class MyService {
        private final DataSource dataSource;

        @Inject
        public MyService(final DataSource dataSource) {
            this.dataSource = dataSource;
        }

        public void singleUnitOfWork() {

            Connection cn = null;

            try {
                cn = dataSource.getConnection();
                // Use the connection
            } finally {
                try {
                    cn.close();
                } catch (Exception e) {}
            }
        }
    }

    private Properties loadProperties() {
        // Load properties from appropriate place...
        // should contain definitions for:
        // url=...
        // username=...
        // password=...
        return new Properties();
    }
}

Для обработки транзакций должен использоваться источник данных Transaction Aware. Я бы не рекомендовал реализовывать это вручную. Используя что-то вроде warp-persist или контейнерного управления транзакциями, однако это будет выглядеть примерно так:

public class TxModule extends AbstractModule {

    @Override
    protected void configure() {
        Names.bindProperties(binder(), loadProperties());

        final TransactionManager tm = getTransactionManager();

        bind(DataSource.class).annotatedWith(Real.class).toProvider(H2DataSourceProvider.class).in(Scopes.SINGLETON);
        bind(DataSource.class).annotatedWith(TxAware.class).to(TxAwareDataSource.class).in(Scopes.SINGLETON);
        bind(TransactionManager.class).toInstance(tm);
        bindInterceptor(Matchers.any(), Matchers.annotatedWith(Transactional.class), new TxMethodInterceptor(tm));
        bind(MyService.class);
    }

    private TransactionManager getTransactionManager() {
        // Get the transaction manager
        return null;
    }

    static class TxMethodInterceptor implements MethodInterceptor {

        private final TransactionManager tm;

        public TxMethodInterceptor(final TransactionManager tm) {
            this.tm = tm;
        }

        @Override
        public Object invoke(final MethodInvocation invocation) throws Throwable {
            // Start tx if necessary
            return invocation.proceed();
            // Commit tx if started here.
        }
    }

    static class TxAwareDataSource implements DataSource {

        static ThreadLocal<Connection> txConnection = new ThreadLocal<Connection>();
        private final DataSource ds;
        private final TransactionManager tm;

        @Inject
        public TxAwareDataSource(@Real final DataSource ds, final TransactionManager tm) {
            this.ds = ds;
            this.tm = tm;
        }

        public Connection getConnection() throws SQLException {
            try {
                final Transaction transaction = tm.getTransaction();
                if (transaction != null && transaction.getStatus() == Status.STATUS_ACTIVE) {

                    Connection cn = txConnection.get();
                    if (cn == null) {
                        cn = new TxAwareConnection(ds.getConnection());
                        txConnection.set(cn);
                    }

                    return cn;

                } else {
                    return ds.getConnection();
                }
            } catch (final SystemException e) {
                throw new SQLException(e);
            }
        }

        // Omitted delegate methods.
    }

    static class TxAwareConnection implements Connection {

        private final Connection cn;

        public TxAwareConnection(final Connection cn) {
            this.cn = cn;
        }

        public void close() throws SQLException {
            try {
                cn.close();
            } finally {
                TxAwareDataSource.txConnection.set(null);
            }
        }

        // Omitted delegate methods.
    }

    static class MyService {
        private final DataSource dataSource;

        @Inject
        public MyService(@TxAware final DataSource dataSource) {
            this.dataSource = dataSource;
        }

        @Transactional
        public void singleUnitOfWork() {
            Connection cn = null;

            try {
                cn = dataSource.getConnection();
                // Use the connection
            } catch (final SQLException e) {
                throw new RuntimeException(e);
            } finally {
                try {
                    cn.close();
                } catch (final Exception e) {}
            }
        }
    }
}
2 голосов
/ 28 февраля 2010

Я бы использовал что-то вроде c3po для непосредственного создания источников данных. Если вы используете ComboPooledDataSource, вам нужен только экземпляр (объединение выполняется под прикрытием), который вы можете привязать напрямую или через поставщика.

Тогда я бы создал перехватчик, который, например, забирает @Transactional, управляет соединением и фиксирует / откатывает. Вы также можете сделать Connection инъекционным, но вам нужно убедиться, что вы где-то закрыли соединения, чтобы их снова можно было включить в пул.

0 голосов
/ 05 февраля 2015

Пожалуйста, проверьте решение, которое я предоставил: Транзакции с Guice и JDBC - Обсуждение решения

это просто очень простая версия и простой подход. но он прекрасно работает для обработки транзакций с Guice и JDBC.

0 голосов
/ 24 марта 2010
  1. Чтобы внедрить источник данных, вам, вероятно, не нужно привязываться к одному экземпляру источника данных, поскольку база данных, которую вы подключаете к функциям в URL-адресе. Используя Guice, можно заставить программистов предоставлять привязку к реализации DataSource ( link ). Этот источник данных может быть внедрен в ConnectionProvider для возврата источника данных.

  2. Соединение должно быть в локальной области потока. Вы даже можете реализовать свою локальную область потока , но все локальные соединения потока должны быть закрыты и удалены из объекта ThreadLocal после операций фиксации или отката, чтобы предотвратить утечку памяти. После взлома я обнаружил, что вам нужно подключить объект Injector, чтобы удалить элементы ThreadLocal. Инжектор может быть легко введен в ваш перехватчик Guice AOP, примерно так:

    protected  void visitThreadLocalScope(Injector injector, 
                        DefaultBindingScopingVisitor visitor) {
        if (injector == null) {
            return;
        }

        for (Map.Entry, Binding> entry : 
                injector.getBindings().entrySet()) {
            final Binding binding = entry.getValue();
            // Not interested in the return value as yet.
            binding.acceptScopingVisitor(visitor);
        }        
    }

    /**
     * Default implementation that exits the thread local scope. This is 
     * essential to clean up and prevent any memory leakage.
     * 
     * <p>The scope is only visited iff the scope is an sub class of or is an
     * instance of {@link ThreadLocalScope}.
     */
    private static final class ExitingThreadLocalScopeVisitor 
            extends DefaultBindingScopingVisitor {
        @Override
        public Void visitScope(Scope scope) {
                        // ThreadLocalScope is the custom scope.
            if (ThreadLocalScope.class.isAssignableFrom(scope.getClass())) {
                ThreadLocalScope threadLocalScope = (ThreadLocalScope) scope;
                threadLocalScope.exit();
            }
            return null;
        }

    }

Убедитесь, что вы вызываете это после того, как метод был вызван и закрывает соединение. Попробуйте это, чтобы увидеть, если это работает.

...