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 |
--------------------------------------------------------------------------------
/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-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-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-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/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/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 |
--------------------------------------------------------------------------------
/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-el/src/main/resources/META-INF/web-fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | me.zzp.ar.el.ResolverSetup
5 |
6 |
7 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/jactiverecord/src/main/java/me/zzp/ar/DB.java:
--------------------------------------------------------------------------------
1 | package me.zzp.ar;
2 |
3 | import java.sql.Connection;
4 | import java.sql.DatabaseMetaData;
5 | import java.sql.PreparedStatement;
6 | import java.sql.ResultSet;
7 | import java.sql.SQLException;
8 | import java.sql.Statement;
9 | import java.sql.Timestamp;
10 | import java.util.Arrays;
11 | import java.util.HashMap;
12 | import java.util.HashSet;
13 | import java.util.LinkedHashMap;
14 | import java.util.Map;
15 | import java.util.Properties;
16 | import java.util.ServiceLoader;
17 | import java.util.Set;
18 | import javax.sql.DataSource;
19 | import me.zzp.ar.d.Dialect;
20 | import me.zzp.ar.ex.DBOpenException;
21 | import me.zzp.ar.ex.IllegalTableNameException;
22 | import me.zzp.ar.ex.SqlExecuteException;
23 | import me.zzp.ar.ex.TransactionException;
24 | import me.zzp.ar.ex.UnsupportedDatabaseException;
25 | import me.zzp.ar.pool.SingletonDataSource;
26 | import me.zzp.util.Seq;
27 |
28 | /**
29 | * 数据库对象。
30 | *
31 | * @since 1.0
32 | * @author redraiment
33 | */
34 | public final class DB {
35 | private static final ServiceLoader dialects;
36 |
37 | static {
38 | dialects = ServiceLoader.load(Dialect.class);
39 | }
40 |
41 | public static DB open(String url) {
42 | return open(url, new Properties());
43 | }
44 |
45 | public static DB open(String url, String username, String password) {
46 | Properties info = new Properties();
47 | info.put("user", username);
48 | info.put("password", password);
49 | return open(url, info);
50 | }
51 |
52 | public static DB open(String url, Properties info) {
53 | try {
54 | return open(new SingletonDataSource(url, info));
55 | } catch (SQLException e) {
56 | throw new DBOpenException(e);
57 | }
58 | }
59 |
60 | public static DB open(DataSource pool) {
61 | try (Connection base = pool.getConnection()) {
62 | for (Dialect dialect : dialects) {
63 | if (dialect.accept(base)) {
64 | base.close();
65 | return new DB(pool, dialect);
66 | }
67 | }
68 |
69 | DatabaseMetaData meta = base.getMetaData();
70 | String version = String.format("%s %d.%d/%s", meta.getDatabaseProductName(),
71 | meta.getDatabaseMajorVersion(),
72 | meta.getDatabaseMinorVersion(),
73 | meta.getDatabaseProductVersion());
74 | throw new UnsupportedDatabaseException(version);
75 | } catch (SQLException e) {
76 | throw new DBOpenException(e);
77 | }
78 | }
79 |
80 | private final DataSource pool;
81 | private final InheritableThreadLocal base;
82 | private final Dialect dialect;
83 | private final Map> columns;
84 | private final Map> relations;
85 | private final Map> hooks;
86 |
87 | private DB(DataSource pool, Dialect dialect) {
88 | this.pool = pool;
89 | this.base = new InheritableThreadLocal<>();
90 | this.columns = new HashMap<>();
91 | this.relations = new HashMap<>();
92 | this.dialect = dialect;
93 | this.hooks = new HashMap<>();
94 | }
95 |
96 | private Connection getConnection() {
97 | try {
98 | return base.get() == null? pool.getConnection(): base.get();
99 | } catch (SQLException e) {
100 | throw new DBOpenException(e);
101 | }
102 | }
103 |
104 | void close(Connection c) {
105 | if (c != null && base.get() != c) {
106 | try {
107 | c.close();
108 | } catch (SQLException e) {
109 | throw new RuntimeException("close Connection fail", e);
110 | }
111 | }
112 | }
113 |
114 | void close(Statement s) {
115 | if (s != null) {
116 | try {
117 | Connection c = s.getConnection();
118 | s.close();
119 | close(c);
120 | } catch (SQLException e) {
121 | throw new RuntimeException("close Statement fail", e);
122 | }
123 | }
124 | }
125 |
126 | void close(ResultSet rs) {
127 | if (rs != null) {
128 | try {
129 | Statement s = rs.getStatement();
130 | rs.close();
131 | close(s);
132 | } catch (SQLException e) {
133 | throw new RuntimeException("close ResultSet fail", e);
134 | }
135 | }
136 | }
137 |
138 | public Set getTableNames() {
139 | Set tables = new HashSet();
140 | try (Connection c = pool.getConnection()) {
141 | DatabaseMetaData db = c.getMetaData();
142 | try (ResultSet rs = db.getTables(null, null, "%", new String[] {"TABLE"})) {
143 | while (rs.next()) {
144 | tables.add(rs.getString("table_name"));
145 | }
146 | }
147 | } catch (SQLException e) {
148 | throw new DBOpenException(e);
149 | }
150 | return tables;
151 | }
152 |
153 | public Set getTables() {
154 | Set tables = new HashSet();
155 | for (String name : getTableNames()) {
156 | tables.add(active(name));
157 | }
158 | return tables;
159 | }
160 |
161 | private Map getColumns(String name) throws SQLException {
162 | if (!columns.containsKey(name)) {
163 | synchronized (columns) {
164 | if (!columns.containsKey(name)) {
165 | String catalog, schema, table;
166 | String[] patterns = name.split("\\.");
167 | if (patterns.length == 1) {
168 | catalog = null;
169 | schema = null;
170 | table = patterns[0];
171 | } else if (patterns.length == 2) {
172 | catalog = null;
173 | schema = patterns[0];
174 | table = patterns[1];
175 | } else if (patterns.length == 3) {
176 | catalog = patterns[0];
177 | schema = patterns[1];
178 | table = patterns[2];
179 | } else {
180 | throw new IllegalArgumentException(String.format("Illegal table name: %s", name));
181 | }
182 |
183 | Map column = new LinkedHashMap<>();
184 | try (Connection c = pool.getConnection()) {
185 | DatabaseMetaData db = c.getMetaData();
186 | try (ResultSet rs = db.getColumns(catalog, schema, table, null)) {
187 | while (rs.next()) {
188 | String columnName = rs.getString("column_name");
189 | if (columnName.equalsIgnoreCase("id")
190 | || columnName.equalsIgnoreCase("created_at")
191 | || columnName.equalsIgnoreCase("updated_at")) {
192 | continue;
193 | }
194 | column.put(parseKeyParameter(columnName), rs.getInt("data_type"));
195 | }
196 | }
197 | }
198 | columns.put(name, column);
199 | }
200 | }
201 | }
202 | return columns.get(name);
203 | }
204 |
205 | public Table active(String name) {
206 | name = dialect.getCaseIdentifier(name);
207 |
208 | if (!relations.containsKey(name)) {
209 | synchronized (relations) {
210 | if (!relations.containsKey(name)) {
211 | relations.put(name, new HashMap());
212 | }
213 | }
214 | }
215 |
216 | if (!hooks.containsKey(name)) {
217 | synchronized (hooks) {
218 | if (!hooks.containsKey(name)) {
219 | hooks.put(name, new HashMap());
220 | }
221 | }
222 | }
223 |
224 | try {
225 | return new Table(this, name, getColumns(name), relations.get(name), hooks.get(name));
226 | } catch (SQLException e) {
227 | throw new IllegalTableNameException(name, e);
228 | }
229 | }
230 |
231 | public PreparedStatement prepare(String sql, Object[] params, int[] types) {
232 | Connection c = getConnection();
233 | try {
234 | PreparedStatement call;
235 | if (sql.trim().toLowerCase().startsWith("insert")) {
236 | call = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
237 | } else {
238 | call = c.prepareStatement(sql);
239 | }
240 |
241 | if (params != null && params.length > 0) {
242 | for (int i = 0; i < params.length; i++) {
243 | if (params[i] != null) {
244 | call.setObject(i + 1, params[i]);
245 | } else {
246 | call.setNull(i + 1, types[i]);
247 | }
248 | }
249 | }
250 | return call;
251 | } catch (SQLException e) {
252 | throw new SqlExecuteException(sql, e);
253 | }
254 | }
255 |
256 | public void execute(String sql, Object[] params, int[] types) {
257 | PreparedStatement call = prepare(sql, params, types);
258 | try {
259 | call.executeUpdate();
260 | } catch (SQLException e) {
261 | throw new SqlExecuteException(sql, e);
262 | } finally {
263 | close(call);
264 | }
265 | }
266 |
267 | public void execute(String sql) {
268 | execute(sql, null, null);
269 | }
270 |
271 | public ResultSet query(String sql, Object... params) {
272 | try {
273 | PreparedStatement call = prepare(sql, params, null);
274 | return call.executeQuery();
275 | } catch (SQLException e) {
276 | throw new SqlExecuteException(sql, e);
277 | }
278 | }
279 |
280 | public Table createTable(String name, String... columns) {
281 | String template = "create table %s (id %s, %s, created_at timestamp, updated_at timestamp)";
282 | execute(String.format(template, name, dialect.getIdentity(), Seq.join(Arrays.asList(columns), ", ")));
283 | return active(name);
284 | }
285 |
286 | public void dropTable(String name) {
287 | execute(String.format("drop table if exists %s", name));
288 | }
289 |
290 | public void createIndex(String name, String table, String... columns) {
291 | execute(String.format("create index %s on %s(%s)", name, table, Seq.join(Arrays.asList(columns), ", ")));
292 | }
293 |
294 | public void dropIndex(String name, String table) {
295 | execute(String.format("drop index %s", name));
296 | }
297 |
298 | /* Transaction */
299 | public void batch(Runnable transaction) {
300 | // TODO: 不支持嵌套事务
301 | try (Connection c = pool.getConnection()) {
302 | boolean commit = c.getAutoCommit();
303 | try {
304 | c.setAutoCommit(false);
305 | } catch (SQLException e) {
306 | throw new TransactionException("transaction setAutoCommit(false)", e);
307 | }
308 | base.set(c);
309 |
310 | try {
311 | transaction.run();
312 | } catch (RuntimeException e) {
313 | try {
314 | c.rollback();
315 | c.setAutoCommit(commit);
316 | } catch (SQLException ex) {
317 | throw new TransactionException("transaction rollback: " + ex.getMessage(), e);
318 | }
319 | throw e;
320 | }
321 |
322 | try {
323 | c.commit();
324 | } catch (SQLException e) {
325 | throw new TransactionException("transaction commit", e);
326 | }
327 | c.setAutoCommit(commit);
328 | } catch (SQLException e) {
329 | throw new DBOpenException(e);
330 | } finally {
331 | base.set(null);
332 | }
333 | }
334 |
335 | public boolean tx(Runnable transaction) {
336 | try {
337 | batch(transaction);
338 | } catch (Throwable e) {
339 | return false;
340 | }
341 | return true;
342 | }
343 |
344 | /* Utility */
345 | public static Timestamp now() {
346 | return new Timestamp(System.currentTimeMillis());
347 | }
348 |
349 | static String parseKeyParameter(String name) {
350 | name = name.toLowerCase();
351 | if (name.endsWith(":")) {
352 | name = name.substring(0, name.length() - 1);
353 | }
354 | return name;
355 | }
356 | }
357 |
--------------------------------------------------------------------------------
/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/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/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/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