现在人们已经认识到Hibernate等ORM有一定的局限性,在CQRS读写分离的架构中,ORM主要用于命令写操作,进行复杂的增删改查CRUD;而SQL用于查询读操作。
Hibernate ORM, jOOQ, HikariCP, Transactions, and S一文分享了他们在自己的NeighborLink项目中如何实践这个原则。
经验如下:
1.使用明确的读写分离. 实体用于CRUD, 简单DTO用于查询结果.
2.读写两边使用同样对象,而不是维护多个模型,经常会在SQL结果中复用实体对象。
2.通过继承实现分层目标,对象的基础层包含对象的基本必要字段,然后分别有实体和DTO继承它。或者使用DTO作为基础,实体继承DTO。
在具体技术上,CRUD采取Hibernate,查询是JOOQ,JOOQ能够以Java方式编写SQL(DSL),而且可以根据数据库中数据表结构自动产生元模型,如果数据表结构变化,产生的元模型会随着改变,这样编译时会出错。元模型和DSL都会以数据库本地语言优化地和数据库打交道。
具体代码如下,前端使用的Spring。 将DataSource 和 HibernateTransactionManager 分享给所有DB交互, 包括ORM, jOOQ, 和直接的JDBC!
Maven配置的库包如下:
<build> <plugins> ... <plugin> <groupId>org.jooq</groupId> <artifactId>jooq-codegen-maven</artifactId> <version>${jooq.version}</version> <executions> <execution> <goals> <goal>generate</goal> </goals> </execution> </executions> <dependencies> <dependency> <groupId>org.jooq</groupId> <artifactId>jooq-codegen</artifactId> <version>${jooq.version}</version> </dependency> </dependencies> <configuration> <jdbc> <driver>com.mysql.jdbc.Driver</driver> <url>jdbc:mysql://[HOST]:[PORT]/[DB NAME]</url> <user>[USER]</user> <password>[PASSWORD]</password> </jdbc> <generator> <name>org.jooq.util.DefaultGenerator</name> <database> <name>org.jooq.util.mysql.MySQLDatabase</name> <includes>.*</includes> <excludes></excludes> <inputSchema>[DB NAME]</inputSchema> </database> <target> <packageName>org.threeriverdev.neighborlink.query</packageName> <directory>target/generated-sources/jooq</directory> </target> </generator> </configuration> </plugin> ... </build>
<dependencies> <!-- Spring framework --> ... <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${spring.version}</version> </dependency> ...
<!-- Hibernate --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency>
<!-- DataSource --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>2.4.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.39</version> </dependency>
<!-- jOOQ --> <dependency> <groupId>org.jooq</groupId> <artifactId>jooq</artifactId> <version>${jooq.version}</version> </dependency> <dependency> <groupId>org.jooq</groupId> <artifactId>jooq-meta</artifactId> <version>${jooq.version}</version> </dependency>
... </dependencies>
|
下面是Spring代码部分:
@Configuration @EnableWebMvc @EnableTransactionManagement @ComponentScan public class AppConfig {
@Autowired private Environment env;
@Autowired private ConfigService configService;
...
@Bean(destroyMethod = "close") public DataSource dataSource() { String dbHost; String dbPort; String dbName; String dbUsername; String dbPassword; if (configService.isProduction()) { // We're in a production environment. dbHost = "localhost"; dbPort = "3306"; dbName = configService.getConfig().getDatabase().getName(); dbUsername = configService.getConfig().getDatabase().getUsername(); dbPassword = configService.getConfig().getDatabase().getPassword(); } else { // Local dev or unit test. dbHost = "[HOST]"; dbPort = "[PORT]"; dbName = "[DB NAME]"; dbUsername = "[USER]"; dbPassword = "[PASSWORD]"; }
final String url = new StringBuilder() .append("jdbc:mysql://") .append(dbHost) .append(":") .append(dbPort) .append("/") .append(dbName) .toString();
HikariDataSource dataSource = new HikariDataSource();
...[HikariCP options]
MysqlDataSource mysqlDataSource = new MysqlDataSource(); mysqlDataSource.setURL(url); mysqlDataSource.setUser(dbUsername); mysqlDataSource.setPassword(dbPassword); ...[MySQL-specific options] dataSource.setDataSource(mysqlDataSource);
return dataSource; }
// HIBERNATE
@Bean public LocalSessionFactoryBean sessionFactory() { LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean(); sessionFactory.setDataSource(dataSource()); sessionFactory.setHibernateProperties(hibernateProperties()); List<String> packages = new ArrayList<String>(); packages.add("org.threeriverdev.neighborlink.entity.core"); ... sessionFactory.setPackagesToScan(packages.toArray(new String[packages.size()]));
return sessionFactory; }
@Bean public HibernateTransactionManager transactionManager() { HibernateTransactionManager txManager = new HibernateTransactionManager(); txManager.setSessionFactory(sessionFactory().getObject()); txManager.setDataSource(dataSource()); return txManager; }
private Properties hibernateProperties() { final Properties properties = new Properties(); ...[Add all "hibernate.*" specific properties -- dialect, etc.] return properties; }
// JOOQ
@Bean public TransactionAwareDataSourceProxy transactionAwareDataSource() { return new TransactionAwareDataSourceProxy(dataSource()); }
@Bean public DataSourceConnectionProvider connectionProvider() { return new DataSourceConnectionProvider(transactionAwareDataSource()); }
@Bean() public DSLContext jooq() { // Generated source assumes the development DB. Dynamically change it to the production DB name. String dbName; if (configService.isProduction()) { // We're in a production environment. dbName = configService.getConfig().getDatabase().getName(); } else { // Local dev or unit test. dbName = "[SANDBOX DB NAME]"; } // The DB name used in the generated DSL (see the Maven plugin use) will not be the same as // the production DB, unless running in a local dev env! withSchemata allows us // to override that during runtime. Settings settings = new Settings().withRenderMapping( new RenderMapping().withSchemata(new MappedSchema().withInput("[SANDBOX DB NAME]").withOutput(dbName))); return DSL.using(connectionProvider(), SQLDialect.MYSQL, settings); } }
|