├── jactiverecord
├── src
│ ├── test
│ │ ├── resources
│ │ │ └── META-INF
│ │ │ │ └── services
│ │ │ │ └── java.sql.Driver
│ │ └── java
│ │ │ └── me
│ │ │ └── zzp
│ │ │ ├── test
│ │ │ ├── TestSuite.java
│ │ │ ├── CountActiveConnection.java
│ │ │ ├── TransactionTest.java
│ │ │ └── CompatibilityTest.java
│ │ │ ├── util
│ │ │ └── SeqTest.java
│ │ │ └── ar
│ │ │ └── sql
│ │ │ └── TSqlBuilderTest.java
│ └── main
│ │ ├── resources
│ │ └── META-INF
│ │ │ └── services
│ │ │ └── me.zzp.ar.d.Dialect
│ │ └── java
│ │ └── me
│ │ └── zzp
│ │ ├── ar
│ │ ├── sql
│ │ │ ├── SqlSyntaxException.java
│ │ │ ├── SqlBuilder.java
│ │ │ ├── package-info.java
│ │ │ ├── TSqlBuilder.java
│ │ │ └── AbstractSqlBuilder.java
│ │ ├── ex
│ │ │ ├── DBOpenException.java
│ │ │ ├── SqlExecuteException.java
│ │ │ ├── TransactionException.java
│ │ │ ├── IllegalTableNameException.java
│ │ │ ├── UnsupportedDatabaseException.java
│ │ │ ├── UndefinedAssociationException.java
│ │ │ ├── IllegalFieldNameException.java
│ │ │ └── package-info.java
│ │ ├── Lambda.java
│ │ ├── d
│ │ │ ├── Dialect.java
│ │ │ ├── MySQLDialect.java
│ │ │ ├── SQLiteDialect.java
│ │ │ ├── PostgreSQLDialect.java
│ │ │ ├── HyperSQLDialect.java
│ │ │ └── package-info.java
│ │ ├── pool
│ │ │ ├── package-info.java
│ │ │ ├── JdbcDataSource.java
│ │ │ ├── SingletonDataSource.java
│ │ │ ├── SingletonConnection.java
│ │ │ ├── SingletonPreparedStatement.java
│ │ │ └── SingletonResultSet.java
│ │ ├── package-info.java
│ │ ├── Query.java
│ │ ├── Association.java
│ │ ├── Record.java
│ │ ├── Table.java
│ │ └── DB.java
│ │ └── util
│ │ ├── package-info.java
│ │ └── Seq.java
├── pom.xml
└── README.md
├── jactioncontroller
├── src
│ └── main
│ │ ├── java
│ │ └── me
│ │ │ └── zzp
│ │ │ └── jac
│ │ │ ├── ex
│ │ │ ├── IllegalPathException.java
│ │ │ └── RedirectionException.java
│ │ │ ├── Controller.java
│ │ │ ├── Dispatcher.java
│ │ │ └── Service.java
│ │ └── resources
│ │ └── META-INF
│ │ └── web-fragment.xml
└── pom.xml
├── jactiverecord-el
├── src
│ └── main
│ │ ├── resources
│ │ └── META-INF
│ │ │ └── web-fragment.xml
│ │ └── java
│ │ └── me
│ │ └── zzp
│ │ └── ar
│ │ └── el
│ │ ├── ResolverSetup.java
│ │ ├── DatabaseSetup.java
│ │ ├── RecordELResolver.java
│ │ └── TableELResolver.java
├── pom.xml
└── README.md
├── java-on-rails
├── src
│ └── main
│ │ └── resources
│ │ └── META-INF
│ │ └── web-fragment.xml
└── pom.xml
├── jactionview
├── src
│ └── main
│ │ ├── resources
│ │ └── META-INF
│ │ │ └── web-fragment.xml
│ │ └── java
│ │ └── me
│ │ └── zzp
│ │ └── jav
│ │ ├── ViewConfig.java
│ │ └── ViewSetup.java
└── pom.xml
├── .gitignore
├── README.md
├── LICENSE
└── pom.xml
/jactiverecord/src/test/resources/META-INF/services/java.sql.Driver:
--------------------------------------------------------------------------------
1 | org.sqlite.JDBC
--------------------------------------------------------------------------------
/jactiverecord/src/main/resources/META-INF/services/me.zzp.ar.d.Dialect:
--------------------------------------------------------------------------------
1 | me.zzp.ar.d.PostgreSQLDialect
2 | me.zzp.ar.d.HyperSQLDialect
3 | me.zzp.ar.d.SQLiteDialect
4 | me.zzp.ar.d.MySQLDialect
--------------------------------------------------------------------------------
/jactioncontroller/src/main/java/me/zzp/jac/ex/IllegalPathException.java:
--------------------------------------------------------------------------------
1 | package me.zzp.jac.ex;
2 |
3 | public final class IllegalPathException extends RuntimeException {
4 | public IllegalPathException(String path) {
5 | super(path);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/sql/SqlSyntaxException.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.sql;
2 |
3 | /**
4 | * 拼装SQL时遇到任何问题,抛出此异常。
5 | *
6 | * @since 1.0
7 | * @author redraiment
8 | */
9 | public class SqlSyntaxException extends RuntimeException {
10 | }
11 |
--------------------------------------------------------------------------------
/jactioncontroller/src/main/java/me/zzp/jac/ex/RedirectionException.java:
--------------------------------------------------------------------------------
1 | package me.zzp.jac.ex;
2 |
3 | public final class RedirectionException extends RuntimeException {
4 | public RedirectionException(String type, String path, Throwable e) {
5 | super(String.format("%s to %s failed", type, path), e);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/ex/DBOpenException.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.ex;
2 |
3 | /**
4 | * 连接数据库时遇到任何问题抛出此异常。
5 | *
6 | * @since 1.0
7 | * @author redraiment
8 | */
9 | public class DBOpenException extends RuntimeException {
10 | public DBOpenException(Throwable cause) {
11 | super(cause);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/ex/SqlExecuteException.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.ex;
2 |
3 | /**
4 | * 执行SQL时遇到任何问题抛出此异常。
5 | *
6 | * @since 1.0
7 | * @author redraiment
8 | */
9 | public class SqlExecuteException extends RuntimeException {
10 | public SqlExecuteException(String sql, Throwable cause) {
11 | super(sql, cause);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/ex/TransactionException.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.ex;
2 |
3 | /**
4 | * 在处理事务时遇到任何异常抛出此异常。
5 | *
6 | * @since 2.0
7 | * @author redraiment
8 | */
9 | public class TransactionException extends RuntimeException {
10 | public TransactionException(String message, Throwable cause) {
11 | super(message, cause);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/jactiverecord/src/test/java/me/zzp/test/TestSuite.java:
--------------------------------------------------------------------------------
1 | package me.zzp.test;
2 |
3 | import me.zzp.ar.sql.TSqlBuilderTest;
4 | import me.zzp.util.SeqTest;
5 | import org.junit.runner.RunWith;
6 | import org.junit.runners.Suite;
7 |
8 | @RunWith(Suite.class)
9 | @Suite.SuiteClasses({
10 | SeqTest.class,
11 | TSqlBuilderTest.class
12 | })
13 | public class TestSuite {
14 | }
15 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/ex/IllegalTableNameException.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.ex;
2 |
3 | /**
4 | * 表不存在。
5 | *
6 | * @since 1.0
7 | * @author redraiment
8 | */
9 | public class IllegalTableNameException extends RuntimeException {
10 | public IllegalTableNameException(String tableName, Throwable e) {
11 | super(String.format("illegal table %s", tableName), e);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/ex/UnsupportedDatabaseException.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.ex;
2 |
3 | /**
4 | * 未找到相应的方言。
5 | *
6 | * @since 1.0
7 | * @author redraiment
8 | */
9 | public class UnsupportedDatabaseException extends RuntimeException {
10 | public UnsupportedDatabaseException(String product) {
11 | super(String.format("Unsupported Database: %s", product));
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/ex/UndefinedAssociationException.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.ex;
2 |
3 | /**
4 | * 遇到未定义的关联时抛出此异常。
5 | *
6 | * @since 1.0
7 | * @author redraiment
8 | */
9 | public class UndefinedAssociationException extends RuntimeException {
10 | public UndefinedAssociationException(String name) {
11 | super(String.format("undefined association name: %s", name));
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/jactiverecord-el/src/main/resources/META-INF/web-fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | me.zzp.ar.el.ResolverSetup
5 |
6 |
7 |
--------------------------------------------------------------------------------
/java-on-rails/src/main/resources/META-INF/web-fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | jactionview-paths
5 | /views
6 |
7 |
8 |
--------------------------------------------------------------------------------
/jactionview/src/main/resources/META-INF/web-fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | me.zzp.jav.ViewSetup
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/ex/IllegalFieldNameException.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.ex;
2 |
3 | /**
4 | * 列不存在。
5 | *
6 | * @since 1.0
7 | * @author redraiment
8 | */
9 | public class IllegalFieldNameException extends RuntimeException {
10 | public IllegalFieldNameException(String fieldName) {
11 | super(String.format("illegal field %s", fieldName));
12 | }
13 |
14 | public IllegalFieldNameException(String fieldName, Throwable cause) {
15 | super(String.format("illegal field %s", fieldName), cause);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Java on Rails
2 |
3 | Yet another Java MVC framework inspired by Ruby on Rails.
4 |
5 | * [jactiverecord](https://github.com/redraiment/java-on-rails/tree/main/jactiverecord): ORM module, implements ActiveRecord pattern in Java.
6 | * [jactiverecord-el](https://github.com/redraiment/java-on-rails/tree/main/jactiverecord-el): JSP expression language (EL) for jActiveRecord, provides Record & Table field accessor.
7 | * [jactioncontroller](https://github.com/redraiment/java-on-rails/tree/main/jactioncontroller): Controller module.
8 | * [jactionview](https://github.com/redraiment/java-on-rails/tree/main/jactionview): View module.
9 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/Lambda.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar;
2 |
3 | import java.lang.reflect.InvocationTargetException;
4 | import java.lang.reflect.Method;
5 | import me.zzp.ar.ex.IllegalFieldNameException;
6 |
7 | final class Lambda {
8 | private final Object o;
9 | private final Method fn;
10 |
11 | Lambda(Object o, Method fn) {
12 | this.o = o;
13 | this.fn = fn;
14 | }
15 |
16 | Object call(Record record, Object value) {
17 | try {
18 | return fn.invoke(o, record, value);
19 | } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
20 | throw new IllegalFieldNameException(fn.getName(), e);
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/d/Dialect.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.d;
2 |
3 | import java.sql.Connection;
4 |
5 | /**
6 | * SQL方言接口。
7 | *
8 | * @since 1.0
9 | * @author redraiment
10 | */
11 | public interface Dialect {
12 | /**
13 | * 判断给定的数据库连接是否使用当前方言。
14 | *
15 | * @param c 数据库连接
16 | * @return 如果该连接属于当前方言,返回true;否则返回false。
17 | */
18 | public boolean accept(Connection c);
19 |
20 | /**
21 | * 返回当前方言定义自增的整数类型主键的方法。
22 | *
23 | * @return 返回当前方言定义自增的整数类型主键的方法。
24 | */
25 | public String getIdentity();
26 |
27 | /**
28 | * 将给定的标识转换成当前数据库内部的大小写形式。
29 | *
30 | * @param identifier 需要转换的标识。
31 | * @return 转换后的标识。
32 | */
33 | public String getCaseIdentifier(String identifier);
34 | }
35 |
--------------------------------------------------------------------------------
/jactioncontroller/src/main/resources/META-INF/web-fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | RESTful Dispatcher
5 | Dispatcher
6 | me.zzp.jac.Dispatcher
7 | 1
8 |
9 |
10 | Dispatcher
11 | /*
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/jactiverecord/src/test/java/me/zzp/test/CountActiveConnection.java:
--------------------------------------------------------------------------------
1 | package me.zzp.test;
2 |
3 | import java.util.Scanner;
4 | import me.zzp.ar.DB;
5 | import me.zzp.ar.Table;
6 | import me.zzp.ar.pool.JdbcDataSource;
7 |
8 | public class CountActiveConnection {
9 | public static void main(String[] args) throws InterruptedException {
10 | DB dbo = DB.open(new JdbcDataSource("jdbc:postgresql:code", "postgres", "123"));
11 | final Table People = dbo.active("person");
12 | for (int i = 0; i < 10; i++) {
13 | Thread th = new Thread(new Runnable() {
14 | @Override
15 | public void run() {
16 | People.first();
17 | }
18 | });
19 | th.start();
20 | th.join();
21 | }
22 | System.out.println("pause");
23 | Scanner cin = new Scanner(System.in);
24 | cin.next();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/jactionview/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 |
6 | me.zzp
7 | java-on-rails
8 | 1.0.0-SNAPSHOT
9 |
10 |
11 | jactionview
12 | 1.0.0-SNAPSHOT
13 | jActionView
14 | jar
15 |
16 |
17 |
18 | jakarta.servlet
19 | jakarta.servlet-api
20 | provided
21 |
22 |
23 |
--------------------------------------------------------------------------------
/jactioncontroller/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 |
6 | me.zzp
7 | java-on-rails
8 | 1.0.0-SNAPSHOT
9 |
10 |
11 | jactioncontroller
12 | 1.0.0-SNAPSHOT
13 | jActionController
14 | jar
15 |
16 |
17 |
18 | jakarta.servlet
19 | jakarta.servlet-api
20 | provided
21 |
22 |
23 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/sql/SqlBuilder.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.sql;
2 |
3 | /**
4 | * SQL语句构造器。
5 | *
6 | * @since 1.0
7 | * @author redraiment
8 | */
9 | public interface SqlBuilder {
10 | public SqlBuilder insert();
11 | public SqlBuilder into(String table);
12 | public SqlBuilder values(String... columns);
13 |
14 | public SqlBuilder update(String table);
15 | public SqlBuilder set(String... columns);
16 |
17 | public SqlBuilder select(String... columns);
18 | public SqlBuilder delete();
19 | public SqlBuilder from(String table);
20 |
21 | public SqlBuilder join(String table);
22 | public SqlBuilder on(String... conditions);
23 |
24 | public SqlBuilder where(String... conditions);
25 |
26 | public SqlBuilder groupBy(String... columns);
27 | public SqlBuilder having(String... conditions);
28 |
29 | public SqlBuilder orderBy(String... columns);
30 |
31 | public SqlBuilder limit(int limit);
32 | public SqlBuilder offset(int offset);
33 |
34 | @Override
35 | public String toString();
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Copyright (c) 2008-2014 Tom Preston-Werner
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the 'Software'), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/d/MySQLDialect.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.d;
2 |
3 | import java.sql.Connection;
4 | import java.sql.DatabaseMetaData;
5 | import java.sql.SQLException;
6 | import me.zzp.ar.ex.DBOpenException;
7 |
8 | /**
9 | * MySQL方言。
10 | *
11 | * @since 1.0
12 | * @author redraiment
13 | */
14 | public class MySQLDialect implements Dialect {
15 | /**
16 | * 判断当前数据库的名称里是否包含mysql(忽略大小写)。
17 | *
18 | * @param c 数据库连接
19 | * @return 如果数据库名称包含mysql,则返回true;否则返回false。
20 | */
21 | @Override
22 | public boolean accept(Connection c) {
23 | try {
24 | DatabaseMetaData d = c.getMetaData();
25 | String name = d.getDatabaseProductName(); // MySQL
26 | return name.toLowerCase().contains("mysql");
27 | } catch (SQLException e) {
28 | throw new DBOpenException(e);
29 | }
30 | }
31 |
32 | /**
33 | * 返回MySQL中定义自增长的整型主键语句。
34 | *
35 | * @return integer primary key auto_increment。
36 | */
37 | @Override
38 | public String getIdentity() {
39 | return "integer primary key auto_increment";
40 | }
41 |
42 | /**
43 | * 原样返回标识。
44 | *
45 | * @param identifier 待转换的标识。
46 | * @return 标识本身。
47 | */
48 | @Override
49 | public String getCaseIdentifier(String identifier) {
50 | return identifier;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/d/SQLiteDialect.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.d;
2 |
3 | import java.sql.Connection;
4 | import java.sql.DatabaseMetaData;
5 | import java.sql.SQLException;
6 | import me.zzp.ar.ex.DBOpenException;
7 |
8 | /**
9 | * SQLite方言。
10 | *
11 | * @since 1.0
12 | * @author redraiment
13 | */
14 | public class SQLiteDialect implements Dialect {
15 | /**
16 | * 判断当前数据库的名称里是否包含sqlite(忽略大小写)。
17 | *
18 | * @param c 数据库连接
19 | * @return 如果数据库名称包含sqlite,则返回true;否则返回false。
20 | */
21 | @Override
22 | public boolean accept(Connection c) {
23 | try {
24 | DatabaseMetaData d = c.getMetaData();
25 | String name = d.getDatabaseProductName(); // SQLite
26 | return name.toLowerCase().contains("sqlite");
27 | } catch (SQLException e) {
28 | throw new DBOpenException(e);
29 | }
30 | }
31 |
32 | /**
33 | * 返回SQLite中定义自增长的整型主键语句。
34 | *
35 | * @return integer primary key autoincrement。
36 | */
37 | @Override
38 | public String getIdentity() {
39 | return "integer primary key autoincrement";
40 | }
41 |
42 | /**
43 | * 原样返回标识。
44 | *
45 | * @param identifier 待转换的标识。
46 | * @return 标识本身。
47 | */
48 | @Override
49 | public String getCaseIdentifier(String identifier) {
50 | return identifier;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/util/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright 2014 redraiment.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | /**
26 | * 工具类集合。
27 | */
28 | package me.zzp.util;
29 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/d/PostgreSQLDialect.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.d;
2 |
3 | import java.sql.Connection;
4 | import java.sql.DatabaseMetaData;
5 | import java.sql.SQLException;
6 | import me.zzp.ar.ex.DBOpenException;
7 |
8 | /**
9 | * PostgreSQL方言。
10 | *
11 | * @since 1.0
12 | * @author redraiment
13 | */
14 | public class PostgreSQLDialect implements Dialect {
15 | /**
16 | * 判断当前数据库的名称里是否包含postgresql(忽略大小写)。
17 | *
18 | * @param c 数据库连接
19 | * @return 如果数据库名称包含postgresql,则返回true;否则返回false。
20 | */
21 | @Override
22 | public boolean accept(Connection c) {
23 | try {
24 | DatabaseMetaData d = c.getMetaData();
25 | String name = d.getDatabaseProductName(); // PostgreSQL
26 | return name.toLowerCase().contains("postgresql");
27 | } catch (SQLException e) {
28 | throw new DBOpenException(e);
29 | }
30 | }
31 |
32 | /**
33 | * 返回PostgreSQL中定义自增长的整型主键语句。
34 | *
35 | * @return serial primary key。
36 | */
37 | @Override
38 | public String getIdentity() {
39 | return "serial primary key";
40 | }
41 |
42 | /**
43 | * 将标识转换成为小写。
44 | *
45 | * @param identifier 待转换的标识。
46 | * @return 小写形式的标识。
47 | */
48 | @Override
49 | public String getCaseIdentifier(String identifier) {
50 | return identifier.toLowerCase();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/sql/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright 2014 redraiment.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | /**
26 | * 用构造器模式实现的SQL语句构造器。
27 | */
28 | package me.zzp.ar.sql;
29 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/pool/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright 2014 redraiment.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | /**
26 | * 提供数据库连接池和多线程相关的工具类。
27 | */
28 | package me.zzp.ar.pool;
29 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright 2014 redraiment.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | /**
26 | * jActiveRecord是Java的ORM框架,灵感来自Ruby on Rails的ActiveRecord。
27 | */
28 | package me.zzp.ar;
29 |
--------------------------------------------------------------------------------
/java-on-rails/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 | me.zzp
5 | raiment
6 | 1.0
7 | jar
8 |
9 |
10 | ${project.groupId}
11 | jactionview
12 | ${project.version}
13 |
14 |
15 | ${project.groupId}
16 | jactioncontroller
17 | ${project.version}
18 |
19 |
20 | ${project.groupId}
21 | jactiverecord-el
22 | 1.2
23 |
24 |
25 |
26 | UTF-8
27 | 1.7
28 | 1.7
29 |
30 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/d/HyperSQLDialect.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.d;
2 |
3 | import java.sql.Connection;
4 | import java.sql.DatabaseMetaData;
5 | import java.sql.SQLException;
6 | import me.zzp.ar.ex.DBOpenException;
7 |
8 | /**
9 | * HyperSQL方言。
10 | *
11 | * @since 1.0
12 | * @author redraiment
13 | */
14 | public class HyperSQLDialect implements Dialect {
15 | /**
16 | * 判断当前数据库的名称里是否包含hsql(忽略大小写)。
17 | *
18 | * @param c 数据库连接
19 | * @return 如果数据库名称包含hsql,则返回true;否则返回false。
20 | */
21 | @Override
22 | public boolean accept(Connection c) {
23 | try {
24 | DatabaseMetaData d = c.getMetaData();
25 | String name = d.getDatabaseProductName(); // HSQL Database Engine
26 | return name.toLowerCase().contains("hsql");
27 | } catch (SQLException e) {
28 | throw new DBOpenException(e);
29 | }
30 | }
31 |
32 | /**
33 | * 返回HyperSQL中定义自增长的整型主键语句。
34 | *
35 | * @return integer primary key generated always as identity (start with 1, increment by 1)。
36 | */
37 | @Override
38 | public String getIdentity() {
39 | return "integer primary key generated always as identity (start with 1, increment by 1)";
40 | }
41 |
42 | /**
43 | * 将标识转换成为大写。
44 | *
45 | * @param identifier 待转换的标识。
46 | * @return 大写形式的标识。
47 | */
48 | @Override
49 | public String getCaseIdentifier(String identifier) {
50 | return identifier.toUpperCase();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/ex/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright 2014 redraiment.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | /**
26 | *
该包统一管理所有异常对象。
27 | * jActiveRecord所有的异常均为RuntimeException,
28 | * 避免了冗余的try {} catch () {}代码。
29 | */
30 | package me.zzp.ar.ex;
31 |
--------------------------------------------------------------------------------
/jactionview/src/main/java/me/zzp/jav/ViewConfig.java:
--------------------------------------------------------------------------------
1 | package me.zzp.jav;
2 |
3 | import java.util.Collections;
4 | import java.util.Enumeration;
5 | import java.util.HashMap;
6 | import java.util.Map;
7 | import javax.servlet.ServletConfig;
8 | import javax.servlet.ServletContext;
9 |
10 | final class ViewConfig implements ServletConfig {
11 | private final ServletContext context;
12 | private final Map params;
13 | private final String name;
14 | private final String url;
15 |
16 | ViewConfig(ServletContext context, String jsp, String prefix) {
17 | this.context = context;
18 | this.name = jsp.substring(prefix.length())
19 | .replaceAll("/", "-")
20 | .replaceAll("\\.jsp", "-view")
21 | .toUpperCase();
22 | this.url = "/".concat(name.toLowerCase());
23 |
24 | params = new HashMap<>();
25 | params.put("jsp-file", jsp);
26 | params.put("jspFile", jsp);
27 | }
28 |
29 | public String getUrl() {
30 | return url;
31 | }
32 |
33 | @Override
34 | public String getServletName() {
35 | return name;
36 | }
37 |
38 | @Override
39 | public ServletContext getServletContext() {
40 | return context;
41 | }
42 |
43 | @Override
44 | public String getInitParameter(String key) {
45 | return params.get(key);
46 | }
47 |
48 | @Override
49 | public Enumeration getInitParameterNames() {
50 | return Collections.enumeration(params.keySet());
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/jactiverecord-el/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 |
6 | me.zzp
7 | java-on-rails
8 | 1.0.0-SNAPSHOT
9 |
10 |
11 | jactiverecord-el
12 | 1.2
13 | jar
14 |
15 | jActiveRecord-EL
16 | Enhance Expression Language in JSP for jActiveRecord
17 |
18 |
19 |
20 | me.zzp
21 | jactiverecord
22 |
23 |
24 | jakarta.servlet
25 | jakarta.servlet-api
26 | provided
27 |
28 |
29 |
30 |
31 |
32 |
33 | maven-source-plugin
34 |
35 |
36 | maven-javadoc-plugin
37 |
38 |
39 | maven-gpg-plugin
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/d/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright 2014 redraiment.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | /**
26 | * 该包提供不同数据库实现的特殊SQL方言。
27 | * 方言采用ServiceLoader机制自动加载,因此需要Java 6及以上版本支持。
28 | * 如果用户需要支持除 SQLite3、HyperSQL、MySQL以及PostgreSQL外的其他数据库,
29 | * 可自行实现me.zzp.ar.d.Dialect,并添加到META-INF/services/me.zzp.ar.d.Dialect
30 | * 文件中。
31 | *
32 | * @since 1.0
33 | */
34 | package me.zzp.ar.d;
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/Query.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar;
2 |
3 | import java.util.List;
4 | import me.zzp.ar.sql.TSqlBuilder;
5 |
6 | /**
7 | * 高级查询对象。
8 | *
9 | * @since 2.0
10 | * @author redraiment
11 | */
12 | public class Query {
13 | private final Table table;
14 | private final TSqlBuilder sql;
15 |
16 | Query(Table table) {
17 | this.table = table;
18 | this.sql = new TSqlBuilder();
19 | }
20 |
21 | public List all(Object... params) {
22 | return table.query(sql, params);
23 | }
24 |
25 | public Record one(Object... params) {
26 | limit(1);
27 | List models = all(params);
28 | if (models == null || models.isEmpty()) {
29 | return null;
30 | } else {
31 | return models.get(0);
32 | }
33 | }
34 |
35 | Query select(String... columns) {
36 | sql.select(columns);
37 | return this;
38 | }
39 |
40 | Query from(String table) {
41 | sql.from(table);
42 | return this;
43 | }
44 |
45 | Query join(String table) {
46 | sql.join(table);
47 | return this;
48 | }
49 |
50 | public Query where(String condition) {
51 | sql.addCondition(condition);
52 | return this;
53 | }
54 |
55 | public Query groupBy(String... columns) {
56 | sql.groupBy(columns);
57 | return this;
58 | }
59 |
60 | public Query having(String... conditions) {
61 | sql.having(conditions);
62 | return this;
63 | }
64 |
65 | public Query orderBy(String... columns) {
66 | sql.orderBy(columns);
67 | return this;
68 | }
69 |
70 | public Query limit(int limit) {
71 | sql.limit(limit);
72 | return this;
73 | }
74 |
75 | public Query offset(int offset) {
76 | sql.offset(offset);
77 | return this;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/jactiverecord/src/test/java/me/zzp/test/TransactionTest.java:
--------------------------------------------------------------------------------
1 | package me.zzp.test;
2 |
3 | import me.zzp.ar.DB;
4 | import me.zzp.ar.Record;
5 | import me.zzp.ar.Table;
6 | import org.junit.After;
7 | import org.junit.Assert;
8 | import org.junit.Before;
9 | import org.junit.Test;
10 |
11 | public class TransactionTest {
12 |
13 | private DB dbo;
14 |
15 | @Before
16 | public void setUp() {
17 | dbo = DB.open("jdbc:sqlite::memory:");
18 | dbo.createTable("tweets", "zombie_id int", "content varchar(64) not null unique");
19 | Table Zombie = dbo.createTable("zombies", "name varchar(64)");
20 | Zombie.hasMany("tweets").by("zombie_id");
21 | }
22 |
23 | @After
24 | public void tearDown() {
25 | if (dbo != null) {
26 | dbo = null;
27 | }
28 | }
29 |
30 | @Test
31 | public void testTx() {
32 | final Table Zombie = dbo.active("zombies");
33 | Table Tweet = dbo.active("tweets");
34 |
35 | dbo.tx(new Runnable() {
36 | @Override
37 | public void run() {
38 | Record ash = Zombie.create("name:", "Ash");
39 | Table tweets = ash.get("tweets");
40 | tweets.create("content:", "Hello world");
41 | }
42 | });
43 | Assert.assertEquals(1, Zombie.all().size());
44 | Assert.assertEquals(1, Tweet.all().size());
45 |
46 | Assert.assertFalse(dbo.tx(new Runnable() {
47 | @Override
48 | public void run() {
49 | Record bob = Zombie.create("name:", "Bob");
50 | Table tweets = bob.get("tweets");
51 | tweets.create("content:", "Hello world");
52 | }
53 | }));
54 | Assert.assertEquals(1, Zombie.all().size());
55 | Assert.assertEquals(1, Tweet.all().size());
56 |
57 | Record ash = Zombie.find(1);
58 | Table tweets = ash.get("tweets");
59 | tweets.create("content:", "Hello ash");
60 | Assert.assertEquals(1, Zombie.all().size());
61 | Assert.assertEquals(2, Tweet.all().size());
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/Association.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar;
2 |
3 | import java.util.Map;
4 | import me.zzp.ar.ex.UndefinedAssociationException;
5 |
6 | /**
7 | * 表之间的关联。
8 | *
9 | * @since 1.0
10 | * @author redraiment
11 | */
12 | public final class Association {
13 | private final Map relations;
14 | private final boolean onlyOne;
15 | private final boolean ancestor;
16 |
17 | private Association assoc;
18 | String target;
19 | String key;
20 |
21 | Association(Map relations, String name, boolean onlyOne, boolean ancestor) {
22 | this.relations = relations;
23 | this.onlyOne = onlyOne;
24 | this.ancestor = ancestor;
25 |
26 | this.target = name;
27 | this.key = name.concat("_id");
28 | this.assoc = null;
29 | }
30 |
31 | public boolean isOnlyOneResult() {
32 | return onlyOne;
33 | }
34 |
35 | public boolean isAncestor() {
36 | return ancestor;
37 | }
38 |
39 | public boolean isCross() {
40 | return assoc != null;
41 | }
42 |
43 | public Association by(String key) {
44 | this.key = key;
45 | return this;
46 | }
47 |
48 | public Association in(String table) {
49 | this.target = table;
50 | return this;
51 | }
52 |
53 | public Association through(String assoc) {
54 | assoc = DB.parseKeyParameter(assoc);
55 | if (relations.containsKey(assoc)) {
56 | this.assoc = relations.get(assoc);
57 | } else {
58 | throw new UndefinedAssociationException(assoc);
59 | }
60 | return this;
61 | }
62 |
63 | String assoc(String source, int id) {
64 | String template = isAncestor()? "%1$s on %2$s.%3$s = %1$s.id": "%1$s on %1$s.%3$s = %2$s.id";
65 | if (isCross()) {
66 | return String.format(template, assoc.target, target, key).concat(" join ").concat(assoc.assoc(source, id));
67 | } else {
68 | return String.format(template.concat(" and %1$s.id = %4$d"), source, target, key, id);
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/jactionview/src/main/java/me/zzp/jav/ViewSetup.java:
--------------------------------------------------------------------------------
1 | package me.zzp.jav;
2 |
3 | import java.util.LinkedList;
4 | import java.util.List;
5 | import java.util.Set;
6 | import javax.servlet.ServletContext;
7 | import javax.servlet.ServletContextEvent;
8 | import javax.servlet.ServletContextListener;
9 | import javax.servlet.ServletException;
10 | import javax.servlet.ServletRegistration.Dynamic;
11 | import org.apache.jasper.servlet.JspServlet;
12 |
13 | public class ViewSetup implements ServletContextListener {
14 |
15 | private List listJsp(ServletContext context, String root) {
16 | List files = new LinkedList<>();
17 | Set paths = context.getResourcePaths(root);
18 | if (paths != null) {
19 | for (String path : paths) {
20 | if (path.endsWith(".jsp")) {
21 | files.add(path);
22 | } else if (path.endsWith("/")) {
23 | files.addAll(listJsp(context, path));
24 | }
25 | }
26 | }
27 | return files;
28 | }
29 |
30 | @Override
31 | public void contextInitialized(ServletContextEvent e) {
32 | ServletContext context = e.getServletContext();
33 | String paths = context.getInitParameter("jactionview-paths");
34 | if (paths == null || paths.isEmpty()) {
35 | return;
36 | }
37 |
38 | for (String root : paths.split(",")) {
39 | if (!root.endsWith("/")) {
40 | root = root.concat("/");
41 | }
42 |
43 | for (String file : listJsp(context, root)) {
44 | ViewConfig view = new ViewConfig(context, file, root);
45 | try {
46 | JspServlet jsp = context.createServlet(JspServlet.class);
47 | jsp.init(view);
48 | Dynamic mapping = context.addServlet(view.getServletName(), jsp);
49 | mapping.addMapping(view.getUrl());
50 | } catch (ServletException ex) {
51 | System.err.println(ex.getMessage());
52 | }
53 | }
54 | }
55 | }
56 |
57 | @Override
58 | public void contextDestroyed(ServletContextEvent e) {
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/jactiverecord/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 |
6 | me.zzp
7 | java-on-rails
8 | 1.0.0-SNAPSHOT
9 |
10 |
11 | jactiverecord
12 | 2.3
13 | jar
14 |
15 | jActiveRecord
16 | ActiveRecord of Ruby on Rails in Java
17 |
18 |
19 |
20 | org.junit.jupiter
21 | junit-jupiter
22 | test
23 |
24 |
25 | com.h2database
26 | h2
27 | test
28 |
29 |
30 | org.xerial
31 | sqlite-jdbc
32 | test
33 |
34 |
35 | org.postgresql
36 | postgresql
37 | test
38 |
39 |
40 | mysql
41 | mysql-connector-java
42 | test
43 |
44 |
45 |
46 |
47 |
48 |
49 | maven-source-plugin
50 |
51 |
52 | maven-javadoc-plugin
53 |
54 |
55 | maven-gpg-plugin
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/jactiverecord-el/src/main/java/me/zzp/ar/el/ResolverSetup.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.el;
2 |
3 | import javax.servlet.ServletContext;
4 | import javax.servlet.ServletContextEvent;
5 | import javax.servlet.ServletContextListener;
6 | import javax.servlet.jsp.JspApplicationContext;
7 | import javax.servlet.jsp.JspFactory;
8 |
9 | /**
10 | * 加载自定义的EL表达式解析器。
11 | * 1. 欲使用jActiveRecord-EL,需要在web.xml添加此监听器:
12 | *
13 | * <listener>
14 | <listener-class>me.zzp.ar.el.ResolverSetup</listener-class>
15 | </listener>
16 | *
17 | * 之后在EL表达式中就能像操作普通JavaBean一样操作 {@link me.zzp.ar.Table}
18 | * 和 {@link me.zzp.ar.Record}。
19 | * 2. jActiveRecord-EL会自动将骆驼法命名的属性转换为下划线命名,
20 | * 欲停止这个特性,可在web.xml中添加上下文参数:
21 | *
22 | * <context-param>
23 | <param-name>jactiverecord-el-camel-case</param-name>
24 | <param-value>false</param-value>
25 | </context-param>
26 | *
27 | * 开启了该选项之后,${user.createdAt}等价于${user.created_at}。
28 | * @author redraiment
29 | * @since 1.0
30 | * @see TableELResolver
31 | * @see RecordELResolver
32 | */
33 | public class ResolverSetup implements ServletContextListener {
34 | /**
35 | * 加载TableELResolver和RecordELResolver。
36 | * @param e 上下文事件对象
37 | */
38 | @Override
39 | public void contextInitialized(ServletContextEvent e) {
40 | ServletContext context = e.getServletContext();
41 | String param = context.getInitParameter("jactiverecord-el-camel-case");
42 | boolean camelCase = !"false".equalsIgnoreCase(param);
43 |
44 | JspFactory factory = JspFactory.getDefaultFactory();
45 | JspApplicationContext resolvers = factory.getJspApplicationContext(context);
46 | resolvers.addELResolver(new TableELResolver());
47 | resolvers.addELResolver(new RecordELResolver(camelCase));
48 | }
49 |
50 | /**
51 | * 什么都没做。
52 | * @param e 上下文事件对象
53 | */
54 | @Override
55 | public void contextDestroyed(ServletContextEvent e) {
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/jactioncontroller/src/main/java/me/zzp/jac/Controller.java:
--------------------------------------------------------------------------------
1 | package me.zzp.jac;
2 |
3 | import java.util.ArrayList;
4 | import java.util.HashMap;
5 | import java.util.List;
6 | import java.util.Map;
7 | import java.util.regex.Matcher;
8 | import java.util.regex.Pattern;
9 | import me.zzp.jac.ex.IllegalPathException;
10 |
11 | final class Controller {
12 | private final Pattern pattern;
13 | private final List names;
14 |
15 | Controller(String path) {
16 | names = new ArrayList<>();
17 |
18 | StringBuilder regex = new StringBuilder();
19 | int count = 0;
20 | int from = -1;
21 | boolean inside = false;
22 | for (int i = 0; i < path.length(); i++) {
23 | char c = path.charAt(i);
24 | switch (c) {
25 | case '{': {
26 | if (count == 0) {
27 | from = i + 1;
28 | inside = true;
29 | }
30 | count++;
31 | } break;
32 | case '}': {
33 | count--;
34 | if (count == 0) {
35 | String[] values = path.substring(from, i).split(":", 2);
36 | names.add(values[0]);
37 | if (values.length == 1) {
38 | regex.append("([^/]+?)");
39 | } else {
40 | regex.append('(').append(values[1]).append(')');
41 | }
42 | inside = false;
43 | } else if (count < 0) {
44 | throw new IllegalPathException(path);
45 | }
46 | } break;
47 | default: {
48 | if (!inside) {
49 | regex.append(c);
50 | if (c == '\\') {
51 | i++;
52 | regex.append(path.charAt(i));
53 | }
54 | }
55 | } break;
56 | }
57 | }
58 |
59 | pattern = Pattern.compile(regex.toString());
60 | }
61 |
62 | Map parse(String path) {
63 | Matcher matcher = pattern.matcher(path);
64 | if (matcher.matches() && matcher.groupCount() == names.size()) {
65 | Map params = new HashMap<>();
66 | for (int i = 0; i < matcher.groupCount(); i++) {
67 | params.put(names.get(i), matcher.group(i + 1));
68 | }
69 | return params;
70 | } else {
71 | return null;
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/sql/TSqlBuilder.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.sql;
2 |
3 | import java.util.Arrays;
4 | import me.zzp.util.Seq;
5 |
6 | /**
7 | * TSQL构造器。
8 | *
9 | * @since 1.0
10 | * @author redraiment
11 | */
12 | public class TSqlBuilder extends AbstractSqlBuilder {
13 | @Override
14 | public SqlBuilder insert() {
15 | return start(Mode.Insert);
16 | }
17 |
18 | @Override
19 | public SqlBuilder into(String table) {
20 | return setTables(table);
21 | }
22 |
23 | @Override
24 | public SqlBuilder values(String... columns) {
25 | return setFields(columns);
26 | }
27 |
28 | @Override
29 | public SqlBuilder update(String table) {
30 | start(Mode.Update);
31 | return setTables(table);
32 | }
33 |
34 | @Override
35 | public SqlBuilder set(String... columns) {
36 | return setFields(columns);
37 | }
38 |
39 | @Override
40 | public SqlBuilder select(String... columns) {
41 | start(Mode.Select);
42 | if (columns != null && columns.length > 0) {
43 | return setFields(columns);
44 | } else {
45 | return setFields("*");
46 | }
47 | }
48 |
49 | @Override
50 | public SqlBuilder delete() {
51 | return start(Mode.Delete);
52 | }
53 |
54 | @Override
55 | public SqlBuilder from(String table) {
56 | return setTables(table);
57 | }
58 |
59 | @Override
60 | public SqlBuilder join(String table) {
61 | return addTable(table);
62 | }
63 |
64 | @Override
65 | public SqlBuilder on(String... conditions) {
66 | String table = tables.removeLast();
67 | return addTable(table.concat(" on ").concat(Seq.join(Arrays.asList(conditions), "and")));
68 | }
69 |
70 | @Override
71 | public SqlBuilder where(String... conditions) {
72 | return setConditions(conditions);
73 | }
74 |
75 | @Override
76 | public SqlBuilder groupBy(String... columns) {
77 | return setGroups(columns);
78 | }
79 |
80 | @Override
81 | public SqlBuilder having(String... conditions) {
82 | return setHaving(conditions);
83 | }
84 |
85 | @Override
86 | public SqlBuilder orderBy(String... columns) {
87 | return setOrders(columns);
88 | }
89 |
90 | @Override
91 | public SqlBuilder limit(int limit) {
92 | return setLimit(limit);
93 | }
94 |
95 | @Override
96 | public SqlBuilder offset(int offset) {
97 | return setOffset(offset);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/jactiverecord-el/src/main/java/me/zzp/ar/el/DatabaseSetup.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.el;
2 |
3 | import javax.naming.InitialContext;
4 | import javax.naming.NamingException;
5 | import javax.servlet.ServletContext;
6 | import javax.servlet.ServletContextEvent;
7 | import javax.servlet.ServletContextListener;
8 | import javax.sql.DataSource;
9 | import me.zzp.ar.DB;
10 |
11 | /**
12 | * 在上下文属性中添加数据库对象。
13 | * 1. 欲自动生成数据库对象,需要在web.xml添加此监听器:
14 | *
15 | * <listener>
16 | <listener-class>me.zzp.ar.el.DatabaseSetup</listener-class>
17 | </listener>
18 | *
19 | * 同时添加上下文参数,指定数据源的路径:
20 | *
21 | * <context-param>
22 | <param-name>jactiverecord-el-data-source</param-name>
23 | <param-value>java:/comp/env/jdbc/DataSource</param-value>
24 | </context-param>
25 | *
26 | * 此时,在Servlet中调用 getServletContext().getAttribute("dbo")
27 | * 就能获得{@link me.zzp.ar.DB}对象。
28 | * 2. 其中“dbo”为默认的属性名,要修改这个名字可通过在web.xml中添加上下文参数:
29 | *
30 | * <context-param>
31 | <param-name>jactiverecord-el-attribute-name</param-name>
32 | <param-value>database</param-value>
33 | </context-param>
34 | *
35 | * 这样,属性名就改成了database。
36 | * @author redraiment
37 | * @since 1.1
38 | * @see me.zzp.ar.DB
39 | */
40 | public class DatabaseSetup implements ServletContextListener {
41 | /**
42 | * 初始化数据库对象。
43 | * @param e 上下文事件对象
44 | */
45 | @Override
46 | public void contextInitialized(ServletContextEvent e) {
47 | ServletContext context = e.getServletContext();
48 | String path = context.getInitParameter("jactiverecord-el-data-source");
49 | if (path == null || path.isEmpty()) {
50 | return;
51 | }
52 | String name = context.getInitParameter("jactiverecord-el-attribute-name");
53 | if (name == null || name.isEmpty()) {
54 | name = "dbo";
55 | }
56 |
57 | try {
58 | DataSource pool = InitialContext.doLookup(path);
59 | DB dbo = DB.open(pool);
60 | context.setAttribute(name, dbo);
61 | } catch (NamingException ex) {
62 | System.err.println(ex.getMessage());
63 | }
64 | }
65 |
66 | /**
67 | * 什么都没做。
68 | * @param e 上下文事件对象
69 | */
70 | @Override
71 | public void contextDestroyed(ServletContextEvent e) {
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/jactiverecord-el/README.md:
--------------------------------------------------------------------------------
1 | # jActiveRecord-EL
2 |
3 | `jActiveRecord-EL`是[jActiveRecord](https://github.com/redraiment/jactiverecord)的辅助项目,简化在EL表达式中访问数据的方法,做到像操作普通`JavaBean`一样操作`Record`和`Table`类型的对象。适合采用了`jActiveRecord`的`Web`项目。
4 |
5 | * 项目主页:[http://github.com/redraiment/jactiverecord-el](http://github.com/redraiment/jactiverecord-el)
6 | * javadoc:[http://zzp.me/jactiverecord-el/](http://zzp.me/jactiverecord-el/)
7 | * jActiveRecord:[http://github.com/redraiment/jactiverecord](http://github.com/redraiment/jactiverecord)
8 |
9 | `jActiveRecord-EL`同样使用`Maven`管理,在`pom.xml`中添加如下依赖即可:
10 |
11 | ```xml
12 |
13 | me.zzp
14 | jactiverecord-el
15 | 1.2
16 |
17 | ```
18 |
19 | # 访问Record属性
20 |
21 | 假设`Record`实例`user`有一个字符串类型的属性`name`,如果不使用`jActiveRecord-EL`,要在EL表达式中获得该属性的值,方法是:
22 |
23 | ```xml
24 | ${user.get("name")}
25 | ```
26 |
27 | 采用`jActiveRecord-EL`之后,方法是:
28 |
29 | ```xml
30 | ${user.name}
31 | ```
32 |
33 | `jActiveRecord-EL`简化了在EL表达式中访问`Record`属性的方法,能像访问`JavaBean`属性一样地访问`Record`的数据。
34 |
35 | # 访问Table方法
36 |
37 | `jActiveRecord-EL`同样简化了访问`Table`对象的方法,支持`all`、`first`、`last`和索引四种查询方式:
38 |
39 | * `all`:调用`Table#all()`。即`${User.all}`等价于`${User.all()}`
40 | * `first`:调用`Table#first()`。即`${User.first}`等价于`${User.first()}`
41 | * `last`:调用`Table#last()`。即`${User.last}`等价于`${User.last()}`
42 | * `索引`:调用`Table#find(int id)`。即`${User[1]}`等价于`${User.find(1)}`
43 |
44 | *注意* `${User[1]}`与`${User.all[1]}`的意义并不相同,前者返回表中`id`等于1的记录;后者返回所有记录(all)中第*二*条记录(索引从0开始)。
45 |
46 | # 配置
47 |
48 | ## 增强EL表达式
49 |
50 | 要使用jActiveRecord-EL,需要在`web.xml`中添加如下信息:
51 |
52 | ```xml
53 |
54 | me.zzp.ar.el.ResolverSetup
55 |
56 | ```
57 |
58 | ## 骆驼命名法(可选)
59 |
60 | `JavaBean`属性的命名规则为骆驼命名法,例如“createdAt”;而数据库表的字段通常采用下划线命名法,例如“created_at”。该选项默认开启,即`${user.created_at}`与`${user.createdAt}`等价。在`web.xml`中添加如下上下文参数即可关闭自动转换开关:
61 |
62 | ```xml
63 |
64 | jactiverecord-el-camel-case
65 | false
66 |
67 | ```
68 |
69 | ## 创建数据库对象(可选)
70 |
71 | 在`web`项目中使用`jActiveRecord`,通常第一步就是通过数据源(`javax.sql.DataSource`)创建数据库对象(`me.zzp.ar.DB`)。因此`jActiveRecord-EL`提供了另一个上下文监听器,在启动服务器的时候自动创建数据库对象,并添加到上下文对象的属性中,设置方法如下:
72 |
73 | ```xml
74 |
75 | me.zzp.ar.el.DatabaseSetup
76 |
77 |
78 | jactiverecord-el-data-source
79 | java:/comp/env/jdbc/DataSource
80 |
81 | ```
82 |
83 | ## 重命名属性名(可选)
84 |
85 | `DatabaseSetup`创建的上下文属性名默认为“dbo”,即在`Servlet`中通过`getServletContext().getAttribute("dbo")`获得数据库对象。如果你不喜欢“dbo”这个名字,可指定以下信息自定义属性名:
86 |
87 | ```xml
88 |
89 | jactiverecord-el-attribute-name
90 | database
91 |
92 | ```
93 |
94 | 这样,属性名就改成了database。
95 |
--------------------------------------------------------------------------------
/jactiverecord/src/test/java/me/zzp/util/SeqTest.java:
--------------------------------------------------------------------------------
1 | package me.zzp.util;
2 |
3 | import java.util.Arrays;
4 | import java.util.Collections;
5 | import org.junit.Assert;
6 | import org.junit.Test;
7 |
8 | public class SeqTest {
9 | @Test
10 | public void testConcat() {
11 | Assert.assertArrayEquals(Seq.array("1", "2", "3", "4"), Seq.concat(Seq.array("1", "2"), "3", "4"));
12 | }
13 |
14 | @Test
15 | public void testRemove() {
16 | Assert.assertArrayEquals(Seq.array("1", "2", "4"), Seq.remove(Seq.array("1", "3", "2", "3", "3", "4"), "3"));
17 | }
18 |
19 | @Test
20 | public void testJoinListNull() {
21 | Assert.assertEquals("", Seq.join(null, ""));
22 | }
23 |
24 | @Test
25 | public void testJoinListEmpty() {
26 | Assert.assertEquals("", Seq.join(Collections.EMPTY_LIST, ""));
27 | }
28 |
29 | @Test
30 | public void testJoinDelimiterNull() {
31 | Assert.assertEquals("123", Seq.join(Arrays.asList("1", "2", "3"), null));
32 | }
33 |
34 | @Test
35 | public void testJoinDelimiterEmpty() {
36 | Assert.assertEquals("123", Seq.join(Arrays.asList("1", "2", "3"), ""));
37 | }
38 |
39 | @Test
40 | public void testCommaList() {
41 | Assert.assertEquals("1, 2, 3", Seq.join(Arrays.asList("1", "2", "3"), ", "));
42 | }
43 |
44 | @Test
45 | public void testConditionList() {
46 | Assert.assertEquals("username = ? and rowid = ? and password = ?", Seq.join(Arrays.asList("username = ?", "rowid = ?", "password = ?"), " and "));
47 | }
48 |
49 | @Test
50 | public void testConstantMap() {
51 | String[] actuals = Seq.map(Arrays.asList("1", "2", "3"), "?").toArray(new String[0]);
52 | Assert.assertArrayEquals(new String[] {"?", "?", "?"}, actuals);
53 | }
54 |
55 | @Test
56 | public void testFormatMap() {
57 | String[] actuals = Seq.map(Arrays.asList("1", "2", "3"), "id = %s").toArray(new String[0]);
58 | Assert.assertArrayEquals(new String[] {"id = 1", "id = 2", "id = 3"}, actuals);
59 | }
60 |
61 | @Test
62 | public void testPartition() {
63 | String[] actuals = null;
64 |
65 | actuals = Seq.partition(Arrays.asList("1", "2", "3", "4"), 2, " ").toArray(new String[0]);
66 | Assert.assertArrayEquals(new String[] {"1 2", "3 4"}, actuals);
67 |
68 | actuals = Seq.partition(Arrays.asList("1", "2", "3", "4", "5"), 2, " ").toArray(new String[0]);
69 | Assert.assertArrayEquals(new String[] {"1 2", "3 4"}, actuals);
70 |
71 | actuals = Seq.partition(Arrays.asList("1", "2", "3", "4", "5", "6"), 3, " ").toArray(new String[0]);
72 | Assert.assertArrayEquals(new String[] {"1 2 3", "4 5 6"}, actuals);
73 | }
74 |
75 | @Test
76 | public void testAssignAt() {
77 | int[] a = new int[5];
78 | Seq.assignAt(a, Seq.array(0, 2, 3), 1, 3, 4);
79 | Seq.assignAt(a, Seq.array(-1, -4), 5, 2);
80 | Assert.assertArrayEquals(new int[] {1, 2, 3, 4, 5}, a);
81 |
82 | String[] s = new String[3];
83 | Seq.assignAt(s, Seq.array(0, 2), "a", "d");
84 | Seq.assignAt(s, Seq.array(-2, -1), "b", "c");
85 | Assert.assertArrayEquals(Seq.array("a", "b", "c"), s);
86 | }
87 | }
--------------------------------------------------------------------------------
/jactioncontroller/src/main/java/me/zzp/jac/Dispatcher.java:
--------------------------------------------------------------------------------
1 | package me.zzp.jac;
2 |
3 | import java.io.IOException;
4 | import java.lang.reflect.Constructor;
5 | import java.lang.reflect.InvocationTargetException;
6 | import java.util.LinkedHashMap;
7 | import java.util.Map;
8 | import java.util.Scanner;
9 | import javax.servlet.ServletException;
10 | import javax.servlet.http.HttpServlet;
11 | import javax.servlet.http.HttpServletRequest;
12 | import javax.servlet.http.HttpServletResponse;
13 |
14 | public class Dispatcher extends HttpServlet {
15 | private static final Map> services = new LinkedHashMap<>();
16 |
17 | public static synchronized void add(String pattern, Class extends Service> type) {
18 | if (Service.class.isAssignableFrom(type)) {
19 | try {
20 | Constructor extends Service> c = type.getConstructor(HttpServletRequest.class, HttpServletResponse.class);
21 | services.put(new Controller(pattern), c);
22 | } catch (NoSuchMethodException | SecurityException e) {
23 | System.err.println(e.getMessage());
24 | }
25 | }
26 | }
27 |
28 | protected Service match(HttpServletRequest request, HttpServletResponse response) {
29 | String path = request.getPathInfo();
30 |
31 | for (Map.Entry> service : services.entrySet()) {
32 | Map url = service.getKey().parse(path);
33 | if (url != null) {
34 | for (Map.Entry info : url.entrySet()) {
35 | request.setAttribute(info.getKey(), info.getValue());
36 | }
37 |
38 | try {
39 | return service.getValue().newInstance(request, response);
40 | } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
41 | System.err.println(e.getMessage());
42 | }
43 | }
44 | }
45 |
46 | return null;
47 | }
48 |
49 | @Override
50 | protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
51 | match(request, response).query();
52 | }
53 |
54 | @Override
55 | protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
56 | match(request, response).create();
57 | }
58 |
59 | @Override
60 | protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
61 | Service service = match(request, response);
62 | if (service != null) {
63 | try (Scanner rin = new Scanner(request.getInputStream()).useDelimiter("\\A")) {
64 | String data = rin.hasNext()? rin.next(): "";
65 | for (String pair : data.split("&")) {
66 | String[] item = pair.split("=", 2);
67 | if (item.length == 2) {
68 | service.params.put(item[0], item[1]);
69 | }
70 | }
71 | }
72 | service.update();
73 | }
74 | }
75 |
76 | @Override
77 | protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
78 | match(request, response).delete();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/jactioncontroller/src/main/java/me/zzp/jac/Service.java:
--------------------------------------------------------------------------------
1 | package me.zzp.jac;
2 |
3 | import java.io.IOException;
4 | import java.util.Arrays;
5 | import java.util.HashMap;
6 | import java.util.Map;
7 | import javax.servlet.ServletContext;
8 | import javax.servlet.ServletException;
9 | import javax.servlet.http.HttpServletRequest;
10 | import javax.servlet.http.HttpServletResponse;
11 | import javax.servlet.http.HttpSession;
12 | import me.zzp.jac.ex.RedirectionException;
13 |
14 | public abstract class Service {
15 | protected final ServletContext app;
16 | protected final HttpSession session;
17 | protected final HttpServletRequest request;
18 | protected final HttpServletResponse response;
19 | protected final Map params;
20 |
21 | public Service(HttpServletRequest request, HttpServletResponse response) {
22 | this.request = request;
23 | this.response = response;
24 | params = new HashMap<>();
25 | session = request.getSession();
26 | app = request.getServletContext();
27 |
28 | for (Map.Entry entry : request.getParameterMap().entrySet()) {
29 | String name = entry.getKey();
30 | String[] values = entry.getValue();
31 | if (values.length == 1) {
32 | params.put(name, values[0]);
33 | } else {
34 | params.put(name, Arrays.asList(values));
35 | }
36 | }
37 | }
38 |
39 | protected E get(String key) {
40 | Object value = null;
41 | if (params.containsKey(key)) {
42 | value = params.get(key);
43 | } else if (request.getAttribute(key) != null) {
44 | value = request.getAttribute(key);
45 | } else if (session.getAttribute(key) != null) {
46 | value = session.getAttribute(key);
47 | } else if (app.getAttribute(key) != null) {
48 | value = app.getAttribute(key);
49 | }
50 | return (E)value;
51 | }
52 |
53 | protected String getStr(String key) {
54 | Object value = get(key);
55 | return value != null? value.toString(): null;
56 | }
57 |
58 | protected int getInt(String key) {
59 | String value = getStr(key);
60 | try {
61 | return Integer.parseInt(value);
62 | } catch(NumberFormatException e) {
63 | return 0;
64 | }
65 | }
66 |
67 | protected void set(String key, Object value) {
68 | request.setAttribute(key, value);
69 | }
70 |
71 | protected void forward(String path) {
72 | try {
73 | request.getRequestDispatcher(path).forward(request, response);
74 | } catch (ServletException | IOException e) {
75 | throw new RedirectionException("forward", path, e);
76 | }
77 | }
78 |
79 | protected void redirect(String path) {
80 | if (path.startsWith("/")) {
81 | path = request.getContextPath().concat(path);
82 | }
83 |
84 | try {
85 | response.sendRedirect(path);
86 | } catch (IOException e) {
87 | throw new RedirectionException("redirect", path, e);
88 | }
89 | }
90 |
91 | public void create() {
92 | }
93 |
94 | public void update() {
95 | }
96 |
97 | public void delete() {
98 | }
99 |
100 | public void query() {
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/Record.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar;
2 |
3 | import java.util.Map;
4 | import java.util.Set;
5 |
6 | /**
7 | * 行记录对象。
8 | *
9 | * @since 1.0
10 | * @author redraiment
11 | */
12 | public final class Record {
13 | private final Table table;
14 | private final Map values;
15 |
16 | Record(Table table, Map values) {
17 | this.table = table;
18 | this.values = values;
19 | }
20 |
21 | public Set columnNames() {
22 | return values.keySet();
23 | }
24 |
25 | public E get(String name) {
26 | name = DB.parseKeyParameter(name);
27 | Object value = null;
28 |
29 | if (values.containsKey(name)) {
30 | value = values.get(name);
31 | } else if (table.relations.containsKey(name)) {
32 | Association relation = table.relations.get(name);
33 | Table active = table.dbo.active(relation.target);
34 | active.join(relation.assoc(table.name, getInt("id")));
35 | if (relation.isAncestor() && !relation.isCross()) {
36 | active.constrain(relation.key, getInt("id"));
37 | }
38 | value = (relation.isOnlyOneResult()? active.first(): active);
39 | }
40 |
41 | String key = "get_".concat(name);
42 | if (table.hooks.containsKey(key)) {
43 | value = table.hooks.get(key).call(this, value);
44 | }
45 | return (E)value;
46 | }
47 |
48 | /* For primitive types */
49 | public boolean getBool(String name) {
50 | return get(name);
51 | }
52 |
53 | public byte getByte(String name) {
54 | return get(name);
55 | }
56 |
57 | public char getChar(String name) {
58 | return get(name);
59 | }
60 |
61 | public short getShort(String name) {
62 | return get(name);
63 | }
64 |
65 | public int getInt(String name) {
66 | return get(name);
67 | }
68 |
69 | public long getLong(String name) {
70 | return get(name);
71 | }
72 |
73 | public float getFloat(String name) {
74 | return get(name);
75 | }
76 |
77 | public double getDouble(String name) {
78 | return get(name);
79 | }
80 |
81 | /* For any other types */
82 |
83 | public String getStr(String name) {
84 | return get(name);
85 | }
86 |
87 | public E get(String name, Class type) {
88 | return type.cast(get(name));
89 | }
90 |
91 | public Record set(String name, Object value) {
92 | name = DB.parseKeyParameter(name);
93 | String key = "set_".concat(name);
94 | if (table.hooks.containsKey(key)) {
95 | value = table.hooks.get(key).call(this, value);
96 | }
97 | values.put(name, value);
98 | return this;
99 | }
100 |
101 | public Record save() {
102 | table.update(this);
103 | return this;
104 | }
105 |
106 | public Record update(Object... args) {
107 | for (int i = 0; i < args.length; i += 2) {
108 | set(args[i].toString(), args[i + 1]);
109 | }
110 | return save();
111 | }
112 |
113 | public void destroy() {
114 | table.delete(this);
115 | }
116 |
117 | @Override
118 | public String toString() {
119 | StringBuilder line = new StringBuilder();
120 | for (Map.Entry e : values.entrySet()) {
121 | line.append(String.format("%s = %s\n", e.getKey(), e.getValue()));
122 | }
123 | return line.toString();
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/pool/JdbcDataSource.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.pool;
2 |
3 | import java.io.PrintWriter;
4 | import java.sql.Connection;
5 | import java.sql.DriverManager;
6 | import java.sql.SQLException;
7 | import java.sql.SQLFeatureNotSupportedException;
8 | import java.util.Properties;
9 | import java.util.logging.Logger;
10 | import javax.sql.DataSource;
11 |
12 | /**
13 | * jActiveRecord默认DataSource实现,每一次均创建一个新的数据库连接。
14 | *
15 | * @since 2.0
16 | * @author redraiment
17 | */
18 | public final class JdbcDataSource implements DataSource {
19 | private final String url;
20 | private final Properties info;
21 |
22 | public JdbcDataSource(String url, String username, String password) {
23 | this.url = url;
24 | info = new Properties();
25 | info.put("user", username);
26 | info.put("password", password);
27 | }
28 |
29 | /**
30 | * 提供连接数据库基本信息。
31 | *
32 | * @param url 数据库连接地址
33 | * @param info 包含用户名、密码等登入信息。
34 | */
35 | public JdbcDataSource(String url, Properties info) {
36 | this.url = url;
37 | this.info = info;
38 | }
39 |
40 | /**
41 | * 每次调用时均返回一个新的数据库连接。
42 | *
43 | * @return 一个新的数据库连接。
44 | * @throws SQLException 连接数据库失败。
45 | */
46 | @Override
47 | public Connection getConnection() throws SQLException {
48 | return DriverManager.getConnection(url, info);
49 | }
50 |
51 | /**
52 | * 每次调用时均返回一个新的数据库连接。
53 | *
54 | * @return 一个新的数据库连接。
55 | * @throws SQLException 连接数据库失败。
56 | */
57 | @Override
58 | public Connection getConnection(String username, String password) throws SQLException {
59 | return DriverManager.getConnection(url, username, password);
60 | }
61 |
62 | /**
63 | * 不支持,永远不会被调用。
64 | *
65 | * @return 无
66 | * @throws SQLException 从不
67 | */
68 | @Override
69 | public PrintWriter getLogWriter() throws SQLException {
70 | throw new UnsupportedOperationException("Not supported yet.");
71 | }
72 |
73 | /**
74 | * 不支持,永远不会被调用。
75 | *
76 | * @throws SQLException 从不
77 | */
78 | @Override
79 | public void setLogWriter(PrintWriter out) throws SQLException {
80 | throw new UnsupportedOperationException("Not supported yet.");
81 | }
82 |
83 | /**
84 | * 不支持,永远不会被调用。
85 | *
86 | * @throws SQLException 从不
87 | */
88 | @Override
89 | public void setLoginTimeout(int seconds) throws SQLException {
90 | throw new UnsupportedOperationException("Not supported yet.");
91 | }
92 |
93 | /**
94 | * 不支持,永远不会被调用。
95 | *
96 | * @return 无
97 | * @throws SQLException 从不
98 | */
99 | @Override
100 | public int getLoginTimeout() throws SQLException {
101 | throw new UnsupportedOperationException("Not supported yet.");
102 | }
103 |
104 | /**
105 | * 不支持,永远不会被调用。
106 | *
107 | * @return 无
108 | * @throws SQLFeatureNotSupportedException 从不
109 | */
110 | @Override
111 | public Logger getParentLogger() throws SQLFeatureNotSupportedException {
112 | throw new UnsupportedOperationException("Not supported yet.");
113 | }
114 |
115 | /**
116 | * 不支持,永远不会被调用。
117 | *
118 | * @param 类型
119 | * @return 无
120 | * @throws SQLException 从不
121 | */
122 | @Override
123 | public T unwrap(Class iface) throws SQLException {
124 | throw new UnsupportedOperationException("Not supported yet.");
125 | }
126 |
127 | /**
128 | * 不支持,永远不会被调用。
129 | *
130 | * @return 无
131 | * @throws SQLException 从不
132 | */
133 | @Override
134 | public boolean isWrapperFor(Class> iface) throws SQLException {
135 | throw new UnsupportedOperationException("Not supported yet.");
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/pool/SingletonDataSource.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.pool;
2 |
3 | import java.io.PrintWriter;
4 | import java.sql.Connection;
5 | import java.sql.DriverManager;
6 | import java.sql.SQLException;
7 | import java.sql.SQLFeatureNotSupportedException;
8 | import java.util.Properties;
9 | import java.util.logging.Logger;
10 | import javax.sql.DataSource;
11 |
12 | /**
13 | * jActiveRecord默认DataSource实现,永远只返回同一个数据库连接。
14 | * 因此,内存型的数据库也能正常工作。
15 | *
16 | * @since 2.0
17 | * @author redraiment
18 | */
19 | public final class SingletonDataSource implements DataSource {
20 | private final Connection instance;
21 |
22 | /**
23 | * 提供连接数据库基本信息。
24 | *
25 | * @param url 数据库连接地址
26 | * @param info 包含用户名、密码等登入信息。
27 | * @throws java.sql.SQLException 连接数据库失败
28 | */
29 | public SingletonDataSource(String url, Properties info) throws SQLException {
30 | final Connection c = DriverManager.getConnection(url, info);
31 | Runtime.getRuntime().addShutdownHook(new Thread() {
32 | @Override
33 | public void run() {
34 | try {
35 | c.close();
36 | } catch (SQLException e) {
37 | throw new RuntimeException("close database fail");
38 | }
39 | }
40 | });
41 | instance = new SingletonConnection(c);
42 | }
43 |
44 | /**
45 | * 每次调用时均返回一个新的数据库连接。
46 | *
47 | * @return 一个新的数据库连接。
48 | * @throws SQLException 连接数据库失败。
49 | */
50 | @Override
51 | public Connection getConnection() throws SQLException {
52 | return instance;
53 | }
54 |
55 | /**
56 | * 每次调用时均返回一个新的数据库连接。
57 | *
58 | * @return 一个新的数据库连接。
59 | * @throws SQLException 连接数据库失败。
60 | */
61 | @Override
62 | public Connection getConnection(String username, String password) throws SQLException {
63 | return instance;
64 | }
65 |
66 | /**
67 | * 不支持,永远不会被调用。
68 | *
69 | * @return 无
70 | * @throws SQLException 从不
71 | */
72 | @Override
73 | public PrintWriter getLogWriter() throws SQLException {
74 | throw new UnsupportedOperationException("Not supported yet.");
75 | }
76 |
77 | /**
78 | * 不支持,永远不会被调用。
79 | *
80 | * @throws SQLException 从不
81 | */
82 | @Override
83 | public void setLogWriter(PrintWriter out) throws SQLException {
84 | throw new UnsupportedOperationException("Not supported yet.");
85 | }
86 |
87 | /**
88 | * 不支持,永远不会被调用。
89 | *
90 | * @throws SQLException 从不
91 | */
92 | @Override
93 | public void setLoginTimeout(int seconds) throws SQLException {
94 | throw new UnsupportedOperationException("Not supported yet.");
95 | }
96 |
97 | /**
98 | * 不支持,永远不会被调用。
99 | *
100 | * @return 无
101 | * @throws SQLException 从不
102 | */
103 | @Override
104 | public int getLoginTimeout() throws SQLException {
105 | throw new UnsupportedOperationException("Not supported yet.");
106 | }
107 |
108 | /**
109 | * 不支持,永远不会被调用。
110 | *
111 | * @return 无
112 | * @throws SQLFeatureNotSupportedException 从不
113 | */
114 | @Override
115 | public Logger getParentLogger() throws SQLFeatureNotSupportedException {
116 | throw new UnsupportedOperationException("Not supported yet.");
117 | }
118 |
119 | /**
120 | * 不支持,永远不会被调用。
121 | *
122 | * @param 类型
123 | * @return 无
124 | * @throws SQLException 从不
125 | */
126 | @Override
127 | public T unwrap(Class iface) throws SQLException {
128 | throw new UnsupportedOperationException("Not supported yet.");
129 | }
130 |
131 | /**
132 | * 不支持,永远不会被调用。
133 | *
134 | * @return 无
135 | * @throws SQLException 从不
136 | */
137 | @Override
138 | public boolean isWrapperFor(Class> iface) throws SQLException {
139 | throw new UnsupportedOperationException("Not supported yet.");
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/jactiverecord/src/test/java/me/zzp/ar/sql/TSqlBuilderTest.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.sql;
2 |
3 | import static org.junit.Assert.assertEquals;
4 | import org.junit.BeforeClass;
5 | import org.junit.Test;
6 |
7 | public class TSqlBuilderTest {
8 | private static SqlBuilder sql = null;
9 |
10 | @BeforeClass
11 | public static void setUpClass() {
12 | sql = new TSqlBuilder();
13 | }
14 |
15 | @Test
16 | public void testSelectAllFrom() {
17 | assertEquals("select * from users", sql.select().from("users").toString());
18 | assertEquals("select * from users", sql.select("*").from("users").toString());
19 | }
20 |
21 | @Test
22 | public void testSelectSomeFrom() {
23 | assertEquals("select name, age from users", sql.select("name", "age").from("users").toString());
24 | }
25 |
26 | @Test
27 | public void testWhere() {
28 | sql.select().from("users").where("age > 17");
29 | assertEquals("select * from users where age > 17", sql.toString());
30 | sql.select().from("users").where("age >= 18 and age <= 25");
31 | assertEquals("select * from users where age >= 18 and age <= 25", sql.toString());
32 | }
33 |
34 | @Test
35 | public void testHaving() {
36 | sql.select("sum(age) as sum", "count(*) as count").from("users").groupBy("age").having("count(*) > 2");
37 | assertEquals("select sum(age) as sum, count(*) as count from users group by age having count(*) > 2", sql.toString());
38 | sql.select().from("users").having("count(*) > 2");
39 | assertEquals("select * from users", sql.toString());
40 | sql.select("age", "count(*)").from("users").groupBy("age");
41 | assertEquals("select age, count(*) from users group by age", sql.toString());
42 | }
43 |
44 | @Test
45 | public void testSort() {
46 | sql.select().from("users").orderBy("age");
47 | assertEquals("select * from users order by age", sql.toString());
48 | sql.select().from("users").orderBy("age asc");
49 | assertEquals("select * from users order by age asc", sql.toString());
50 | sql.select().from("users").orderBy("age desc");
51 | assertEquals("select * from users order by age desc", sql.toString());
52 | sql.select().from("users").orderBy("age", "name");
53 | assertEquals("select * from users order by age, name", sql.toString());
54 | }
55 |
56 | @Test
57 | public void testPaging() {
58 | sql.select().from("users").limit(10);
59 | assertEquals("select * from users limit 10", sql.toString());
60 | sql.select().from("users").offset(10);
61 | assertEquals("select * from users offset 10", sql.toString());
62 | sql.select().from("users").limit(10).offset(20);
63 | assertEquals("select * from users limit 10 offset 20", sql.toString());
64 | sql.select().from("users").offset(20).limit(10);
65 | assertEquals("select * from users limit 10 offset 20", sql.toString());
66 | }
67 |
68 | @Test
69 | public void testQuery() {
70 | assertEquals("select age, count(*) from users where id > 1 and age >= 18 group by age having count(*) > 2 order by age desc limit 10 offset 100",
71 | sql.select("age", "count(*)")
72 | .from("users")
73 | .where("id > 1", "age >= 18")
74 | .groupBy("age")
75 | .having("count(*) > 2")
76 | .orderBy("age desc")
77 | .limit(10)
78 | .offset(100)
79 | .toString());
80 | }
81 |
82 | @Test
83 | public void testUpdate() {
84 | assertEquals("update users set age = ?", sql.update("users").set("age").toString());
85 | assertEquals("update users set name = ?, age = ? where id = ?", sql.update("users").set("name", "age").where("id = ?").toString());
86 | assertEquals("update users set age = ? where id > ? and name = ?", sql.update("users").set("age").where("id > ?", "name = ?").toString());
87 | }
88 |
89 | @Test
90 | public void testDelete() {
91 | assertEquals("delete from users", sql.delete().from("users").toString());
92 | assertEquals("delete from users where id > 2 and name = ?", sql.delete().from("users").where("id > 2", "name = ?").toString());
93 | }
94 |
95 | @Test
96 | public void testInsert() {
97 | assertEquals("insert into users (name, age) values (?, ?)",
98 | sql.insert().into("users")
99 | .values("name", "age")
100 | .toString());
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/jactiverecord-el/src/main/java/me/zzp/ar/el/RecordELResolver.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.el;
2 |
3 | import java.beans.FeatureDescriptor;
4 | import java.util.ArrayList;
5 | import java.util.Iterator;
6 | import java.util.List;
7 | import javax.el.ELContext;
8 | import javax.el.ELResolver;
9 | import me.zzp.ar.Record;
10 |
11 | /**
12 | * me.zzp.ar.Record的EL表达式解析器。
13 | * 简化Record对象方法调用的方式。
14 | * @author redraiment
15 | * @since 1.0
16 | */
17 | public final class RecordELResolver extends ELResolver {
18 | private final boolean camelCase;
19 |
20 | public RecordELResolver() {
21 | this(false);
22 | }
23 |
24 | public RecordELResolver(boolean camelCase) {
25 | this.camelCase = camelCase;
26 | }
27 |
28 | private String getKey(Object property) {
29 | String key = property.toString();
30 | return camelCase? key.replaceAll("(?=[A-Z])", "_").toLowerCase(): key;
31 | }
32 |
33 | /**
34 | * 像访问普通JavaBean一样访问Record中的字段。
35 | * ${user.name}等价于${user.get("name")}。
36 | * @param context EL表达式上下文
37 | * @param base Table对象
38 | * @param property 属性
39 | * @return 返回相应属性的值
40 | */
41 | @Override
42 | public Object getValue(ELContext context, Object base, Object property) {
43 | if (base != null && base instanceof Record && property != null) {
44 | context.setPropertyResolved(true);
45 | Record record = (Record) base;
46 | return record.get(getKey(property));
47 | } else {
48 | context.setPropertyResolved(false);
49 | return null;
50 | }
51 | }
52 |
53 | /**
54 | * 获取属性在数据库中相应的类型。
55 | * @param context EL表达式上下文
56 | * @param base Table对象
57 | * @param property 属性
58 | * @return 返回相应属性的类型
59 | */
60 | @Override
61 | public Class> getType(ELContext context, Object base, Object property) {
62 | if (base != null && base instanceof Record && property != null) {
63 | context.setPropertyResolved(true);
64 | Record record = (Record) base;
65 | Object o = record.get(getKey(property));
66 | return o == null? null: o.getClass();
67 | } else {
68 | context.setPropertyResolved(false);
69 | return null;
70 | }
71 | }
72 |
73 | /**
74 | * 设置属性值。
75 | * 等价于调用Record#set(String name, Object value)。
76 | * @param context EL表达式上下文
77 | * @param base Table对象
78 | * @param property 属性
79 | * @param value 值
80 | */
81 | @Override
82 | public void setValue(ELContext context, Object base, Object property, Object value) {
83 | if (base != null && base instanceof Record && property != null) {
84 | context.setPropertyResolved(true);
85 | Record record = (Record) base;
86 | record.set(getKey(property), value);
87 | } else {
88 | context.setPropertyResolved(false);
89 | }
90 | }
91 |
92 | /**
93 | * 均可写。
94 | * @param context EL表达式上下文
95 | * @param base Table对象
96 | * @param property 属性
97 | * @return false
98 | */
99 | @Override
100 | public boolean isReadOnly(ELContext context, Object base, Object property) {
101 | if (base != null && base instanceof Record && property != null) {
102 | context.setPropertyResolved(true);
103 | return false;
104 | } else {
105 | context.setPropertyResolved(false);
106 | return false;
107 | }
108 | }
109 |
110 | /**
111 | * 返回由列名组成的列表。
112 | * @param context EL表达式上下文
113 | * @param base Table对象
114 | * @return 包含当前表的所有列名
115 | */
116 | @Override
117 | public Iterator getFeatureDescriptors(ELContext context, Object base) {
118 | List list = new ArrayList<>();
119 | if (base != null && base instanceof Record) {
120 | Record record = (Record) base;
121 | for (String column : record.columnNames()) {
122 | FeatureDescriptor feature = new FeatureDescriptor();
123 | feature.setDisplayName(column);
124 | feature.setName(column);
125 | feature.setShortDescription(column);
126 | feature.setHidden(false);
127 | feature.setExpert(false);
128 | feature.setPreferred(true);
129 | list.add(feature);
130 | }
131 | }
132 | return list.iterator();
133 | }
134 |
135 | /**
136 | * 属性为字符串类型。
137 | * @param context EL表达式上下文
138 | * @param base Table对象
139 | * @return String.class
140 | */
141 | @Override
142 | public Class> getCommonPropertyType(ELContext context, Object base) {
143 | return String.class;
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/jactiverecord-el/src/main/java/me/zzp/ar/el/TableELResolver.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.el;
2 |
3 | import java.beans.FeatureDescriptor;
4 | import java.util.ArrayList;
5 | import java.util.Arrays;
6 | import java.util.Iterator;
7 | import java.util.List;
8 | import javax.el.ELContext;
9 | import javax.el.ELResolver;
10 | import me.zzp.ar.Record;
11 | import me.zzp.ar.Table;
12 |
13 | /**
14 | * me.zzp.ar.Table的EL表达式解析器。
15 | * 简化Table对象方法调用的方式。
16 | * @author redraiment
17 | * @since 1.0
18 | */
19 | public final class TableELResolver extends ELResolver {
20 | /**
21 | * 支持all、first、last和索引四种查询方式:
22 | *
23 | * - all:调用
Table#all()。即${User.all}等价于${User.all()}
24 | * - first:调用
Table#first()。即${User.first}等价于${User.first()}
25 | * - last:调用
Table#last()。即${User.last}等价于${User.last()}
26 | * - 索引:调用
Table#find(int id)。即${User[1]}等价于${User.find(1)}
27 | *
28 | * @param context EL表达式上下文
29 | * @param base Table对象
30 | * @param property 属性
31 | * @return 返回相应的方法调用结果
32 | */
33 | @Override
34 | public Object getValue(ELContext context, Object base, Object property) {
35 | if (base != null && base instanceof Table && property != null) {
36 | context.setPropertyResolved(true);
37 | Table table = (Table) base;
38 | if (property instanceof String) {
39 | if (property.equals("all")) {
40 | return table.all();
41 | } else if (property.equals("first")) {
42 | return table.first();
43 | } else if (property.equals("last")) {
44 | return table.last();
45 | }
46 | } else if (property instanceof Number) {
47 | Number index = (Number) property;
48 | return table.find(index.intValue());
49 | }
50 | return null;
51 | } else {
52 | context.setPropertyResolved(false);
53 | return null;
54 | }
55 | }
56 |
57 | /**
58 | * 如果属性值为all,则返回 {@link java.util.List} 类型;
59 | * 否则返回 {@link me.zzp.ar.Record} 类型。
60 | * @param context EL表达式上下文
61 | * @param base Table对象
62 | * @param property 属性
63 | * @return 返回相应属性的类型
64 | */
65 | @Override
66 | public Class> getType(ELContext context, Object base, Object property) {
67 | if (base != null && base instanceof Table && property != null) {
68 | context.setPropertyResolved(true);
69 | if (property instanceof String) {
70 | if (property.equals("all")) {
71 | return List.class;
72 | } else if (property.equals("first") || property.equals("last")) {
73 | return Record.class;
74 | }
75 | } else if (property instanceof Number) {
76 | return Record.class;
77 | }
78 | return null;
79 | } else {
80 | context.setPropertyResolved(false);
81 | return null;
82 | }
83 | }
84 |
85 | /**
86 | * Table不允许修改。
87 | * @param context EL表达式上下文
88 | * @param base Table对象
89 | * @param property 属性
90 | * @param value 值
91 | */
92 | @Override
93 | public void setValue(ELContext context, Object base, Object property, Object value) {
94 | context.setPropertyResolved(base != null && base instanceof Table);
95 | }
96 |
97 | /**
98 | * 总是返回true。
99 | * @param context EL表达式上下文
100 | * @param base Table对象
101 | * @param property 属性
102 | * @return true
103 | */
104 | @Override
105 | public boolean isReadOnly(ELContext context, Object base, Object property) {
106 | if (base != null && base instanceof Table) {
107 | context.setPropertyResolved(true);
108 | return true;
109 | } else {
110 | context.setPropertyResolved(false);
111 | return false;
112 | }
113 | }
114 |
115 | /**
116 | * 仅返回all、first和last三个方法名。
117 | * @param context EL表达式上下文
118 | * @param base Table对象
119 | * @return 包含all、first和last三个方法名
120 | */
121 | @Override
122 | public Iterator getFeatureDescriptors(ELContext context, Object base) {
123 | List list = new ArrayList<>();
124 | if (base != null && base instanceof Table) {
125 | for (String column : Arrays.asList("all", "first", "last")) {
126 | FeatureDescriptor feature = new FeatureDescriptor();
127 | feature.setDisplayName(column);
128 | feature.setName(column);
129 | feature.setShortDescription(column);
130 | feature.setHidden(false);
131 | feature.setExpert(false);
132 | feature.setPreferred(true);
133 | list.add(feature);
134 | }
135 | }
136 | return list.iterator();
137 | }
138 |
139 | /**
140 | * 属性为字符串类型。
141 | * @param context EL表达式上下文
142 | * @param base Table对象
143 | * @return String.class
144 | */
145 | @Override
146 | public Class> getCommonPropertyType(ELContext context, Object base) {
147 | return String.class;
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/sql/AbstractSqlBuilder.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.sql;
2 |
3 | import java.util.Arrays;
4 | import java.util.Deque;
5 | import java.util.LinkedList;
6 | import me.zzp.util.Seq;
7 |
8 | /**
9 | * 实现生成insert、update、delete和select的语句的方法。
10 | * 由子类自行实现如何填充数据。
11 | *
12 | * @since 1.0
13 | * @author redraiment
14 | */
15 | public abstract class AbstractSqlBuilder implements SqlBuilder {
16 | protected static enum Mode {
17 | Select,
18 | Insert,
19 | Update,
20 | Delete
21 | }
22 |
23 | protected Mode mode;
24 | protected Deque fields;
25 | protected Deque tables;
26 | protected Deque conditions;
27 | protected Deque groups;
28 | protected Deque having;
29 | protected Deque orders;
30 | protected int limit;
31 | protected int offset;
32 |
33 | protected SqlBuilder start(Mode mode) {
34 | this.mode = mode;
35 | fields = new LinkedList<>();
36 | tables = new LinkedList<>();
37 | conditions = new LinkedList<>();
38 | groups = new LinkedList<>();
39 | having = new LinkedList<>();
40 | orders = new LinkedList<>();
41 | limit = offset = -1;
42 | return this;
43 | }
44 |
45 | public SqlBuilder addField(String field) {
46 | fields.addLast(field);
47 | return this;
48 | }
49 |
50 | public SqlBuilder addTable(String table) {
51 | tables.addLast(table);
52 | return this;
53 | }
54 |
55 | public SqlBuilder addCondition(String condition) {
56 | conditions.addLast(condition);
57 | return this;
58 | }
59 |
60 | public SqlBuilder addGroup(String group) {
61 | groups.addLast(group);
62 | return this;
63 | }
64 |
65 | public SqlBuilder addHaving(String having) {
66 | this.having.addLast(having);
67 | return this;
68 | }
69 |
70 | public SqlBuilder addOrder(String order) {
71 | orders.addLast(order);
72 | return this;
73 | }
74 |
75 | public SqlBuilder setFields(String... fields) {
76 | this.fields.clear();
77 | this.fields.addAll(Arrays.asList(fields));
78 | return this;
79 | }
80 |
81 | public SqlBuilder setTables(String... tables) {
82 | this.tables.clear();
83 | this.tables.addAll(Arrays.asList(tables));
84 | return this;
85 | }
86 |
87 | public SqlBuilder setConditions(String... conditions) {
88 | this.conditions.clear();
89 | this.conditions.addAll(Arrays.asList(conditions));
90 | return this;
91 | }
92 |
93 | public SqlBuilder setGroups(String... groups) {
94 | this.groups.clear();
95 | this.groups.addAll(Arrays.asList(groups));
96 | return this;
97 | }
98 |
99 | public SqlBuilder setHaving(String... having) {
100 | this.having.clear();
101 | this.having.addAll(Arrays.asList(having));
102 | return this;
103 | }
104 |
105 | public SqlBuilder setOrders(String... orders) {
106 | this.orders.clear();
107 | this.orders.addAll(Arrays.asList(orders));
108 | return this;
109 | }
110 |
111 | public SqlBuilder setLimit(int limit) {
112 | this.limit = limit;
113 | return this;
114 | }
115 |
116 | public SqlBuilder setOffset(int offset) {
117 | this.offset = offset;
118 | return this;
119 | }
120 |
121 | // toString
122 |
123 | private String selectToString() {
124 | StringBuilder sql = new StringBuilder("select ");
125 | sql.append(Seq.join(fields, ", "))
126 | .append(" from ")
127 | .append(Seq.join(tables, " join "));
128 | if (!conditions.isEmpty()) {
129 | sql.append(" where ").append(Seq.join(conditions, " and "));
130 | }
131 | if (!groups.isEmpty()) {
132 | sql.append(" group by ").append(Seq.join(groups, ", "));
133 | if (!having.isEmpty()) {
134 | sql.append(" having ").append(Seq.join(having, " and "));
135 | }
136 | }
137 | if (!orders.isEmpty()) {
138 | sql.append(" order by ").append(Seq.join(orders, ", "));
139 | }
140 | if (limit > 0) {
141 | sql.append(" limit ").append(Integer.toString(limit));
142 | }
143 | if (offset > -1) {
144 | sql.append(" offset ").append(Integer.toString(offset));
145 | }
146 |
147 | return sql.toString();
148 | }
149 |
150 | private String insertToString() {
151 | StringBuilder sql = new StringBuilder("insert into ");
152 | sql.append(tables.getFirst())
153 | .append(" (")
154 | .append(Seq.join(fields, ", "))
155 | .append(") values (")
156 | .append(Seq.join(Seq.map(fields, "?"), ", "))
157 | .append(")");
158 | return sql.toString();
159 | }
160 |
161 | private String updateToString() {
162 | StringBuilder sql = new StringBuilder("update ");
163 | sql.append(tables.getFirst())
164 | .append(" set ")
165 | .append(Seq.join(Seq.map(fields, "%s = ?"), ", "));
166 |
167 | if (!conditions.isEmpty()) {
168 | sql.append(" where ").append(Seq.join(conditions, " and "));
169 | }
170 | return sql.toString();
171 | }
172 |
173 | private String deleteToString() {
174 | StringBuilder sql = new StringBuilder("delete from ");
175 | sql.append(tables.getFirst());
176 | if (!conditions.isEmpty()) {
177 | sql.append(" where ").append(Seq.join(conditions, " and "));
178 | }
179 | return sql.toString();
180 | }
181 |
182 | @Override
183 | public String toString() {
184 | switch (mode) {
185 | case Select: return selectToString();
186 | case Insert: return insertToString();
187 | case Update: return updateToString();
188 | case Delete: return deleteToString();
189 | default: return "";
190 | }
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | me.zzp
8 | java-on-rails
9 | 1.0.0-SNAPSHOT
10 | pom
11 |
12 | https://github.com/redraiment/java-on-rails
13 |
14 |
15 |
16 | redraiment
17 | Zhang, Zepeng
18 | redraiment@gmail.com
19 | http://zzp.me
20 |
21 |
22 |
23 |
24 |
25 | MIT License
26 | http://www.opensource.org/licenses/mit-license.php
27 |
28 |
29 |
30 |
31 | scm:git:git@github.com:redraiment/java-on-rails.git
32 | scm:git:git@github.com:redraiment/java-on-rails.git
33 | git@github.com:redraiment/java-on-rails.git
34 |
35 |
36 |
37 | jactioncontroller
38 | jactionview
39 | jactiverecord
40 | jactiverecord-el
41 |
42 |
43 |
44 | UTF-8
45 | 8
46 | 8
47 |
48 |
49 |
50 |
51 |
52 | me.zzp
53 | jactioncontroller
54 | 1.0.0-SNAPSHOT
55 |
56 |
57 | me.zzp
58 | jactionview
59 | 1.0.0-SNAPSHOT
60 |
61 |
62 | me.zzp
63 | jactiverecord
64 | 2.3
65 |
66 |
67 | me.zzp
68 | jactiverecord-el
69 | 1.2
70 |
71 |
72 |
73 | jakarta.servlet
74 | jakarta.servlet-api
75 | 6.0.0
76 |
77 |
78 | com.h2database
79 | h2
80 | 2.1.214
81 |
82 |
83 | org.xerial
84 | sqlite-jdbc
85 | 3.39.3.0
86 |
87 |
88 | org.postgresql
89 | postgresql
90 | 42.5.0
91 |
92 |
93 | mysql
94 | mysql-connector-java
95 | 8.0.30
96 |
97 |
98 |
99 | org.junit
100 | junit-bom
101 | 5.9.0
102 | pom
103 | import
104 |
105 |
106 |
107 |
108 |
109 |
110 | sonatype-nexus-snapshots
111 | Sonatype Nexus snapshot repository
112 | https://oss.sonatype.org/content/repositories/snapshots
113 |
114 |
115 | sonatype-nexus-staging
116 | Sonatype Nexus release repository
117 | https://oss.sonatype.org/service/local/staging/deploy/maven2/
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | org.apache.maven.plugins
127 | maven-source-plugin
128 | 3.2.1
129 |
130 |
131 | package
132 |
133 | jar-no-fork
134 |
135 |
136 |
137 |
138 |
139 |
140 | org.apache.maven.plugins
141 | maven-javadoc-plugin
142 | 3.4.1
143 |
144 |
145 | package
146 |
147 | jar
148 |
149 |
150 |
151 |
152 |
153 |
154 | org.apache.maven.plugins
155 | maven-gpg-plugin
156 | 3.0.1
157 |
158 |
159 | verify
160 |
161 | sign
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/util/Seq.java:
--------------------------------------------------------------------------------
1 | package me.zzp.util;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Arrays;
5 | import java.util.Collection;
6 | import java.util.List;
7 |
8 | /**
9 | * 数组和List相关的工具方法。
10 | *
11 | * @since 1.0
12 | * @author redraiment
13 | */
14 | public final class Seq {
15 | /**
16 | * 将一系列对象组装成数组。
17 | *
18 | * @param 元素类型。
19 | * @param args 多个同类型的元素。
20 | * @return 返回由这些元素组成的数组。
21 | */
22 | public static E[] array(E... args) {
23 | return args;
24 | }
25 |
26 | /**
27 | * 将一系列对象组装成List。
28 | *
29 | * @param 元素类型。
30 | * @param args 多个同类型的元素。
31 | * @return 返回由这些元素组成的List。
32 | */
33 | public static List list(E... args) {
34 | List list = new ArrayList<>();
35 | list.addAll(Arrays.asList(args));
36 | return list;
37 | }
38 |
39 | /**
40 | * 追加任意多个新int到int数组末尾。
41 | *
42 | * @param a 现有int数组。
43 | * @param b 任意多个新int。
44 | * @return 返回组装好的新int数组。
45 | */
46 | public static int[] concat(int[] a, int... b) {
47 | int[] c = new int[a.length + b.length];
48 | System.arraycopy(a, 0, c, 0, a.length);
49 | System.arraycopy(b, 0, c, a.length, b.length);
50 | return c;
51 | }
52 |
53 | /**
54 | * 追加任意多个新元素到数组末尾。
55 | *
56 | * @param 元素的类型。
57 | * @param a 现有数组。
58 | * @param b 任意多个新元素。
59 | * @return 返回组装好的新数组。
60 | */
61 | public static E[] concat(E[] a, E... b) {
62 | return merge(a, b);
63 | }
64 |
65 | /**
66 | * 将数组连接成字符串。
67 | *
68 | * @param delimiter 分隔符。
69 | * @param args 任意多个元素。
70 | * @return 连接后的字符串。
71 | */
72 | public static String join(String delimiter, Object... args) {
73 | return join(Arrays.asList(args), delimiter);
74 | }
75 |
76 | /**
77 | * 将容器里的元素连接成字符串。
78 | *
79 | * @param list 包含任意多个元素的容器。
80 | * @param delimiter 分隔符。
81 | * @return 连接后的字符串。
82 | */
83 | public static String join(Collection> list, String delimiter) {
84 | if (list == null || list.isEmpty()) {
85 | return "";
86 | }
87 | if (delimiter == null) {
88 | delimiter = "";
89 | }
90 |
91 | StringBuilder s = new StringBuilder();
92 | boolean first = true;
93 | for (Object e : list) {
94 | if (first) {
95 | first = false;
96 | } else {
97 | s.append(delimiter);
98 | }
99 | s.append(e);
100 | }
101 | return s.toString();
102 | }
103 |
104 | /**
105 | * 合并两个数组。
106 | *
107 | * @param 数组元素的类型。
108 | * @param a 前半部分数组。
109 | * @param b 后半部分数组。
110 | * @return 合并后的数组。
111 | */
112 | public static E[] merge(E[] a, E[] b) {
113 | List list = merge(Arrays.asList(a), Arrays.asList(b));
114 | return list.toArray(a);
115 | }
116 |
117 | /**
118 | * 合并两个List。
119 | *
120 | * @param List元素的类型。
121 | * @param a 前半部分List。
122 | * @param b 后半部分List。
123 | * @return 合并后的List。
124 | */
125 | public static List merge(List a, List b) {
126 | List list = new ArrayList<>();
127 | list.addAll(a);
128 | list.addAll(b);
129 | return list;
130 | }
131 |
132 | /**
133 | * 从数组中删除所有与给定对象equals的元素。
134 | *
135 | * @param 元素类型。
136 | * @param a 给定数组。
137 | * @param e 给定对象
138 | * @return 去除e之后的数组。
139 | */
140 | public static E[] remove(E[] a, E e) {
141 | List list = remove(Arrays.asList(a), e);
142 | return (E[])list.toArray();
143 | }
144 |
145 | /**
146 | * 从List中删除所有与给定对象equals的元素。
147 | *
148 | * @param 元素类型。
149 | * @param a 给定List。
150 | * @param e 给定对象
151 | * @return 去除e之后的List。
152 | */
153 | public static List remove(List a, E e) {
154 | List list = new ArrayList<>();
155 | for (E o : a) {
156 | if (!o.equals(e)) {
157 | list.add(o);
158 | }
159 | }
160 | return list;
161 | }
162 |
163 | /**
164 | * 根据给定的下标,选出多个元素,并组成新的数组。
165 | *
166 | * @param 元素类型。
167 | * @param a 给定数组。
168 | * @param indexes 任意多个要获取的元素下标。
169 | * @return 根据下标获得的元素组成的新数组。
170 | */
171 | public static E[] valuesAt(E[] a, int... indexes) {
172 | List list = valuesAt(Arrays.asList(a), indexes);
173 | return (E[])list.toArray();
174 | }
175 |
176 | /**
177 | * 根据给定的下标,选出多个元素,并组成新的List。
178 | *
179 | * @param 元素类型。
180 | * @param from 给定List。
181 | * @param indexes 任意多个要获取的元素下标。
182 | * @return 根据下标获得的元素组成的新List。
183 | */
184 | public static List valuesAt(List from, int... indexes) {
185 | List list = new ArrayList<>();
186 | for (int i : indexes) {
187 | if (0 <= i && i < from.size()) {
188 | list.add(from.get(i));
189 | } else if (-from.size() <= i && i < 0) {
190 | list.add(from.get(from.size() + i));
191 | } else {
192 | list.add(null);
193 | }
194 | }
195 | return list;
196 | }
197 |
198 | /**
199 | * 同时给int数组中多个位置同时赋值。
200 | *
201 | * @param a 给定int数组。
202 | * @param indexes 待赋值的位置下标。
203 | * @param values 与下标一一对应的int值。
204 | * @return 返回赋值后的新int数组。
205 | */
206 | public static int[] assignAt(int[] a, Integer[] indexes, int... values) {
207 | if (indexes.length != values.length) {
208 | throw new IllegalArgumentException(String.format("index.length(%d) != values.length(%d)", indexes.length, values.length));
209 | }
210 | for (int i = 0; i < indexes.length; i++) {
211 | int index = indexes[i];
212 | if (0 <= index && index < a.length) {
213 | a[index] = values[i];
214 | } else if (-a.length <= index && index < 0) {
215 | a[a.length + index] = values[i];
216 | } else {
217 | throw new ArrayIndexOutOfBoundsException(index);
218 | }
219 | }
220 | return a;
221 | }
222 |
223 | /**
224 | * 同时给数组中多个位置同时赋值。
225 | *
226 | * @param 数组元素类型。
227 | * @param a 给定数组。
228 | * @param indexes 待赋值的位置下标。
229 | * @param values 与下标一一对应的值。
230 | * @return 返回赋值后的新数组。
231 | */
232 | public static E[] assignAt(E[] a, Integer[] indexes, E... values) {
233 | if (indexes.length != values.length) {
234 | throw new IllegalArgumentException(String.format("index.length(%d) != values.length(%d)", indexes.length, values.length));
235 | }
236 | for (int i = 0; i < indexes.length; i++) {
237 | int index = indexes[i];
238 | if (0 <= index && index < a.length) {
239 | a[index] = values[i];
240 | } else if (-a.length <= index && index < 0) {
241 | a[a.length + index] = values[i];
242 | } else {
243 | throw new ArrayIndexOutOfBoundsException(index);
244 | }
245 | }
246 | return a;
247 | }
248 |
249 | /**
250 | * 根据规定格式,对容器中的每个元素进行格式化,并返回格式化后的结果。
251 | *
252 | * @param from 包含任意多个元素的容器。
253 | * @param format 格式化模板,与printf兼容。
254 | * @return 格式化后的新列表。
255 | */
256 | public static List map(Collection> from, String format) {
257 | List to = new ArrayList<>(from.size());
258 | for (Object e : from) {
259 | to.add(String.format(format, e));
260 | }
261 | return to;
262 | }
263 |
264 | /**
265 | * 拆分容器,每份至多包含n个元素,将每堆元素连接成一个独立字符串。
266 | * @param from 包含任意多个元素的容器。
267 | * @param n 子元素个数。
268 | * @param delimiter 分隔符。
269 | * @return 拆分后的字符串List。
270 | */
271 | public static List partition(Collection from, int n, String delimiter) {
272 | List to = new ArrayList<>();
273 | List buffer = new ArrayList<>(n);
274 | for (String e : from) {
275 | buffer.add(e);
276 | if (buffer.size() >= n) {
277 | to.add(join(buffer, delimiter));
278 | buffer.clear();
279 | }
280 | }
281 | return to;
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/jactiverecord/README.md:
--------------------------------------------------------------------------------
1 | # jActiveRecord
2 |
3 | `jActiveRecord`是我根据自己的喜好用`Java`实现的对象关系映射(ORM)库,灵感来自`Ruby on Rails`的`ActiveRecord`。它拥有以下特色:
4 |
5 | 1. 零配置:无XML配置文件、无Annotation注解。
6 | 1. 零依赖:不依赖任何第三方库,运行环境为Java 6或以上版本。
7 | 1. 零SQL:无需显式地写任何SQL语句,甚至多表关联、分页等高级查询亦是如此。
8 | 1. 动态性:和其他库不同,无需为每张表定义一个相对应的静态类。表、表对象、行对象等都能动态创建和动态获取。
9 | 1. 简化:`jActiveRecord`虽是模仿`ActiveRecord`,它同时做了一些简化。例如,所有的操作仅涉及DB、Table和Record三个类,并且HasMany、HasAndBelongsToMany等关联对象职责单一化,容易理解。
10 | 1. 支持多数据库访问
11 | 1. 多线程安全
12 | 1. 支持事务
13 |
14 | # 入门
15 |
16 | 参考[Rails For Zombies](http://railsforzombies.org/),我们一步一步创建一套僵尸微博系统的数据层。
17 |
18 | ## 安装
19 |
20 | `jActiveRecord`采用`Maven`维护,并已发布到中央库,仅需在`pom.xml`中添加如下声明:
21 |
22 | ```xml
23 |
24 | me.zzp
25 | jactiverecord
26 | 2.3
27 |
28 | ```
29 |
30 | ## 连接数据库
31 |
32 | `jActiveRecord`的入口是`me.zzp.ar.DB`类,通过open这个静态方法创建数据库对象,open方法的参数与`java.sql.DriverManager#getConnection`兼容。
33 |
34 | ```java
35 | DB sqlite3 = DB.open("jdbc:sqlite::memory:");
36 | ```
37 |
38 | `DB#open`默认只创建一个数据库连接,因此内存型数据库能正常使用;真实项目中推荐使用`C3P0`等创建连接池。作为演示,此处用sqlite创建一个内存数据库。
39 |
40 | ## 创建表
41 |
42 | 首先要创建一张用户信息表,此处的用户当然是僵尸(Zombie),包含名字(name)和墓地(graveyard)两个信息。
43 |
44 | ```java
45 | Table Zombie = sqlite3.createTable("zombies", "name text", "graveyard text");
46 | ```
47 |
48 | `createTable`方法的第一个参数是数据库表的名字,之后可以跟随任意个描述字段的参数,格式是名字+类型,用空格隔开。
49 |
50 | `createTable`方法会自动添加一个自增长(auto increment)的`id`字段作为主键。由于各个数据库实现自增长字段的方式不同,目前`jActiveRecord`的“创建表”功能支持如下数据库:
51 |
52 | * HyperSQL
53 | * MySQL
54 | * PostgreSQL
55 | * SQLite
56 |
57 | 如果你使用的数据库不在上述列表中,可以自己实现`me.zzp.ar.d.Dialect`接口,并添加到`META-INF/services/me.zzp.ar.d.Dialect`。`jActiveRecord`采用`Java 6`的`ServiceLoader`自动加载实现`Dialect`接口的类。
58 |
59 | 此外`jActiveRecord`还会额外添加`created_at`和`updated_at`两个字段,类型均为`timestamp`,分别保存记录被创建和更新的时间。因此,上述代码总共创建了5个字段:`id`、`name`、`graveyard`、`created_at`和`updated_at`。
60 |
61 | ## 添加
62 |
63 | ```java
64 | Table Zombie = sqlite3.active("zombies");
65 | Zombie.create("name:", "Ash", "graveyard:", "Glen Haven Memorial Cemetery");
66 | Zombie.create("name", "Bob", "graveyard", "Chapel Hill Cemetery");
67 | Zombie.create("graveyard", "My Fathers Basement", "name", "Jim");
68 | ```
69 |
70 | 首先用`DB#active`获取之前创建的表对象,然后使用`Table#create`新增一条记录(并且立即返回刚创建的记录)。该方法可使用“命名参数”,来突显每个值的含义。由于Java语法不支持命名参数,因此列名末尾允许带一个冗余的冒号,即“name:”与“name”是等价的;此外键值对顺序无关,因此第三条名为“Jim”的僵尸记录也能成功创建。
71 |
72 | ## 查询
73 |
74 | `jActiveRecord`提供了下列查询方法:
75 |
76 | * `Record find(int id)`:返回指定`id`的记录。
77 | * `List all()`:返回符合约束的所有记录。
78 | * `List paging(int page, int size)`:基于`all()`的分页查询,`page`从`0`开始。
79 | * `Record first()`:基于`all()`,返回按`id`排序的第一条记录。
80 | * `Record last()`:基于`all()`,返回按`id`排序的最后一条记录。
81 | * `List where(String condition, Object... args)`:基于`all()`,返回符合条件的所有记录。条件表达式兼容`java.sql.PreparedStatement`。
82 | * `Record first(String condition, Object... args)`:基于`where()`,返回按`id`排序的第一条记录。
83 | * `Record last(String condition, Object... args)`:基于`where()`,返回按`id`排序的最后一条记录。
84 | * `List findBy(String key, Object value)`:基于`all()`,返回指定列与`value`相等的所有记录。
85 | * `Record findA(String key, Object value)`:基于`findBy()`,返回按`id`排序的第一条记录。
86 |
87 | `first`、`last`和`find`等方法仅返回一条记录;另一些方法可能返回多条记录,因此返回`List`。
88 |
89 | 例如,获得`id`为3的僵尸有以下方法:
90 |
91 | ```java
92 | Zombie.find(3);
93 | Zombie.findA("name", "Jim");
94 | Zombie.first("graveyard like ?", "My Father%");
95 | ```
96 |
97 | 数据库返回的记录被包装成`Record`对象,使用`Record#get`获取数据。借助泛型,能根据左值自动转换数据类型:
98 |
99 | ```java
100 | Record jim = Zombie.find(3);
101 | int id = jim.get("id");
102 | String name = jim.get("name");
103 | Timestamp createdAt = jim.get("created_at");
104 | ```
105 |
106 | 此外,`Record`同样提供了诸如`getInt`、`getStr`等常用类型的强制转换接口。
107 |
108 | `jActiveRecord`不使用`Bean`,因为`Bean`不通用,你不得不为每张表创建一个相应的`Bean`类;使用`Bean`除了能在编译期检查`getter`和`setter`的名字是否有拼写错误,没有任何好处;
109 |
110 | ## 更新
111 |
112 | 通过查询获得目标对象,接着可以做一些更新操作。例如将编号为3的僵尸的目的改成“Benny Hills Memorial”。
113 |
114 | 调用`Record#set`方法可更新记录中的值,然后调用`Record#save`或`Table#update`保存修改结果;或者调用`Record#update`一步完成更新和保存操作,该方法和`create`一样接受任意多个命名参数。
115 |
116 | ```java
117 | Record jim = Zombie.find(3);
118 | jim.set("graveyard", "Benny Hills Memorial").save();
119 | jim.update("graveyard:", "Benny Hills Memorial"); // Same with above
120 | ```
121 |
122 | ## 删除
123 |
124 | `Table#delete`和`Record#destroy`都能删除一条记录,`Table#purge`能删除当前约束下所有的记录。
125 |
126 | ```java
127 | Zombie.find(1).destroy();
128 | Zombie.delete(Zombie.find(1)); // Same with above
129 | ```
130 |
131 | 上述代码功能相同:删除`id`为1的僵尸。
132 |
133 | ## 关联
134 |
135 | 到了最精彩的部分了!ORM库除了将记录映射成对象,还要将表之间的关联信息面向对象化。
136 |
137 | `jActiveRecord`提供与RoR一样的四种关联关系,并做了简化:
138 |
139 | * Table#belongsTo
140 | * Table#hasOne
141 | * Table#hasMany
142 | * Table#hasAndBelongsToMany
143 |
144 | 每个方法接收一个字符串参数`name`作为关系的名字,并返回`Association`关联对象,拥有以下三个方法:
145 |
146 | * by:指定外键的名字,默认使用`name` + "_id"作为外键的名字。
147 | * in:指定关联表的名字,默认与`name`相同。
148 | * through:关联组合,参数为其他已经指定的关联的名字。即通过其他关联实现跨表访问(`join`多张表)。
149 |
150 | ### 一对多
151 |
152 | 回到僵尸微博系统的问题上,上面的章节仅创建了一张用户表,现在创建另一张表`tweets`保存微博信息:
153 |
154 | ```java
155 | Table Tweet = sqlite3.createTable("tweets", "zombie_id int", "content text");
156 | ```
157 |
158 | 其中`zombie_id`作为外键与`zombies`表的`id`像关联。即每个僵尸有多条相关联的微博,而每条微博仅有一个相关联的僵尸。`jActiveRecord`中用`hasMany`和`belongsTo`来描述这种“一对多”的关系。其中`hasMany`在“一”方使用,`belongsTo`在“多”放使用(即外键所在的表)。
159 |
160 | ```java
161 | Zombie.hasMany("tweets").by("zombie_id");
162 | Tweet.belongsTo("zombie").by("zombie_id").in("zombies");
163 | ```
164 |
165 | 接着,就能通过关联名从`Record`中获取关联对象了。例如,获取`Jim`的所有微博:
166 |
167 | ```java
168 | Record jim = Zombie.find(3);
169 | Table jimTweets = jim.get("tweets");
170 | for (Record tweet : jimTweets.all()) {
171 | // ...
172 | }
173 | ```
174 |
175 | 或者根据微博获得相应的僵尸信息:
176 |
177 | ```java
178 | Record zombie = Tweet.find(1).get("zombie");
179 | ```
180 |
181 | 你可能已经注意到了:`hasMany`会返回多条记录,因此返回`Table`类型;`belongsTo`永远只返回一条记录,因此返回`Record`。此外,还有一种特殊的一对多关系:`hasOne`,即“多”方有且仅有一条记录。`hasOne`的用法和`hasMany`相同,只是返回值是`Record`而不是`Table`。
182 |
183 | ### 关联组合
184 |
185 | 让我们再往微博系统中加入“评论”功能:
186 |
187 | ```java
188 | Table Comment = sqlite3.createTable("comments", "zombie_id int", "tweet_id", "content text");
189 | ```
190 |
191 | 一条微博可以收到多条评论;而一个僵尸有多条微博。因此,僵尸和收到的评论是一种组合的关系:僵尸`hasMany`微博`hasMany`评论。`jActiveRecord`提供`through`描述这种组合的关联关系。
192 |
193 | ```java
194 | Zombie.hasMany("tweets").by("zombie_id"); // has defined above
195 | Zombie.hasMany("receive_comments").by("tweet_id").through("tweets");
196 | Zombie.hasMany("send_comments").by("zombie_id").in("comments");
197 | ```
198 |
199 | 上面的规则描述了`Zombie`首先能找到`Tweet`,借助`Tweet.tweet_id`又能找到`Comment`。第三行代码描述`Zombie`通过`Comment`的`zombie_id`可直接获取发出去的评论。
200 |
201 | 事实上,`through`可用于组合任意类型的关联,例如`hasAndBelongsToMany`依赖`hasOne`、`belongsTo`依赖另一条`belongsTo`……
202 |
203 | ### 多对多
204 |
205 | RoR中多对多关联有`has_many through`和`has_and_belongs_to_many`两种方法,且功能上有重叠之处。`jActiveRecord`仅保留`hasAndBelongsToMany`这一种方式来描述多对多关联。多对多关联要求有一张独立的映射表,记录映射关系。即两个“多”方都没有包含彼此的外键,而是借助第三张表同时保存它们的外键。
206 |
207 | 例如,为每条微博添加所在城市的信息,而城市单独作为一张表。
208 |
209 | ```java
210 | sqlite3.dropTable("tweets");
211 | Tweet = sqlite3.createTable("tweets", "zombie_id int", "city_id int", "content text");
212 | Table City = sqlite3.createTable("cities", "name text");
213 | ```
214 |
215 | 其中表`cities`包含所有城市的信息,`tweets`记录僵尸和城市的关联关系。`Zombie`为了自己去过的`City`,它首先要连接到表`tweets`,再通过它访问`cities`。
216 |
217 | ```java
218 | Zombie.hasMany("tweets").by("zombie_id"); // has defined above
219 | Zombie.hasAndBelongsToMany("travelled_cities").by("city_id").in("cities").through("tweets");
220 | ```
221 |
222 | 顾名思义,多对多的关联返回的类型一定是`Table`而不是`Record`。
223 |
224 | ### 关联总结
225 |
226 | * 一对一:有外键的表用`belongsTo`;无外键的表用`hasOne`。
227 | * 一对多:有外键的表用`belongsTo`;无外键的表用`hasMany`。
228 | * 多对多:两个多方都用`hasAndBelongsToMany`;映射表用`belongsTo`。
229 |
230 | 通过`through`可以任意组合其他关联。
231 |
232 | # 总结
233 |
234 | 本文通过一个微博系统的例子,介绍了`jActiveRecord`的常用功能。更多特性请访问本站[Wiki](https://github.com/redraiment/jactiverecord/wiki)。
235 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/pool/SingletonConnection.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar.pool;
2 |
3 | import java.sql.Array;
4 | import java.sql.Blob;
5 | import java.sql.CallableStatement;
6 | import java.sql.Clob;
7 | import java.sql.Connection;
8 | import java.sql.DatabaseMetaData;
9 | import java.sql.NClob;
10 | import java.sql.PreparedStatement;
11 | import java.sql.SQLClientInfoException;
12 | import java.sql.SQLException;
13 | import java.sql.SQLWarning;
14 | import java.sql.SQLXML;
15 | import java.sql.Savepoint;
16 | import java.sql.Statement;
17 | import java.sql.Struct;
18 | import java.util.Map;
19 | import java.util.Properties;
20 | import java.util.concurrent.Executor;
21 |
22 | /**
23 | *
24 | * @author redraiment
25 | * @since 2.2
26 | * @see SingleConnectionDataSource
27 | */
28 | final class SingletonConnection implements Connection {
29 | private final Connection c;
30 |
31 | SingletonConnection(Connection c) {
32 | this.c = c;
33 | }
34 |
35 | @Override
36 | public Statement createStatement() throws SQLException {
37 | return c.createStatement();
38 | }
39 |
40 | @Override
41 | public PreparedStatement prepareStatement(String sql) throws SQLException {
42 | return new SingletonPreparedStatement(this, c.prepareStatement(sql));
43 | }
44 |
45 | @Override
46 | public CallableStatement prepareCall(String sql) throws SQLException {
47 | return c.prepareCall(sql);
48 | }
49 |
50 | @Override
51 | public String nativeSQL(String sql) throws SQLException {
52 | return c.nativeSQL(sql);
53 | }
54 |
55 | @Override
56 | public void setAutoCommit(boolean autoCommit) throws SQLException {
57 | c.setAutoCommit(autoCommit);
58 | }
59 |
60 | @Override
61 | public boolean getAutoCommit() throws SQLException {
62 | return c.getAutoCommit();
63 | }
64 |
65 | @Override
66 | public void commit() throws SQLException {
67 | c.commit();
68 | }
69 |
70 | @Override
71 | public void rollback() throws SQLException {
72 | c.rollback();
73 | }
74 |
75 | @Override
76 | public void close() throws SQLException {
77 | }
78 |
79 | @Override
80 | public boolean isClosed() throws SQLException {
81 | return c.isClosed();
82 | }
83 |
84 | @Override
85 | public DatabaseMetaData getMetaData() throws SQLException {
86 | return c.getMetaData();
87 | }
88 |
89 | @Override
90 | public void setReadOnly(boolean readOnly) throws SQLException {
91 | c.setReadOnly(readOnly);
92 | }
93 |
94 | @Override
95 | public boolean isReadOnly() throws SQLException {
96 | return c.isReadOnly();
97 | }
98 |
99 | @Override
100 | public void setCatalog(String catalog) throws SQLException {
101 | c.setCatalog(catalog);
102 | }
103 |
104 | @Override
105 | public String getCatalog() throws SQLException {
106 | return c.getCatalog();
107 | }
108 |
109 | @Override
110 | public void setTransactionIsolation(int level) throws SQLException {
111 | c.setTransactionIsolation(level);
112 | }
113 |
114 | @Override
115 | public int getTransactionIsolation() throws SQLException {
116 | return c.getTransactionIsolation();
117 | }
118 |
119 | @Override
120 | public SQLWarning getWarnings() throws SQLException {
121 | return c.getWarnings();
122 | }
123 |
124 | @Override
125 | public void clearWarnings() throws SQLException {
126 | c.clearWarnings();
127 | }
128 |
129 | @Override
130 | public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
131 | return c.createStatement(resultSetType, resultSetConcurrency);
132 | }
133 |
134 | @Override
135 | public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
136 | return new SingletonPreparedStatement(this, c.prepareStatement(sql, resultSetType, resultSetConcurrency));
137 | }
138 |
139 | @Override
140 | public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
141 | return c.prepareCall(sql, resultSetType, resultSetConcurrency);
142 | }
143 |
144 | @Override
145 | public Map> getTypeMap() throws SQLException {
146 | return c.getTypeMap();
147 | }
148 |
149 | @Override
150 | public void setTypeMap(Map> map) throws SQLException {
151 | c.setTypeMap(map);
152 | }
153 |
154 | @Override
155 | public void setHoldability(int holdability) throws SQLException {
156 | c.setHoldability(holdability);
157 | }
158 |
159 | @Override
160 | public int getHoldability() throws SQLException {
161 | return c.getHoldability();
162 | }
163 |
164 | @Override
165 | public Savepoint setSavepoint() throws SQLException {
166 | return c.setSavepoint();
167 | }
168 |
169 | @Override
170 | public Savepoint setSavepoint(String name) throws SQLException {
171 | return c.setSavepoint(name);
172 | }
173 |
174 | @Override
175 | public void rollback(Savepoint savepoint) throws SQLException {
176 | c.rollback(savepoint);
177 | }
178 |
179 | @Override
180 | public void releaseSavepoint(Savepoint savepoint) throws SQLException {
181 | c.releaseSavepoint(savepoint);
182 | }
183 |
184 | @Override
185 | public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
186 | return c.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability);
187 | }
188 |
189 | @Override
190 | public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
191 | return new SingletonPreparedStatement(this, c.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability));
192 | }
193 |
194 | @Override
195 | public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
196 | return c.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
197 | }
198 |
199 | @Override
200 | public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
201 | return new SingletonPreparedStatement(this, c.prepareStatement(sql, autoGeneratedKeys));
202 | }
203 |
204 | @Override
205 | public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
206 | return new SingletonPreparedStatement(this, c.prepareStatement(sql, columnIndexes));
207 | }
208 |
209 | @Override
210 | public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
211 | return new SingletonPreparedStatement(this, c.prepareStatement(sql, columnNames));
212 | }
213 |
214 | @Override
215 | public Clob createClob() throws SQLException {
216 | return c.createClob();
217 | }
218 |
219 | @Override
220 | public Blob createBlob() throws SQLException {
221 | return c.createBlob();
222 | }
223 |
224 | @Override
225 | public NClob createNClob() throws SQLException {
226 | return c.createNClob();
227 | }
228 |
229 | @Override
230 | public SQLXML createSQLXML() throws SQLException {
231 | return c.createSQLXML();
232 | }
233 |
234 | @Override
235 | public boolean isValid(int timeout) throws SQLException {
236 | return c.isValid(timeout);
237 | }
238 |
239 | @Override
240 | public void setClientInfo(String name, String value) throws SQLClientInfoException {
241 | c.setClientInfo(name, value);
242 | }
243 |
244 | @Override
245 | public void setClientInfo(Properties properties) throws SQLClientInfoException {
246 | c.setClientInfo(properties);
247 | }
248 |
249 | @Override
250 | public String getClientInfo(String name) throws SQLException {
251 | return c.getClientInfo(name);
252 | }
253 |
254 | @Override
255 | public Properties getClientInfo() throws SQLException {
256 | return c.getClientInfo();
257 | }
258 |
259 | @Override
260 | public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
261 | return c.createArrayOf(typeName, elements);
262 | }
263 |
264 | @Override
265 | public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
266 | return c.createStruct(typeName, attributes);
267 | }
268 |
269 | @Override
270 | public void setSchema(String schema) throws SQLException {
271 | c.setSchema(schema);
272 | }
273 |
274 | @Override
275 | public String getSchema() throws SQLException {
276 | return c.getSchema();
277 | }
278 |
279 | @Override
280 | public void abort(Executor executor) throws SQLException {
281 | c.abort(executor);
282 | }
283 |
284 | @Override
285 | public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
286 | c.setNetworkTimeout(executor, milliseconds);
287 | }
288 |
289 | @Override
290 | public int getNetworkTimeout() throws SQLException {
291 | return c.getNetworkTimeout();
292 | }
293 |
294 | @Override
295 | public T unwrap(Class iface) throws SQLException {
296 | return c.unwrap(iface);
297 | }
298 |
299 | @Override
300 | public boolean isWrapperFor(Class> iface) throws SQLException {
301 | return c.isWrapperFor(iface);
302 | }
303 | }
304 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/Table.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar;
2 |
3 | import java.lang.reflect.Method;
4 | import java.sql.PreparedStatement;
5 | import java.sql.ResultSet;
6 | import java.sql.ResultSetMetaData;
7 | import java.sql.SQLException;
8 | import java.sql.Types;
9 | import java.util.ArrayList;
10 | import java.util.Collections;
11 | import java.util.HashMap;
12 | import java.util.LinkedHashMap;
13 | import java.util.LinkedList;
14 | import java.util.List;
15 | import java.util.Map;
16 | import me.zzp.ar.ex.IllegalFieldNameException;
17 | import me.zzp.ar.ex.SqlExecuteException;
18 | import me.zzp.ar.sql.SqlBuilder;
19 | import me.zzp.ar.sql.TSqlBuilder;
20 | import me.zzp.util.Seq;
21 |
22 | /**
23 | * 表对象。
24 | *
25 | * @since 1.0
26 | * @author redraiment
27 | */
28 | public final class Table {
29 | final DB dbo;
30 | final String name;
31 | final Map columns;
32 | final Map relations;
33 | final Map hooks;
34 | final String primaryKey;
35 |
36 | private String foreignTable;
37 | private final Map foreignKeys = new HashMap<>();
38 |
39 | Table(DB dbo, String name, Map columns, Map relations, Map hooks) {
40 | this.dbo = dbo;
41 | this.name = name;
42 | this.columns = columns;
43 | this.relations = relations;
44 | this.hooks = hooks;
45 | this.primaryKey = name.concat(".id");
46 | }
47 |
48 | /**
49 | * 继承给定的JavaBean,扩展Record对象的get和set方法。
50 | *
51 | * @param bean 希望被继承的JavaBean
52 | * @return 返回Table自身
53 | * @since 2.3
54 | */
55 | public Table extend(Object bean) {
56 | Class> type = bean.getClass();
57 | for (Method method : type.getDeclaredMethods()) {
58 | Class> returnType = method.getReturnType();
59 | Class>[] params = method.getParameterTypes();
60 | String key = method.getName();
61 |
62 | if (params.length == 2
63 | && key.length() > 3
64 | && (key.startsWith("get") || key.startsWith("set"))
65 | && params[0].isAssignableFrom(Record.class)
66 | && params[1].isAssignableFrom(Object.class)
67 | && Object.class.isAssignableFrom(returnType)) {
68 | key = key.replaceAll("(?=[A-Z])", "_").toLowerCase();
69 | hooks.put(key, new Lambda(bean, method));
70 | }
71 | }
72 |
73 | return this;
74 | }
75 |
76 | public Map getColumns() {
77 | return Collections.unmodifiableMap(columns);
78 | }
79 |
80 | /* Association */
81 | private Association assoc(String name, boolean onlyOne, boolean ancestor) {
82 | name = DB.parseKeyParameter(name);
83 | Association assoc = new Association(relations, name, onlyOne, ancestor);
84 | relations.put(name, assoc);
85 | return assoc;
86 | }
87 |
88 | public Association belongsTo(String name) {
89 | return assoc(name, true, false);
90 | }
91 |
92 | public Association hasOne(String name) {
93 | return assoc(name, true, true);
94 | }
95 |
96 | public Association hasMany(String name) {
97 | return assoc(name, false, true);
98 | }
99 |
100 | public Association hasAndBelongsToMany(String name) {
101 | return assoc(name, false, false);
102 | }
103 |
104 | private String[] getForeignKeys() {
105 | List conditions = new ArrayList<>();
106 | for (Map.Entry e : foreignKeys.entrySet()) {
107 | conditions.add(String.format("%s.%s = %d", name, e.getKey(), e.getValue()));
108 | }
109 | return conditions.toArray(new String[0]);
110 | }
111 |
112 | public Table constrain(String key, int id) {
113 | foreignKeys.put(DB.parseKeyParameter(key), id);
114 | return this;
115 | }
116 |
117 | public Table join(String table) {
118 | this.foreignTable = table;
119 | return this;
120 | }
121 |
122 | /* CRUD */
123 | public Record create(Object... args) {
124 | Map data = new HashMap<>();
125 | data.putAll(foreignKeys);
126 | for (int i = 0; i < args.length; i += 2) {
127 | String key = DB.parseKeyParameter(args[i].toString());
128 | if (!columns.containsKey(key)) {
129 | throw new IllegalFieldNameException(key);
130 | }
131 | Object value = args[i + 1];
132 | data.put(key, value);
133 | }
134 |
135 | String[] fields = new String[data.size() + 2];
136 | int[] types = new int[data.size() + 2];
137 | Object[] values = new Object[data.size() + 2];
138 | int index = 0;
139 | for (Map.Entry e : data.entrySet()) {
140 | fields[index] = e.getKey();
141 | types[index] = columns.get(e.getKey());
142 | values[index] = e.getValue();
143 | index++;
144 | }
145 | Seq.assignAt(fields, Seq.array(-2, -1), "created_at", "updated_at");
146 | Seq.assignAt(types, Seq.array(-2, -1), Types.TIMESTAMP, Types.TIMESTAMP);
147 | Seq.assignAt(values, Seq.array(-2, -1), DB.now(), DB.now());
148 |
149 | SqlBuilder sql = new TSqlBuilder();
150 | sql.insert().into(name).values(fields);
151 | PreparedStatement call = dbo.prepare(sql.toString(), values, types);
152 | try {
153 | int id = 0;
154 | if (call.executeUpdate() > 0) {
155 | ResultSet rs = call.getGeneratedKeys();
156 | if (rs != null && rs.next()) {
157 | id = rs.getInt(1);
158 | rs.close();
159 | }
160 | }
161 | return id > 0 ? find(id) : null;
162 | } catch (SQLException e) {
163 | throw new SqlExecuteException(sql.toString(), e);
164 | } finally {
165 | dbo.close(call);
166 | }
167 | }
168 |
169 | /**
170 | * 根据现有的Record创建新的Record.
171 | * 为跨数据库之间导数据提供便捷接口;同时也方便根据模板创建多条相似的纪录。
172 | * @param o Record对象
173 | * @return 根据参数创建的新的Record对象
174 | */
175 | public Record create(Record o) {
176 | List