├── .gitignore ├── src ├── test │ ├── java │ │ └── com │ │ │ └── activepersistence │ │ │ ├── repository │ │ │ ├── models │ │ │ │ ├── Gender.java │ │ │ │ ├── CommentRepository.java │ │ │ │ ├── ClientRepository.java │ │ │ │ ├── Comment.java │ │ │ │ ├── PostRepository.java │ │ │ │ ├── Client.java │ │ │ │ └── Post.java │ │ │ └── cases │ │ │ │ ├── QueryingTest.java │ │ │ │ ├── arel │ │ │ │ ├── DeleteManagerTest.java │ │ │ │ ├── UpdateManagerTest.java │ │ │ │ ├── SelectmanagerTest.java │ │ │ │ ├── EntityTest.java │ │ │ │ └── visitors │ │ │ │ │ └── ToJpqlTest.java │ │ │ │ ├── connectionadaters │ │ │ │ └── DatabaseStatementsTest.java │ │ │ │ ├── ScopingTest.java │ │ │ │ ├── relation │ │ │ │ ├── CalculationTest.java │ │ │ │ ├── FinderMethodsTest.java │ │ │ │ ├── SpawnMethodsTest.java │ │ │ │ ├── MutationTest.java │ │ │ │ └── QueryMethodsTest.java │ │ │ │ ├── PersistenceTest.java │ │ │ │ ├── RelationTest.java │ │ │ │ └── SanitizationTest.java │ │ │ └── IntegrationTest.java │ └── resources │ │ ├── datasets │ │ ├── comments.xml │ │ ├── clients.xml │ │ └── posts.xml │ │ ├── payara-resources.xml │ │ ├── arquillian.xml │ │ └── test-persistence.xml └── main │ └── java │ └── com │ └── activepersistence │ ├── repository │ ├── arel │ │ ├── nodes │ │ │ ├── Distinct.java │ │ │ ├── Avg.java │ │ │ ├── Max.java │ │ │ ├── Min.java │ │ │ ├── Sum.java │ │ │ ├── StringJoin.java │ │ │ ├── Count.java │ │ │ ├── Node.java │ │ │ ├── And.java │ │ │ ├── Assignment.java │ │ │ ├── Grouping.java │ │ │ ├── Constructor.java │ │ │ ├── JoinSource.java │ │ │ ├── Join.java │ │ │ ├── JpqlLiteral.java │ │ │ ├── Function.java │ │ │ ├── DeleteStatement.java │ │ │ ├── SelectStatement.java │ │ │ ├── UpdateStatement.java │ │ │ └── SelectCore.java │ │ ├── visitors │ │ │ ├── Visitable.java │ │ │ ├── Visitor.java │ │ │ └── ToJpql.java │ │ ├── Source.java │ │ ├── TreeManager.java │ │ ├── Entity.java │ │ ├── DeleteManager.java │ │ ├── UpdateManager.java │ │ └── SelectManager.java │ ├── connectionadapters │ │ ├── QueryType.java │ │ ├── JpaAdapter.java │ │ ├── DatabaseStatements.java │ │ └── Quoting.java │ ├── relation │ │ ├── ValueMethods.java │ │ ├── SpawnMethods.java │ │ ├── Merger.java │ │ ├── FinderMethods.java │ │ ├── Calculation.java │ │ ├── Values.java │ │ └── QueryMethods.java │ ├── Arel.java │ ├── Core.java │ ├── ScopeRegistry.java │ ├── Base.java │ ├── Persistence.java │ ├── NullRelation.java │ ├── Callbacks.java │ ├── Scoping.java │ ├── Sanitization.java │ ├── Querying.java │ └── Relation.java │ ├── ReadOnlyRecord.java │ ├── PreparedStatementInvalid.java │ ├── ActivePersistenceError.java │ └── model │ ├── BaseIdentity.java │ └── Base.java ├── LICENSE ├── pom.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/models/Gender.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.models; 2 | 3 | public enum Gender { MALE, FEMALE; } 4 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/Distinct.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | public class Distinct extends Node { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/visitors/Visitable.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.visitors; 2 | 3 | public interface Visitable { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/connectionadapters/QueryType.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.connectionadapters; 2 | 3 | public enum QueryType { JPQL, SQL } 4 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/Source.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel; 2 | 3 | import com.activepersistence.repository.arel.visitors.Visitable; 4 | 5 | public interface Source extends Visitable { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/models/CommentRepository.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.models; 2 | 3 | import com.activepersistence.repository.Base; 4 | 5 | public class CommentRepository extends Base { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/ReadOnlyRecord.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence; 2 | 3 | public class ReadOnlyRecord extends ActivePersistenceError { 4 | 5 | public ReadOnlyRecord(String message) { 6 | super(message); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/Avg.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | public class Avg extends Function { 4 | 5 | public Avg(JpqlLiteral expression) { 6 | super(expression); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/Max.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | public class Max extends Function { 4 | 5 | public Max(JpqlLiteral expression) { 6 | super(expression); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/Min.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | public class Min extends Function { 4 | 5 | public Min(JpqlLiteral expression) { 6 | super(expression); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/Sum.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | public class Sum extends Function { 4 | 5 | public Sum(JpqlLiteral expression) { 6 | super(expression); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/PreparedStatementInvalid.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence; 2 | 3 | public class PreparedStatementInvalid extends ActivePersistenceError { 4 | 5 | public PreparedStatementInvalid(String message) { 6 | super(message); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/relation/ValueMethods.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.relation; 2 | 3 | public enum ValueMethods { 4 | INCLUDES, EAGER_LOAD, SELECT, GROUP, ORDER, JOINS, UNSCOPE, 5 | LIMIT, OFFSET, LOCK, DISTINCT, REORDERING, CONSTRUCTOR, READONLY, 6 | WHERE, HAVING, FROM 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/ActivePersistenceError.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence; 2 | 3 | import javax.persistence.PersistenceException; 4 | 5 | public class ActivePersistenceError extends PersistenceException { 6 | 7 | public ActivePersistenceError(String message) { 8 | super(message); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/StringJoin.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | import com.activepersistence.repository.arel.Source; 4 | 5 | public class StringJoin extends Join implements Source { 6 | 7 | public StringJoin(JpqlLiteral path) { 8 | super(path); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/test/resources/datasets/comments.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/Count.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | public class Count extends Function { 4 | 5 | public Count(JpqlLiteral expression) { 6 | super(expression); 7 | } 8 | 9 | public Count(JpqlLiteral expression, boolean distinct) { 10 | super(expression, distinct); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/models/ClientRepository.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.models; 2 | 3 | import com.activepersistence.repository.Base; 4 | import com.activepersistence.repository.Relation; 5 | 6 | public class ClientRepository extends Base { 7 | 8 | @Override 9 | public Relation defaultScope() { 10 | return where("client.active = true"); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/datasets/clients.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/Node.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | import com.activepersistence.repository.arel.Entity; 4 | import com.activepersistence.repository.arel.visitors.Visitable; 5 | 6 | public abstract class Node implements Visitable { 7 | 8 | public String toJpql() { 9 | var collector = new StringBuilder(); 10 | collector = Entity.visitor.accept(this, collector); 11 | return collector.toString(); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/And.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | import com.activepersistence.repository.arel.visitors.Visitable; 4 | import java.util.List; 5 | 6 | public class And extends Node { 7 | 8 | private final List children; 9 | 10 | public And(List children) { 11 | this.children = children; 12 | } 13 | 14 | public List getChildren() { 15 | return children; 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/TreeManager.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel; 2 | 3 | import com.activepersistence.repository.arel.visitors.Visitable; 4 | 5 | public abstract class TreeManager { 6 | 7 | public abstract Visitable getAst(); 8 | 9 | public abstract Visitable getCtx(); 10 | 11 | public String toJpql() { 12 | var collector = new StringBuilder(); 13 | collector = Entity.visitor.accept(getAst(), collector); 14 | return collector.toString(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/Assignment.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | public class Assignment extends Node { 4 | 5 | private final JpqlLiteral field; 6 | private final Object value; 7 | 8 | public Assignment(JpqlLiteral field, Object value) { 9 | this.field = field; 10 | this.value = value; 11 | } 12 | 13 | public JpqlLiteral getField() { 14 | return field; 15 | } 16 | 17 | public Object getValue() { 18 | return value; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/test/resources/payara-resources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/Grouping.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | import static com.activepersistence.repository.Arel.jpql; 4 | import com.activepersistence.repository.arel.visitors.Visitable; 5 | 6 | public class Grouping extends Node { 7 | 8 | private final Visitable value; 9 | 10 | public Grouping(Visitable value) { 11 | this.value = value; 12 | } 13 | 14 | public Grouping(String value) { 15 | this.value = jpql(value); 16 | } 17 | 18 | public Visitable getValue() { 19 | return value; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/model/BaseIdentity.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.model; 2 | 3 | import javax.persistence.GeneratedValue; 4 | import static javax.persistence.GenerationType.IDENTITY; 5 | import javax.persistence.Id; 6 | import javax.persistence.MappedSuperclass; 7 | 8 | @MappedSuperclass 9 | public abstract class BaseIdentity extends Base { 10 | 11 | @Id @GeneratedValue(strategy = IDENTITY) 12 | private Long id; 13 | 14 | @Override 15 | public Long getId() { 16 | return id; 17 | } 18 | 19 | @Override 20 | public void setId(Long id) { 21 | this.id = id; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/test/resources/datasets/posts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/Constructor.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | import com.activepersistence.repository.arel.visitors.Visitable; 4 | import java.util.List; 5 | 6 | public class Constructor extends Node { 7 | 8 | private final String name; 9 | 10 | private final List projections; 11 | 12 | public Constructor(String name, List projections) { 13 | this.name = name; 14 | this.projections = projections; 15 | } 16 | 17 | public List getProjections() { 18 | return projections; 19 | } 20 | 21 | public String getName() { 22 | return name; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/resources/arquillian.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | jdbc/arquillian 9 | 10 | 11 | 12 | SET REFERENTIAL_INTEGRITY FALSE 13 | SET REFERENTIAL_INTEGRITY TRUE 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/JoinSource.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | import com.activepersistence.repository.arel.Source; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public class JoinSource extends Node { 8 | 9 | private Source source; 10 | 11 | private final List joins = new ArrayList(); 12 | 13 | public JoinSource(Source source) { 14 | this.source = source; 15 | } 16 | 17 | public Source getSource() { 18 | return source; 19 | } 20 | 21 | public void setSource(Source source) { 22 | this.source = source; 23 | } 24 | 25 | public List getJoins() { 26 | return joins; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/Arel.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository; 2 | 3 | import com.activepersistence.repository.arel.nodes.JpqlLiteral; 4 | import com.activepersistence.repository.arel.nodes.StringJoin; 5 | import static java.util.Arrays.stream; 6 | import java.util.List; 7 | import static java.util.stream.Collectors.toList; 8 | 9 | public class Arel { 10 | 11 | public static JpqlLiteral jpql(String rawJpql) { 12 | return new JpqlLiteral(rawJpql); 13 | } 14 | 15 | public static List jpqlList(String[] rawJpqls) { 16 | return stream(rawJpqls).map(v -> jpql(v)).collect(toList()); 17 | } 18 | 19 | public static StringJoin createStringJoin(String to) { 20 | return new StringJoin(jpql(to)); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/IntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence; 2 | 3 | import org.jboss.arquillian.container.test.api.Deployment; 4 | import org.jboss.arquillian.junit.Arquillian; 5 | import org.jboss.shrinkwrap.api.ShrinkWrap; 6 | import org.jboss.shrinkwrap.api.asset.EmptyAsset; 7 | import org.jboss.shrinkwrap.api.spec.WebArchive; 8 | import org.junit.runner.RunWith; 9 | 10 | @RunWith(Arquillian.class) 11 | public abstract class IntegrationTest { 12 | 13 | @Deployment 14 | public static WebArchive createDeployment() { 15 | return ShrinkWrap.create(WebArchive.class, "test.war") 16 | .addPackages(true, "com.activepersistence") 17 | .addAsResource("test-persistence.xml", "META-INF/persistence.xml") 18 | .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml"); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/test/resources/test-persistence.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | jdbc/arquillian 4 | NONE 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/Core.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository; 2 | 3 | import com.activepersistence.repository.arel.Entity; 4 | import com.activepersistence.repository.relation.Values; 5 | import static java.beans.Introspector.decapitalize; 6 | 7 | public interface Core { 8 | 9 | public Class getEntityClass(); 10 | 11 | public default String getPrimaryKey() { 12 | return "id"; 13 | } 14 | 15 | public default String getAlias() { 16 | return decapitalize(getEntityClass().getSimpleName()); 17 | } 18 | 19 | public default Entity getArelEntity() { 20 | return new Entity(getEntityClass(), getAlias()); 21 | } 22 | 23 | public default Relation getRelation() { 24 | return new Relation(((Base) this), new Values()); 25 | } 26 | 27 | public default String getPrimaryKeyAttr() { 28 | return getAlias() + "." + getPrimaryKey(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/Join.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | public abstract class Join extends Node { 4 | 5 | private final JpqlLiteral path; 6 | 7 | private JpqlLiteral alias; 8 | 9 | private JpqlLiteral constraint; 10 | 11 | public Join(JpqlLiteral path) { 12 | this.path = path; 13 | } 14 | 15 | public Join(JpqlLiteral path, JpqlLiteral alias) { 16 | this.path = path; 17 | this.alias = alias; 18 | } 19 | 20 | public Join(JpqlLiteral path, JpqlLiteral alias, JpqlLiteral constraint) { 21 | this.path = path; 22 | this.alias = alias; 23 | this.constraint = constraint; 24 | } 25 | 26 | public JpqlLiteral getPath() { 27 | return path; 28 | } 29 | 30 | public JpqlLiteral getAlias() { 31 | return alias; 32 | } 33 | 34 | public JpqlLiteral getConstraint() { 35 | return constraint; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/JpqlLiteral.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | import com.activepersistence.repository.arel.Source; 4 | 5 | public class JpqlLiteral implements Source { 6 | 7 | private final String value; 8 | 9 | public JpqlLiteral(String value) { 10 | this.value = value; 11 | } 12 | 13 | public Count count() { 14 | return new Count(this); 15 | } 16 | 17 | public Count count(boolean distinct) { 18 | return new Count(this, distinct); 19 | } 20 | 21 | public Sum sum() { 22 | return new Sum(this); 23 | } 24 | 25 | public Max maximum() { 26 | return new Max(this); 27 | } 28 | 29 | public Min minimum() { 30 | return new Min(this); 31 | } 32 | 33 | public Avg average() { 34 | return new Avg(this); 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return value; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/models/Comment.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.models; 2 | 3 | import com.activepersistence.model.BaseIdentity; 4 | import javax.persistence.Entity; 5 | import javax.persistence.Lob; 6 | import javax.persistence.ManyToOne; 7 | 8 | @Entity 9 | public class Comment extends BaseIdentity { 10 | 11 | @Lob 12 | private String body; 13 | 14 | @ManyToOne 15 | private Post post; 16 | 17 | public Comment() { 18 | } 19 | 20 | public Comment(String body, Post post) { 21 | this.body = body; this.post = post; 22 | } 23 | 24 | // 25 | public String getBody() { 26 | return body; 27 | } 28 | 29 | public void setBody(String body) { 30 | this.body = body; 31 | } 32 | 33 | public Post getPost() { 34 | return post; 35 | } 36 | 37 | public void setPost(Post post) { 38 | this.post = post; 39 | } 40 | // 41 | 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/connectionadapters/JpaAdapter.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.connectionadapters; 2 | 3 | import com.activepersistence.repository.arel.visitors.ToJpql; 4 | import com.activepersistence.repository.arel.visitors.Visitor; 5 | import javax.persistence.EntityManager; 6 | 7 | public class JpaAdapter implements DatabaseStatements { 8 | 9 | private final EntityManager entityManager; 10 | 11 | private final Class entityClass; 12 | 13 | private final Visitor visitor; 14 | 15 | public JpaAdapter(EntityManager entityManager, Class entityClass) { 16 | this.entityManager = entityManager; 17 | this.entityClass = entityClass; 18 | this.visitor = new ToJpql(); 19 | } 20 | 21 | @Override 22 | public EntityManager getEntityManager() { 23 | return entityManager; 24 | } 25 | 26 | @Override 27 | public Class getEntityClass() { 28 | return entityClass; 29 | } 30 | 31 | @Override 32 | public Visitor getVisitor() { 33 | return visitor; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/QueryingTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases; 2 | 3 | import com.activepersistence.IntegrationTest; 4 | import com.activepersistence.repository.models.Post; 5 | import com.activepersistence.repository.models.PostRepository; 6 | import java.util.List; 7 | import javax.inject.Inject; 8 | import org.jboss.arquillian.persistence.UsingDataSet; 9 | import static org.junit.Assert.*; 10 | import org.junit.Test; 11 | 12 | @UsingDataSet({"posts.xml", "comments.xml"}) 13 | public class QueryingTest extends IntegrationTest { 14 | 15 | @Inject 16 | private PostRepository postRepository; 17 | 18 | @Test 19 | public void testFindBySql() { 20 | List results = postRepository.findBySql("SELECT id, title FROM Post WHERE id = 5").getResultList(); assertEquals("flood", results.get(0).getTitle()); 21 | } 22 | 23 | @Test 24 | public void testFindByJpql() { 25 | List results = postRepository.findByJpql("SELECT p FROM Post p WHERE p.id = 5").getResultList(); assertEquals("flood", results.get(0).getTitle()); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/Function.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | import static com.activepersistence.repository.Arel.jpql; 4 | 5 | public abstract class Function extends Node { 6 | 7 | private final JpqlLiteral expression; 8 | 9 | private JpqlLiteral alias; 10 | 11 | private boolean distinct; 12 | 13 | public Function(JpqlLiteral expression) { 14 | this.expression = expression; 15 | } 16 | 17 | public Function(JpqlLiteral expression, boolean distinct) { 18 | this.expression = expression; 19 | this.distinct = distinct; 20 | } 21 | 22 | public Function as(String alias) { 23 | this.alias = jpql(alias); 24 | return this; 25 | } 26 | 27 | public JpqlLiteral getExpression() { 28 | return expression; 29 | } 30 | 31 | public JpqlLiteral getAlias() { 32 | return alias; 33 | } 34 | 35 | public boolean isDistinct() { 36 | return distinct; 37 | } 38 | 39 | public void setDistinct(boolean distinct) { 40 | this.distinct = distinct; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/relation/SpawnMethods.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.relation; 2 | 3 | import com.activepersistence.repository.Base; 4 | import com.activepersistence.repository.Relation; 5 | 6 | public interface SpawnMethods { 7 | 8 | public Base getRepository(); 9 | 10 | public Values getValues(); 11 | 12 | public default Relation spawn() { 13 | return new Relation(thiz()); 14 | } 15 | 16 | public default Relation merge(Relation other) { 17 | return spawn().merge$(other); 18 | } 19 | 20 | public default Relation merge$(Relation other) { 21 | return new Merger(thiz(), other).merge(); 22 | } 23 | 24 | public default Relation except(ValueMethods... skips) { 25 | return relationWith(getValues().except(skips)); 26 | } 27 | 28 | public default Relation only(ValueMethods... onlies) { 29 | return relationWith(getValues().slice(onlies)); 30 | } 31 | 32 | private Relation relationWith(Values values) { 33 | return new Relation(getRepository(), values); 34 | } 35 | 36 | private Relation thiz() { 37 | return (Relation) this; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/arel/DeleteManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases.arel; 2 | 3 | import com.activepersistence.repository.arel.DeleteManager; 4 | import com.activepersistence.repository.arel.Entity; 5 | import com.activepersistence.repository.models.Post; 6 | import static org.junit.Assert.assertEquals; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | 10 | 11 | public class DeleteManagerTest { 12 | 13 | private DeleteManager manager; 14 | 15 | @Before 16 | public void setup() { 17 | manager = new DeleteManager(); 18 | manager.from(new Entity(Post.class, "post")); 19 | } 20 | 21 | @Test 22 | public void testAll() { 23 | assertEquals("DELETE FROM Post post", manager.toJpql()); 24 | } 25 | 26 | @Test 27 | public void testWhere() { 28 | assertEquals("DELETE FROM Post post WHERE 1=0", manager.where("1=0").toJpql()); 29 | } 30 | 31 | @Test 32 | public void testOrder() { 33 | assertEquals("DELETE FROM Post post ORDER BY post.id", manager.order("post.id").toJpql()); 34 | } 35 | 36 | @Test 37 | public void testWhereAnd() { 38 | assertEquals("DELETE FROM Post post WHERE 1=0 AND 1=2", manager.where("1=0").where("1=2").toJpql()); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/ScopeRegistry.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository; 2 | 3 | public class ScopeRegistry { 4 | 5 | private static final ThreadLocal currentScopeRegistry = new ThreadLocal(); 6 | 7 | private static final ThreadLocal ignoreDefaultRegistry = ThreadLocal.withInitial(() -> false); 8 | 9 | public enum ValidScopeTypes { CURRENT_SCOPE, IGNORE_DEFAULT_SCOPE } 10 | 11 | public static Object valueFor(ValidScopeTypes scopeType) { 12 | switch (scopeType) { 13 | case CURRENT_SCOPE: 14 | return currentScopeRegistry.get(); 15 | case IGNORE_DEFAULT_SCOPE: 16 | return ignoreDefaultRegistry.get(); 17 | default: 18 | throw new RuntimeException("Scope not supported: " + scopeType); 19 | } 20 | } 21 | 22 | public static void setValueFor(ValidScopeTypes scopeType, Object value) { 23 | switch (scopeType) { 24 | case CURRENT_SCOPE: 25 | currentScopeRegistry.set((Relation) value); 26 | break; 27 | case IGNORE_DEFAULT_SCOPE: 28 | ignoreDefaultRegistry.set((Boolean) value); 29 | break; 30 | default: 31 | throw new RuntimeException("Scope not supported: " + scopeType); 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/connectionadaters/DatabaseStatementsTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases.connectionadaters; 2 | 3 | import com.activepersistence.IntegrationTest; 4 | import static com.activepersistence.repository.connectionadapters.QueryType.JPQL; 5 | import static com.activepersistence.repository.connectionadapters.QueryType.SQL; 6 | import com.activepersistence.repository.models.Post; 7 | import com.activepersistence.repository.models.PostRepository; 8 | import java.util.List; 9 | import javax.inject.Inject; 10 | import org.jboss.arquillian.persistence.UsingDataSet; 11 | import static org.junit.Assert.assertEquals; 12 | import org.junit.Test; 13 | 14 | @UsingDataSet({"posts.xml", "comments.xml"}) 15 | public class DatabaseStatementsTest extends IntegrationTest { 16 | 17 | @Inject 18 | private PostRepository postRepository; 19 | 20 | @Test 21 | public void testSelectAll() { 22 | List results = postRepository.getConnection().selectAll("SELECT id, title FROM Post WHERE id = 5", SQL).getResultList(); assertEquals(5L, results.get(0)[0]); assertEquals("flood", results.get(0)[1]); 23 | } 24 | 25 | @Test 26 | public void testSelectEntity() { 27 | List results = postRepository.getConnection().selectAll("SELECT p FROM Post p WHERE p.id = 5", JPQL).getResultList(); assertEquals(5L, (long) results.get(0).getId()); assertEquals("flood", results.get(0).getTitle()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/Entity.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel; 2 | 3 | import com.activepersistence.repository.arel.visitors.ToJpql; 4 | import com.activepersistence.repository.arel.visitors.Visitor; 5 | 6 | public class Entity implements Source { 7 | 8 | public static Visitor visitor = new ToJpql(); 9 | 10 | private final Class klass; 11 | 12 | private final String alias; 13 | 14 | public Entity(Class klass, String alias) { 15 | this.klass = klass; 16 | this.alias = alias; 17 | } 18 | 19 | public SelectManager from() { 20 | return new SelectManager(this); 21 | } 22 | 23 | public SelectManager where(String condition) { 24 | return from().where(condition); 25 | } 26 | 27 | public SelectManager group(String... fields) { 28 | return from().group(fields); 29 | } 30 | 31 | public SelectManager having(String condition) { 32 | return from().having(condition); 33 | } 34 | 35 | public SelectManager order(String... expr) { 36 | return from().order(expr); 37 | } 38 | 39 | public SelectManager project(String... things) { 40 | return from().project(things); 41 | } 42 | 43 | public String getSimpleName() { 44 | return klass.getSimpleName(); 45 | } 46 | 47 | public String getName() { 48 | return klass.getName(); 49 | } 50 | 51 | public String getAlias() { 52 | return alias; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/DeleteStatement.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | import com.activepersistence.repository.arel.Entity; 4 | import com.activepersistence.repository.arel.visitors.Visitable; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class DeleteStatement extends Node { 9 | 10 | private Entity relation; 11 | 12 | private List wheres = new ArrayList(); 13 | 14 | private List orders = new ArrayList(); 15 | 16 | private int limit; 17 | 18 | private int offset; 19 | 20 | public Entity getRelation() { 21 | return relation; 22 | } 23 | 24 | public void setRelation(Entity relation) { 25 | this.relation = relation; 26 | } 27 | 28 | public List getWheres() { 29 | return wheres; 30 | } 31 | 32 | public void setWheres(List wheres) { 33 | this.wheres = wheres; 34 | } 35 | 36 | public List getOrders() { 37 | return orders; 38 | } 39 | 40 | public void setOrders(List orders) { 41 | this.orders = orders; 42 | } 43 | 44 | public int getLimit() { 45 | return limit; 46 | } 47 | 48 | public void setLimit(int limit) { 49 | this.limit = limit; 50 | } 51 | 52 | public int getOffset() { 53 | return offset; 54 | } 55 | 56 | public void setOffset(int offset) { 57 | this.offset = offset; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/visitors/Visitor.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.visitors; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.lang.reflect.Method; 5 | 6 | public abstract class Visitor { 7 | 8 | public abstract String compile(Visitable node, StringBuilder collector); 9 | 10 | public StringBuilder accept(Visitable object, StringBuilder collector) { 11 | return visit(object, collector); 12 | } 13 | 14 | public StringBuilder visit(Visitable o, StringBuilder collector) { 15 | var dispatchMethod = getMethodFor(o.getClass()); 16 | if (collector != null) { 17 | return send(dispatchMethod, o, collector); 18 | } else { 19 | return send(dispatchMethod, o); 20 | } 21 | } 22 | 23 | private Method getMethodFor(Class klass) { 24 | return getMethod("visit" + klass.getSimpleName(), klass, StringBuilder.class); 25 | } 26 | 27 | private Method getMethod(String methodName, Class... params) { 28 | try { 29 | return this.getClass().getMethod(methodName, params); 30 | } catch (NoSuchMethodException | SecurityException ex) { 31 | throw new RuntimeException("Cannot visit " + methodName, ex); 32 | } 33 | } 34 | 35 | private StringBuilder send(Method method, Object... params) { 36 | try { 37 | return (StringBuilder) method.invoke(this, params); 38 | } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { 39 | throw new RuntimeException("Cannot send method " + method.getName(), ex); 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/models/PostRepository.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.models; 2 | 3 | import com.activepersistence.repository.Base; 4 | import com.activepersistence.repository.Relation; 5 | 6 | public class PostRepository extends Base { 7 | 8 | public Relation oneEqZero() { 9 | return where("1=0"); 10 | } 11 | 12 | public Relation twoEqZero() { 13 | return where("2=0"); 14 | } 15 | 16 | @Override 17 | public void beforeSave(Post post) { 18 | if (post.getBody().equals("execBeforeSave")) post.setBody("OK"); 19 | } 20 | 21 | @Override 22 | public void afterSave(Post post) { 23 | if (post.getBody().equals("execAfterSave")) post.setBody("OK"); 24 | } 25 | 26 | @Override 27 | public void beforeCreate(Post post) { 28 | if (post.getBody().equals("execBeforeCreate")) post.setBody("OK"); 29 | } 30 | 31 | @Override 32 | public void afterCreate(Post post) { 33 | if (post.getBody().equals("execAfterCreate")) post.setBody("OK"); 34 | } 35 | 36 | @Override 37 | public void beforeUpdate(Post post) { 38 | if (post.getBody().equals("execBeforeUpdate")) post.setBody("OK"); 39 | } 40 | 41 | @Override 42 | public void afterUpdate(Post post) { 43 | if (post.getBody().equals("execAfterUpdate")) post.setBody("OK"); 44 | } 45 | 46 | @Override 47 | public void beforeDestroy(Post post) { 48 | if (post.getBody().equals("execBeforeDestroy")) post.setBody("OK"); 49 | } 50 | 51 | @Override 52 | public void afterDestroy(Post post) { 53 | if (post.getBody().equals("execAfterDestroy")) post.setBody("OK"); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/SelectStatement.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | import com.activepersistence.repository.arel.Entity; 4 | import com.activepersistence.repository.arel.visitors.Visitable; 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import javax.persistence.LockModeType; 9 | 10 | public class SelectStatement extends Node { 11 | 12 | private final SelectCore core; 13 | 14 | private final List orders = new ArrayList(); 15 | 16 | private HashMap hints = new HashMap(); 17 | 18 | private LockModeType lock = LockModeType.NONE; 19 | 20 | private int limit; 21 | 22 | private int offset; 23 | 24 | public SelectStatement(Entity entity) { 25 | this.core = new SelectCore(entity); 26 | } 27 | 28 | public SelectCore getCore() { 29 | return core; 30 | } 31 | 32 | public int getLimit() { 33 | return limit; 34 | } 35 | 36 | public void setLimit(int limit) { 37 | this.limit = limit; 38 | } 39 | 40 | public int getOffset() { 41 | return offset; 42 | } 43 | 44 | public void setOffset(int offset) { 45 | this.offset = offset; 46 | } 47 | 48 | public LockModeType getLock() { 49 | return lock; 50 | } 51 | 52 | public void setLock(LockModeType lock) { 53 | this.lock = lock; 54 | } 55 | 56 | public void setHints(HashMap hints) { 57 | this.hints = hints; 58 | } 59 | 60 | public HashMap getHints() { 61 | return hints; 62 | } 63 | 64 | public List getOrders() { 65 | return orders; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/UpdateStatement.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | import com.activepersistence.repository.arel.Entity; 4 | import com.activepersistence.repository.arel.visitors.Visitable; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class UpdateStatement extends Node { 9 | 10 | private Entity relation; 11 | 12 | private List wheres = new ArrayList(); 13 | 14 | private List orders = new ArrayList(); 15 | 16 | private List values = new ArrayList(); 17 | 18 | private int limit; 19 | 20 | private int offset; 21 | 22 | public Entity getRelation() { 23 | return relation; 24 | } 25 | 26 | public void setRelation(Entity relation) { 27 | this.relation = relation; 28 | } 29 | 30 | public List getWheres() { 31 | return wheres; 32 | } 33 | 34 | public void setWheres(List wheres) { 35 | this.wheres = wheres; 36 | } 37 | 38 | public List getOrders() { 39 | return orders; 40 | } 41 | 42 | public void setOrders(List orders) { 43 | this.orders = orders; 44 | } 45 | 46 | public List getValues() { 47 | return values; 48 | } 49 | 50 | public void setValues(List values) { 51 | this.values = values; 52 | } 53 | 54 | public int getLimit() { 55 | return limit; 56 | } 57 | 58 | public void setLimit(int limit) { 59 | this.limit = limit; 60 | } 61 | 62 | public int getOffset() { 63 | return offset; 64 | } 65 | 66 | public void setOffset(int offset) { 67 | this.offset = offset; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/ScopingTest.java: -------------------------------------------------------------------------------- 1 | 2 | package com.activepersistence.repository.cases; 3 | 4 | import com.activepersistence.IntegrationTest; 5 | import com.activepersistence.repository.models.ClientRepository; 6 | import com.activepersistence.repository.models.PostRepository; 7 | import javax.inject.Inject; 8 | import org.jboss.arquillian.persistence.UsingDataSet; 9 | import static org.junit.Assert.assertEquals; 10 | import org.junit.Test; 11 | 12 | @UsingDataSet({"posts.xml", "comments.xml"}) 13 | public class ScopingTest extends IntegrationTest { 14 | 15 | @Inject 16 | private PostRepository postRepository; 17 | 18 | @Inject 19 | private ClientRepository clientRepository; 20 | 21 | @Test 22 | public void testAll() { 23 | assertEquals("SELECT post FROM Post post", postRepository.all().toJpql()); 24 | } 25 | 26 | @Test 27 | public void testUnscoped() { 28 | assertEquals("SELECT client FROM Client client", clientRepository.unscoped().toJpql()); 29 | } 30 | 31 | @Test 32 | public void testUnscopedAfter() { 33 | assertEquals("SELECT client FROM Client client", clientRepository.where("1=0").unscoped().toJpql()); 34 | } 35 | 36 | @Test 37 | public void testUnscopedBlock() { 38 | assertEquals("SELECT client FROM Client client WHERE (1=0)", clientRepository.unscoped(() -> clientRepository.where("1=0")).toJpql()); 39 | } 40 | 41 | @Test 42 | public void testDefaultScope() { 43 | assertEquals("SELECT client FROM Client client WHERE (client.active = true)", clientRepository.all().toJpql()); 44 | } 45 | 46 | @Test 47 | public void testDefaultScopeWhere() { 48 | assertEquals("SELECT client FROM Client client WHERE (client.active = true) AND (1=0)", clientRepository.where("1=0").toJpql()); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/DeleteManager.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel; 2 | 3 | import static com.activepersistence.repository.Arel.jpql; 4 | import static com.activepersistence.repository.Arel.jpqlList; 5 | import com.activepersistence.repository.arel.nodes.DeleteStatement; 6 | import com.activepersistence.repository.arel.visitors.Visitable; 7 | import java.util.List; 8 | 9 | public class DeleteManager extends TreeManager { 10 | 11 | private final DeleteStatement ast; 12 | 13 | private final DeleteStatement ctx; 14 | 15 | public DeleteManager() { 16 | this.ast = new DeleteStatement(); 17 | this.ctx = ast; 18 | } 19 | 20 | public DeleteManager from(Entity entity) { 21 | ast.setRelation(entity); return this; 22 | } 23 | 24 | public DeleteManager limit(int limit) { 25 | ast.setLimit(limit); return this; 26 | } 27 | 28 | public DeleteManager offset(int offset) { 29 | ast.setOffset(offset); return this; 30 | } 31 | 32 | public DeleteManager where(String condition) { 33 | ctx.getWheres().add(jpql(condition)); return this; 34 | } 35 | 36 | public DeleteManager order(String... expr) { 37 | ast.getOrders().addAll(jpqlList(expr)); return this; 38 | } 39 | 40 | public void setWheres(List exprs) { 41 | ast.setWheres(exprs); 42 | } 43 | 44 | public void setOrders(List exprs) { 45 | ast.setOrders(exprs); 46 | } 47 | 48 | public int getLimit() { 49 | return ast.getLimit(); 50 | } 51 | 52 | public int getOffset() { 53 | return ast.getOffset(); 54 | } 55 | 56 | @Override 57 | public DeleteStatement getAst() { 58 | return ast; 59 | } 60 | 61 | @Override 62 | public DeleteStatement getCtx() { 63 | return ctx; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/Base.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository; 2 | 3 | import com.activepersistence.repository.connectionadapters.JpaAdapter; 4 | import java.lang.reflect.Array; 5 | import java.lang.reflect.ParameterizedType; 6 | import javax.persistence.EntityManager; 7 | import javax.persistence.PersistenceContext; 8 | 9 | public abstract class Base implements Core, Callbacks, Querying, Scoping, Sanitization { 10 | 11 | private final Class entityClass; 12 | 13 | @PersistenceContext 14 | private EntityManager entityManager; 15 | 16 | public Base() { 17 | entityClass = resolveEntityClass(); 18 | } 19 | 20 | @Override 21 | public Class getEntityClass() { 22 | return entityClass; 23 | } 24 | 25 | @Override 26 | public EntityManager getEntityManager() { 27 | return entityManager; 28 | } 29 | 30 | @Override 31 | public Relation getRelation() { 32 | return Core.super.getRelation(); 33 | } 34 | 35 | @Override 36 | public Relation all() { 37 | return Scoping.super.all(); 38 | } 39 | 40 | @Override 41 | public Class getRealClass() { 42 | return getClass().getSuperclass(); 43 | } 44 | 45 | public JpaAdapter getConnection() { 46 | return new JpaAdapter(getEntityManager(), entityClass); 47 | } 48 | 49 | @Override 50 | public Relation defaultScope() { 51 | throw new UnsupportedOperationException(); 52 | } 53 | 54 | // 55 | private Class resolveEntityClass() { 56 | return (Class) Array.get(getParameterizedType().getActualTypeArguments(), 0); 57 | } 58 | 59 | private ParameterizedType getParameterizedType() { 60 | return (ParameterizedType) getRealClass().getGenericSuperclass(); 61 | } 62 | // 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/arel/UpdateManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases.arel; 2 | 3 | import com.activepersistence.repository.arel.Entity; 4 | import com.activepersistence.repository.arel.UpdateManager; 5 | import com.activepersistence.repository.models.Post; 6 | import java.util.Map; 7 | import static org.junit.Assert.assertEquals; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | 11 | public class UpdateManagerTest { 12 | 13 | private UpdateManager manager; 14 | 15 | @Before 16 | public void setup() { 17 | manager = new UpdateManager(); 18 | manager.entity(new Entity(Post.class, "post")); 19 | } 20 | 21 | @Test 22 | public void testAll() { 23 | assertEquals("UPDATE Post post SET post.title = 'testing'", 24 | manager.set("post.title = 'testing'").toJpql()); 25 | } 26 | 27 | @Test 28 | public void testAllMap() { 29 | assertEquals("UPDATE Post post SET post.title = 'testing'", 30 | manager.set(Map.of("post.title", "testing")).toJpql()); 31 | } 32 | 33 | @Test 34 | public void testWhere() { 35 | assertEquals("UPDATE Post post SET post.title = 'testing' WHERE post.title = 'old'", 36 | manager.set("post.title = 'testing'").where("post.title = 'old'").toJpql()); 37 | } 38 | 39 | @Test 40 | public void testOrder() { 41 | assertEquals("UPDATE Post post SET post.title = 'testing' ORDER BY post.id", 42 | manager.set("post.title = 'testing'").order("post.id").toJpql()); 43 | } 44 | 45 | @Test 46 | public void testWhereAnd() { 47 | assertEquals("UPDATE Post post SET post.title = 'testing' WHERE post.title = 'old' AND post.likes_count > 0", 48 | manager.set("post.title = 'testing'").where("post.title = 'old'").where("post.likes_count > 0").toJpql()); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/nodes/SelectCore.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.nodes; 2 | 3 | import com.activepersistence.repository.arel.Entity; 4 | import com.activepersistence.repository.arel.visitors.Visitable; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class SelectCore extends Node { 9 | 10 | private final Entity entity; 11 | 12 | private JoinSource source; 13 | 14 | private Distinct setQuantifier; 15 | 16 | private Constructor constructor; 17 | 18 | private final List projections = new ArrayList(); 19 | private final List wheres = new ArrayList(); 20 | private final List groups = new ArrayList(); 21 | private final List havings = new ArrayList(); 22 | 23 | public SelectCore(Entity entity) { 24 | this.entity = entity; 25 | this.source = new JoinSource(entity); 26 | } 27 | 28 | public Distinct getSetQuantifier() { 29 | return setQuantifier; 30 | } 31 | 32 | public Constructor getConstructor() { 33 | return constructor; 34 | } 35 | 36 | public List getGroups() { 37 | return groups; 38 | } 39 | 40 | public List getHavings() { 41 | return havings; 42 | } 43 | 44 | public JoinSource getSource() { 45 | return source; 46 | } 47 | 48 | public void setSource(JoinSource source) { 49 | this.source = source; 50 | } 51 | 52 | public void setDistinct(boolean value) { 53 | this.setQuantifier = value ? new Distinct() : null; 54 | } 55 | 56 | public void setConstructor(boolean value) { 57 | this.constructor = value ? new Constructor(entity.getName(), projections) : null; 58 | } 59 | 60 | public List getProjections() { 61 | return projections; 62 | } 63 | 64 | public List getWheres() { 65 | return wheres; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/Persistence.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository; 2 | 3 | import com.activepersistence.model.Base; 4 | import javax.persistence.EntityManager; 5 | import static javax.persistence.LockModeType.NONE; 6 | import static javax.persistence.LockModeType.PESSIMISTIC_WRITE; 7 | import javax.transaction.Transactional; 8 | 9 | public interface Persistence { 10 | 11 | public EntityManager getEntityManager(); 12 | 13 | public Class getEntityClass(); 14 | 15 | public default T save(Base entity) { 16 | return createOrUpdate(entity); 17 | } 18 | 19 | public default void destroy(Base entity) { 20 | var em = getEntityManager(); 21 | 22 | if (entity.isPersisted()) { 23 | var existing = em.find(getEntityClass(), entity.getId()); 24 | 25 | if (existing == null) { 26 | // delete is a NOOP 27 | } else { 28 | em.remove(em.contains(entity) ? entity : em.merge(entity)); 29 | } 30 | } 31 | } 32 | 33 | @Transactional 34 | public default void reload(T entity) { 35 | reload(entity, false); 36 | } 37 | 38 | @Transactional 39 | public default void reload(T entity, boolean lock) { 40 | getEntityManager().refresh(entity, lock ? PESSIMISTIC_WRITE : NONE); 41 | } 42 | 43 | public default T _createRecord(T entity) { 44 | getEntityManager().persist(entity); getEntityManager().flush(); return entity; 45 | } 46 | 47 | public default T _updateRecord(T entity) { 48 | var merged = getEntityManager().merge(entity); getEntityManager().flush(); return merged; 49 | } 50 | 51 | private T createOrUpdate(Base entity) { 52 | if (entity.isDestroyed()) { 53 | return (T) entity; 54 | } else { 55 | if (entity.isNewRecord()) { 56 | return _createRecord((T) entity); 57 | } else { 58 | return _updateRecord((T) entity); 59 | } 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/NullRelation.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository; 2 | 3 | import static com.activepersistence.repository.relation.Calculation.Operations.AVG; 4 | import static com.activepersistence.repository.relation.Calculation.Operations.COUNT; 5 | import static com.activepersistence.repository.relation.Calculation.Operations.MAX; 6 | import static com.activepersistence.repository.relation.Calculation.Operations.MIN; 7 | import static com.activepersistence.repository.relation.Calculation.Operations.SUM; 8 | import java.util.ArrayList; 9 | import static java.util.Arrays.asList; 10 | import static java.util.Collections.emptyList; 11 | import static java.util.Collections.emptyMap; 12 | import java.util.List; 13 | 14 | public class NullRelation extends Relation { 15 | 16 | public NullRelation(Relation other) { 17 | super(other); 18 | } 19 | 20 | @Override 21 | public List pluck(String... fields) { 22 | return new ArrayList(); 23 | } 24 | 25 | @Override 26 | public int deleteAll() { 27 | return 0; 28 | } 29 | 30 | @Override 31 | public int updateAll(String updates) { 32 | return 0; 33 | } 34 | 35 | @Override 36 | public boolean isEmpty() { 37 | return true; 38 | } 39 | 40 | @Override 41 | public String toJpql() { 42 | return ""; 43 | } 44 | 45 | @Override 46 | public Object calculate(Operations operation, String field) { 47 | if (asList(COUNT, SUM).contains(operation)) { 48 | return getValues().getGroup().isEmpty() ? 0L : emptyMap(); 49 | } else if (asList(AVG, MIN, MAX).contains(operation)) { 50 | return getValues().getGroup().isEmpty() ? null : emptyMap(); 51 | } else { 52 | throw new RuntimeException("Operation not supported: " + operation); 53 | } 54 | } 55 | 56 | @Override 57 | public boolean exists() { 58 | return false; 59 | } 60 | 61 | @Override 62 | public List execQueries() { 63 | return emptyList(); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/Callbacks.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository; 2 | 3 | import com.activepersistence.model.Base; 4 | import java.util.function.Supplier; 5 | import javax.transaction.Transactional; 6 | 7 | public interface Callbacks extends Persistence { 8 | 9 | public default void beforeSave(T entity) {} 10 | 11 | public default void afterSave(T entity) {} 12 | 13 | public default void beforeCreate(T entity) {} 14 | 15 | public default void afterCreate(T entity) {} 16 | 17 | public default void beforeUpdate(T entity) {} 18 | 19 | public default void afterUpdate(T entity) {} 20 | 21 | public default void beforeDestroy(T entity) {} 22 | 23 | public default void afterDestroy(T entity) {} 24 | 25 | @Override @Transactional 26 | public default T save(Base entity) { 27 | return _runSaveCallbacks((T) entity,() -> Persistence.super.save(entity)); 28 | } 29 | 30 | @Override @Transactional 31 | public default void destroy(Base entity) { 32 | _runDestroyCallbacks((T) entity,() -> Persistence.super.destroy(entity)); 33 | } 34 | 35 | @Override 36 | public default T _createRecord(T entity) { 37 | return _runCreateCallbacks(entity,() -> Persistence.super._createRecord(entity)); 38 | } 39 | 40 | @Override 41 | public default T _updateRecord(T entity) { 42 | return _runUpdateCallbacks(entity,() -> Persistence.super._updateRecord(entity)); 43 | } 44 | 45 | private T _runSaveCallbacks(T entity, Supplier yield) { 46 | beforeSave(entity); var result = yield.get(); afterSave(result); return result; 47 | } 48 | 49 | private T _runCreateCallbacks(T entity, Supplier yield) { 50 | beforeCreate(entity); var result = yield.get(); afterCreate(result); return result; 51 | } 52 | 53 | private T _runUpdateCallbacks(T entity, Supplier yield) { 54 | beforeUpdate(entity); var result = yield.get(); afterUpdate(result); return result; 55 | } 56 | 57 | private void _runDestroyCallbacks(T entity, Runnable yield) { 58 | beforeDestroy(entity); yield.run(); afterDestroy(entity); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/arel/SelectmanagerTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases.arel; 2 | 3 | import com.activepersistence.repository.arel.Entity; 4 | import com.activepersistence.repository.arel.SelectManager; 5 | import com.activepersistence.repository.models.Post; 6 | import static org.junit.Assert.assertEquals; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | 10 | public class SelectmanagerTest { 11 | 12 | private SelectManager manager; 13 | 14 | @Before 15 | public void setup() { 16 | manager = new SelectManager(new Entity(Post.class, "post")); 17 | } 18 | 19 | @Test 20 | public void testProject() { 21 | assertEquals("SELECT FROM Post post", manager.toJpql()); 22 | } 23 | 24 | @Test 25 | public void testConstruct() { 26 | assertEquals("SELECT NEW com.activepersistence.repository.models.Post() FROM Post post", manager.constructor(true).toJpql()); 27 | } 28 | 29 | @Test 30 | public void testDistinct() { 31 | assertEquals("SELECT DISTINCT FROM Post post", manager.distinct(true).toJpql()); 32 | } 33 | 34 | @Test 35 | public void testFrom() { 36 | assertEquals("SELECT FROM Another post", manager.from("Another post").toJpql()); 37 | } 38 | 39 | @Test 40 | public void testJoin() { 41 | assertEquals("SELECT FROM Post post JOIN post.comments c", manager.join("JOIN post.comments c").toJpql()); 42 | } 43 | 44 | @Test 45 | public void testWhere() { 46 | assertEquals("SELECT FROM Post post WHERE post.id = 1", manager.where("post.id = 1").toJpql()); 47 | } 48 | 49 | @Test 50 | public void testGroup() { 51 | assertEquals("SELECT FROM Post post GROUP BY post.id, post.title", manager.group("post.id, post.title").toJpql()); 52 | } 53 | 54 | @Test 55 | public void testHaving() { 56 | assertEquals("SELECT FROM Post post HAVING SUM(post.commentsCount) > 1", manager.having("SUM(post.commentsCount) > 1").toJpql()); 57 | } 58 | 59 | @Test 60 | public void testOrder() { 61 | assertEquals("SELECT FROM Post post ORDER BY post.title", manager.order("post.title").toJpql()); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/models/Client.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.models; 2 | 3 | import com.activepersistence.model.BaseIdentity; 4 | import java.math.BigDecimal; 5 | import java.math.BigInteger; 6 | import javax.persistence.Entity; 7 | import javax.persistence.Enumerated; 8 | 9 | @Entity 10 | public class Client extends BaseIdentity { 11 | 12 | private String name; 13 | 14 | private Integer age; 15 | 16 | private boolean active; 17 | 18 | private Float weight; 19 | 20 | private Double ratio; 21 | 22 | private BigDecimal salary; 23 | 24 | @Enumerated 25 | private Gender gender; 26 | 27 | private BigInteger sequence; 28 | 29 | public Client() { 30 | } 31 | 32 | // 33 | public String getName() { 34 | return name; 35 | } 36 | 37 | public void setName(String name) { 38 | this.name = name; 39 | } 40 | 41 | public Integer getAge() { 42 | return age; 43 | } 44 | 45 | public void setAge(Integer age) { 46 | this.age = age; 47 | } 48 | 49 | public Gender getGender() { 50 | return gender; 51 | } 52 | 53 | public void setGender(Gender gender) { 54 | this.gender = gender; 55 | } 56 | 57 | public Float getWeight() { 58 | return weight; 59 | } 60 | 61 | public void setWeight(Float weight) { 62 | this.weight = weight; 63 | } 64 | 65 | public Double getRatio() { 66 | return ratio; 67 | } 68 | 69 | public void setRatio(Double ratio) { 70 | this.ratio = ratio; 71 | } 72 | 73 | public BigDecimal getSalary() { 74 | return salary; 75 | } 76 | 77 | public void setSalary(BigDecimal salary) { 78 | this.salary = salary; 79 | } 80 | 81 | public boolean isActive() { 82 | return active; 83 | } 84 | 85 | public void setActive(boolean active) { 86 | this.active = active; 87 | } 88 | 89 | public BigInteger getSequence() { 90 | return sequence; 91 | } 92 | 93 | public void setSequence(BigInteger sequence) { 94 | this.sequence = sequence; 95 | } 96 | // 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/UpdateManager.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel; 2 | 3 | import static com.activepersistence.repository.Arel.jpql; 4 | import static com.activepersistence.repository.Arel.jpqlList; 5 | import com.activepersistence.repository.arel.nodes.Assignment; 6 | import com.activepersistence.repository.arel.nodes.UpdateStatement; 7 | import com.activepersistence.repository.arel.visitors.Visitable; 8 | import static java.util.Arrays.asList; 9 | import java.util.List; 10 | import java.util.Map; 11 | import static java.util.stream.Collectors.toList; 12 | 13 | public class UpdateManager extends TreeManager { 14 | 15 | private final UpdateStatement ast; 16 | 17 | private final UpdateStatement ctx; 18 | 19 | public UpdateManager() { 20 | this.ast = new UpdateStatement(); 21 | this.ctx = ast; 22 | } 23 | 24 | public UpdateManager entity(Entity entity) { 25 | ast.setRelation(entity); return this; 26 | } 27 | 28 | public UpdateManager set(Map values) { 29 | ast.setValues(values.entrySet().stream().map(v -> new Assignment(jpql(v.getKey()), v.getValue())).collect(toList())); return this; 30 | } 31 | 32 | public UpdateManager set(String values) { 33 | ast.setValues(asList(jpql(values))); return this; 34 | } 35 | 36 | public UpdateManager limit(int limit) { 37 | ast.setLimit(limit); return this; 38 | } 39 | 40 | public UpdateManager offset(int offset) { 41 | ast.setOffset(offset); return this; 42 | } 43 | 44 | public UpdateManager where(String condition) { 45 | ctx.getWheres().add(jpql(condition)); return this; 46 | } 47 | 48 | public UpdateManager order(String... expr) { 49 | ast.getOrders().addAll(jpqlList(expr)); return this; 50 | } 51 | 52 | public void setWheres(List exprs) { 53 | ast.setWheres(exprs); 54 | } 55 | 56 | public void setOrders(List exprs) { 57 | ast.setOrders(exprs); 58 | } 59 | 60 | public int getLimit() { 61 | return ast.getLimit(); 62 | } 63 | 64 | public int getOffset() { 65 | return ast.getOffset(); 66 | } 67 | 68 | @Override 69 | public UpdateStatement getAst() { 70 | return ast; 71 | } 72 | 73 | @Override 74 | public UpdateStatement getCtx() { 75 | return ctx; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/connectionadapters/DatabaseStatements.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.connectionadapters; 2 | 3 | import com.activepersistence.repository.arel.DeleteManager; 4 | import com.activepersistence.repository.arel.SelectManager; 5 | import com.activepersistence.repository.arel.UpdateManager; 6 | import com.activepersistence.repository.arel.visitors.Visitor; 7 | import static com.activepersistence.repository.connectionadapters.QueryType.JPQL; 8 | import java.util.List; 9 | import java.util.Map; 10 | import javax.persistence.EntityManager; 11 | import javax.persistence.Query; 12 | 13 | public interface DatabaseStatements { 14 | 15 | public Visitor getVisitor(); 16 | 17 | public Class getEntityClass(); 18 | 19 | public EntityManager getEntityManager(); 20 | 21 | public default Query selectAll(String queryString, QueryType queryType) { 22 | if (queryType == JPQL) { 23 | return getEntityManager().createQuery(queryString); 24 | } else { 25 | return getEntityManager().createNativeQuery(queryString); 26 | } 27 | } 28 | 29 | public default List selectAll(SelectManager arel) { 30 | return setHints(createQuery(arel), arel.getHints()).getResultList(); 31 | } 32 | 33 | public default int update(UpdateManager arel) { 34 | return createQuery(arel).executeUpdate(); 35 | } 36 | 37 | public default int delete(DeleteManager arel) { 38 | return createQuery(arel).executeUpdate(); 39 | } 40 | 41 | public default String toJpql(SelectManager arel) { 42 | return getVisitor().compile(arel.getAst(), new StringBuilder()); 43 | } 44 | 45 | private Query createQuery(SelectManager arel) { 46 | return getEntityManager().createQuery(arel.toJpql()).setLockMode(arel.getLock()).setFirstResult(arel.getOffset()).setMaxResults(arel.getLimit()); 47 | } 48 | 49 | private Query createQuery(UpdateManager arel) { 50 | return getEntityManager().createQuery(arel.toJpql()).setFirstResult(arel.getOffset()).setMaxResults(arel.getLimit()); 51 | } 52 | 53 | private Query createQuery(DeleteManager arel) { 54 | return getEntityManager().createQuery(arel.toJpql()).setFirstResult(arel.getOffset()).setMaxResults(arel.getLimit()); 55 | } 56 | 57 | private Query setHints(Query query, Map hints) { 58 | hints.forEach(query::setHint); return query; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/models/Post.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.models; 2 | 3 | import com.activepersistence.model.BaseIdentity; 4 | import java.time.LocalDateTime; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import static javax.persistence.CascadeType.PERSIST; 8 | import static javax.persistence.CascadeType.REMOVE; 9 | import javax.persistence.Entity; 10 | import javax.persistence.OneToMany; 11 | 12 | @Entity 13 | public class Post extends BaseIdentity { 14 | 15 | private String title; 16 | 17 | private String body; 18 | 19 | private Integer likesCount; 20 | 21 | @OneToMany(mappedBy = "post", cascade = { PERSIST, REMOVE }) 22 | private List comments = new ArrayList(); 23 | 24 | private LocalDateTime createdAt; 25 | 26 | private LocalDateTime updatedAt; 27 | 28 | public Post() { 29 | } 30 | 31 | public Post(Long id, String title) { 32 | super.setId(id); this.title = title; 33 | } 34 | 35 | public Post(String title, String body, Integer likesCount) { 36 | this.title = title; this.body = body; this.likesCount = likesCount; 37 | } 38 | 39 | public Comment commentsBuild(String body) { 40 | var comment = new Comment(body, this); comments.add(comment); return comment; 41 | } 42 | 43 | // 44 | public String getTitle() { 45 | return title; 46 | } 47 | 48 | public void setTitle(String title) { 49 | this.title = title; 50 | } 51 | 52 | public String getBody() { 53 | return body; 54 | } 55 | 56 | public void setBody(String body) { 57 | this.body = body; 58 | } 59 | 60 | public Integer getLikesCount() { 61 | return likesCount; 62 | } 63 | 64 | public void setLikesCount(Integer likesCount) { 65 | this.likesCount = likesCount; 66 | } 67 | 68 | public List getComments() { 69 | return comments; 70 | } 71 | 72 | public LocalDateTime getCreatedAt() { 73 | return createdAt; 74 | } 75 | 76 | @Override 77 | public void setCreatedAt(LocalDateTime createdAt) { 78 | this.createdAt = createdAt; 79 | } 80 | 81 | public LocalDateTime getUpdatedAt() { 82 | return updatedAt; 83 | } 84 | 85 | @Override 86 | public void setUpdatedAt(LocalDateTime updatedAt) { 87 | this.updatedAt = updatedAt; 88 | } 89 | // 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/relation/Merger.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.relation; 2 | 3 | import com.activepersistence.repository.Relation; 4 | import javax.persistence.LockModeType; 5 | 6 | public class Merger { 7 | 8 | private final Relation relation; 9 | 10 | private final Relation other; 11 | 12 | private final Values values; 13 | 14 | public Merger(Relation relation, Relation other) { 15 | this.relation = relation; 16 | this.other = other; 17 | this.values = other.getValues(); 18 | } 19 | 20 | public Relation merge() { 21 | mergeSingleValues(); 22 | mergeMultiValues(); 23 | mergeClauses(); 24 | 25 | return relation; 26 | } 27 | 28 | private void mergeSingleValues() { 29 | if (shouldReplaceLockValue()) relation.lock$(values.getLock()); 30 | if (values.isDistinct() != false) relation.distinct$(values.isDistinct()); 31 | if (values.getLimit() != 0) relation.limit$(values.getLimit()); 32 | if (values.getOffset() != 0) relation.offset$(values.getOffset()); 33 | } 34 | 35 | private void mergeMultiValues() { 36 | relation.getValues().getWhere().addAll(values.getWhere()); 37 | relation.getValues().getHaving().addAll(values.getHaving()); 38 | 39 | values.getSelect().forEach(relation::select$); 40 | values.getJoins().forEach(relation::joins$); 41 | values.getGroup().forEach(relation::group$); 42 | values.getOrder().forEach(this::mergeOrder$); 43 | 44 | values.getIncludes().forEach(relation::includes$); 45 | values.getEagerLoad().forEach(relation::eagerLoad$); 46 | 47 | values.getUnscope().forEach(relation::unscope$); 48 | } 49 | 50 | private void mergeClauses() { 51 | if (shouldReplaceFromClause()) relation.getValues().setFrom(values.getFrom()); 52 | } 53 | 54 | private boolean shouldReplaceLockValue() { 55 | return relation.getValues().getLock() == LockModeType.NONE; 56 | } 57 | 58 | private boolean shouldReplaceFromClause() { 59 | return relation.getValues().getFrom() == null && values.getFrom() != null 60 | && relation.getEntityClass() == other.getEntityClass(); 61 | } 62 | 63 | private void mergeOrder$(String value) { 64 | if (values.isReordering()) { 65 | relation.reorder$(value); 66 | } else { 67 | relation.order$(value); 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/arel/EntityTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases.arel; 2 | 3 | import com.activepersistence.repository.arel.Entity; 4 | import com.activepersistence.repository.models.Post; 5 | import static org.junit.Assert.assertEquals; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | public class EntityTest { 10 | 11 | private Entity relation; 12 | 13 | @Before 14 | public void setup() { 15 | relation = new Entity(Post.class, "post"); 16 | } 17 | 18 | @Test 19 | public void testProject() { 20 | assertEquals("SELECT post FROM Post post", 21 | relation.project("post").toJpql()); 22 | } 23 | 24 | @Test 25 | public void testConstructor() { 26 | assertEquals("SELECT NEW com.activepersistence.repository.models.Post(post.id, post.title) FROM Post post", 27 | relation.project("post.id", "post.title").constructor(true).toJpql()); 28 | } 29 | 30 | @Test 31 | public void testDistinct() { 32 | assertEquals("SELECT DISTINCT post FROM Post post", 33 | relation.project("post").distinct(true).toJpql()); 34 | } 35 | 36 | @Test 37 | public void testJoin() { 38 | assertEquals("SELECT post FROM Post post JOIN post.comments c", 39 | relation.project("post").join("JOIN post.comments c").toJpql()); 40 | } 41 | 42 | @Test 43 | public void testWhere() { 44 | assertEquals("SELECT post FROM Post post WHERE post.id = 1", 45 | relation.project("post").where("post.id = 1").toJpql()); 46 | } 47 | 48 | @Test 49 | public void testGroup() { 50 | assertEquals("SELECT post.title, SUM(post.commentsCount) FROM Post post GROUP BY post.title", 51 | relation.project("post.title, SUM(post.commentsCount)").group("post.title").toJpql()); 52 | } 53 | 54 | @Test 55 | public void testHaving() { 56 | assertEquals("SELECT post.title, SUM(post.commentsCount) FROM Post post GROUP BY post.title HAVING SUM(post.commentsCount) > 10", 57 | relation.project("post.title, SUM(post.commentsCount)").group("post.title").having("SUM(post.commentsCount) > 10").toJpql()); 58 | } 59 | 60 | @Test 61 | public void testOrder() { 62 | assertEquals("SELECT post FROM Post post ORDER BY post.id", 63 | relation.project("post").order("post.id").toJpql()); 64 | } 65 | 66 | @Test 67 | public void testFromSubQuery() { 68 | assertEquals("SELECT subquery FROM (SELECT post.id, post.title FROM Post post) subquery", 69 | relation.project("subquery").from("(SELECT post.id, post.title FROM Post post) subquery").toJpql()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/Scoping.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository; 2 | 3 | import static com.activepersistence.repository.ScopeRegistry.ValidScopeTypes.CURRENT_SCOPE; 4 | import static com.activepersistence.repository.ScopeRegistry.ValidScopeTypes.IGNORE_DEFAULT_SCOPE; 5 | import java.lang.reflect.Method; 6 | import static java.util.Optional.ofNullable; 7 | import java.util.function.Supplier; 8 | 9 | public interface Scoping { 10 | 11 | public Relation getRelation(); 12 | 13 | public Relation defaultScope(); 14 | 15 | public Class getRealClass(); 16 | 17 | public default Relation all() { 18 | if (getCurrentScope() != null) { 19 | return new Relation(getCurrentScope()); 20 | } else { 21 | return defaultScoped(); 22 | } 23 | } 24 | 25 | public default Relation unscoped() { 26 | return getRelation(); 27 | } 28 | 29 | public default Relation unscoped(Supplier yield) { 30 | return getRelation().scoping(yield); 31 | } 32 | 33 | public static Relation getCurrentScope() { 34 | return (Relation) ScopeRegistry.valueFor(CURRENT_SCOPE); 35 | } 36 | 37 | public static void setCurrentScope(Relation scope) { 38 | ScopeRegistry.setValueFor(CURRENT_SCOPE, scope); 39 | } 40 | 41 | private static Boolean shouldIgnoreDefaultScope() { 42 | return (Boolean) ScopeRegistry.valueFor(IGNORE_DEFAULT_SCOPE); 43 | } 44 | 45 | private static void setIgnoreDefaultScope(boolean ignore) { 46 | ScopeRegistry.setValueFor(IGNORE_DEFAULT_SCOPE, ignore); 47 | } 48 | 49 | private Relation defaultScoped() { 50 | return ofNullable(buildDefaultScope()).orElseGet(() -> getRelation()); 51 | } 52 | 53 | private Relation buildDefaultScope() { 54 | if (defaultScopeOverride()) { 55 | return evaluateDefaultScope(() -> getRelation().merge$(defaultScope())); 56 | } else { 57 | return null; 58 | } 59 | } 60 | 61 | private Relation evaluateDefaultScope(Supplier yield) { 62 | if (shouldIgnoreDefaultScope()) return null; 63 | try { 64 | setIgnoreDefaultScope(true); return yield.get(); 65 | } finally { 66 | setIgnoreDefaultScope(false); 67 | } 68 | } 69 | 70 | private boolean defaultScopeOverride() { 71 | return Base.class != defaultScopeMethod().getDeclaringClass(); 72 | } 73 | 74 | private Method defaultScopeMethod() { 75 | try { 76 | return getRealClass().getMethod("defaultScope"); 77 | } catch (NoSuchMethodException | SecurityException ex) { 78 | throw new RuntimeException(ex.getMessage()); 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/relation/CalculationTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases.relation; 2 | 3 | import com.activepersistence.IntegrationTest; 4 | import com.activepersistence.repository.models.PostRepository; 5 | import static java.util.Arrays.asList; 6 | import java.util.List; 7 | import java.util.Map; 8 | import javax.inject.Inject; 9 | import org.jboss.arquillian.persistence.UsingDataSet; 10 | import static org.junit.Assert.assertArrayEquals; 11 | import static org.junit.Assert.assertEquals; 12 | import org.junit.Test; 13 | 14 | @UsingDataSet({"posts.xml", "comments.xml"}) 15 | public class CalculationTest extends IntegrationTest { 16 | 17 | @Inject 18 | private PostRepository postRepository; 19 | 20 | @Test 21 | public void testCount() { 22 | assertEquals(2L, postRepository.where("post.id IN (1, 2)").count()); 23 | } 24 | 25 | @Test 26 | public void testCountAllDistinct() { 27 | assertEquals(2L, postRepository.where("post.id IN (4, 5)").distinct().count()); 28 | } 29 | 30 | @Test 31 | public void testCountDistinct() { 32 | assertEquals(1L, postRepository.where("post.id IN (4, 5)").distinct().count("post.title")); 33 | } 34 | 35 | @Test 36 | public void testCountLimit() { 37 | assertEquals(1L, postRepository.where("post.id IN (1, 2)").limit(1).count()); 38 | } 39 | 40 | @Test 41 | public void testCountAllDistinctLimit() { 42 | assertEquals(1L, postRepository.where("post.id IN (4, 5)").limit(1).distinct().count()); 43 | } 44 | 45 | @Test 46 | public void testCountDistinctLimit() { 47 | assertEquals(1L, postRepository.where("post.id IN (4, 5)").limit(1).distinct().count("post.title")); 48 | } 49 | 50 | @Test 51 | public void testMinimum() { 52 | assertEquals(1, postRepository.minimum("post.likesCount")); 53 | } 54 | 55 | @Test 56 | public void testMaximum() { 57 | assertEquals(9999, postRepository.maximum("post.likesCount")); 58 | } 59 | 60 | @Test 61 | public void testSum() { 62 | assertEquals(8L, postRepository.where("post.id IN (1, 2)").distinct().sum("post.likesCount")); 63 | } 64 | 65 | @Test 66 | public void testPluck() { 67 | List results = postRepository.where("post.id IN (4, 5)").pluck("post.title"); 68 | assertEquals(asList("flood", "flood"), results); 69 | } 70 | 71 | @Test 72 | public void testPluckMultiple() { 73 | List results = postRepository.where("post.id IN (4, 5)").pluck("post.id", "post.title"); 74 | assertArrayEquals(new Object[] {4L, "flood"}, results.get(0)); 75 | assertArrayEquals(new Object[] {5L, "flood"}, results.get(1)); 76 | } 77 | 78 | @Test 79 | public void testIds() { 80 | assertEquals(asList(1L, 2L), postRepository.where("post.id IN (1, 2)").ids()); 81 | } 82 | 83 | @Test 84 | public void testCountGrouped() { 85 | Map result = (Map) postRepository.where("post.id IN (3, 4, 5)").group("post.title").count(); 86 | assertEquals(Long.valueOf(1), result.get("beautiful night")); 87 | assertEquals(Long.valueOf(2), result.get("flood")); 88 | assertEquals(2, result.size()); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/relation/FinderMethodsTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases.relation; 2 | 3 | import com.activepersistence.IntegrationTest; 4 | import com.activepersistence.repository.models.PostRepository; 5 | import javax.inject.Inject; 6 | import javax.persistence.EntityNotFoundException; 7 | import org.jboss.arquillian.persistence.UsingDataSet; 8 | import static org.junit.Assert.*; 9 | import org.junit.Test; 10 | 11 | @UsingDataSet({"posts.xml", "comments.xml"}) 12 | public class FinderMethodsTest extends IntegrationTest { 13 | 14 | @Inject 15 | private PostRepository postRepository; 16 | 17 | @Test 18 | public void testTake() { 19 | assertNotNull(postRepository.take()); 20 | } 21 | 22 | @Test 23 | public void testTakeWithLimit() { 24 | assertEquals(2, postRepository.take(2).size()); 25 | } 26 | 27 | @Test 28 | public void testTake$() { 29 | assertThrows(EntityNotFoundException.class,() -> postRepository.where("1=0").take$()); 30 | } 31 | 32 | @Test 33 | public void testFirst() { 34 | assertEquals((Long) 1L, postRepository.first().getId()); 35 | } 36 | 37 | @Test 38 | public void testFirstWithLimit() { 39 | assertEquals(2, postRepository.first(2).size()); 40 | } 41 | 42 | @Test 43 | public void testLast() { 44 | assertEquals((Long) 9999L, postRepository.last().getId()); 45 | } 46 | 47 | @Test 48 | public void testLastWithLimit() { 49 | assertEquals((Long) 9999L, postRepository.last(2).get(0).getId()); 50 | } 51 | 52 | @Test 53 | public void testLast$() { 54 | assertThrows(EntityNotFoundException.class,() -> postRepository.where("1=0").last$()); 55 | } 56 | 57 | @Test 58 | public void testFindString() { 59 | assertEquals((Long) 2L, postRepository.find("2").getId()); 60 | } 61 | 62 | @Test 63 | public void testFind() { 64 | assertEquals((Long) 2L, postRepository.find(2L).getId()); 65 | } 66 | 67 | @Test 68 | public void testFindMultiple() { 69 | assertEquals(2 ,postRepository.find(1L, 2L).size()); 70 | } 71 | 72 | @Test 73 | public void testFindBy() { 74 | assertEquals((Long) 1L ,postRepository.findBy("post.title = 'hello world'").getId()); 75 | } 76 | 77 | @Test 78 | public void testFindBy$() { 79 | assertThrows(EntityNotFoundException.class, () -> postRepository.findBy$("post.title = 'not found'")); 80 | } 81 | 82 | @Test 83 | public void testFindByExpression() { 84 | assertEquals((Long) 1L ,postRepository.findByExpression("IdAndTitle", 1, "hello world").getId()); 85 | } 86 | 87 | @Test 88 | public void testFindByExpression$() { 89 | assertThrows(EntityNotFoundException.class, () -> postRepository.findByExpression$("Title", "not found")); 90 | } 91 | 92 | @Test 93 | public void testExists() { 94 | assertTrue(postRepository.where("post.title = 'hello world'").exists()); 95 | } 96 | 97 | @Test 98 | public void testExistsWithParam() { 99 | assertTrue(postRepository.exists("post.title = 'hello world'")); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/model/Base.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.model; 2 | 3 | import com.activepersistence.ReadOnlyRecord; 4 | import java.time.LocalDateTime; 5 | import java.util.Objects; 6 | import javax.persistence.MappedSuperclass; 7 | import javax.persistence.PostLoad; 8 | import javax.persistence.PostPersist; 9 | import javax.persistence.PostRemove; 10 | import javax.persistence.PostUpdate; 11 | import javax.persistence.PrePersist; 12 | import javax.persistence.PreRemove; 13 | import javax.persistence.PreUpdate; 14 | import javax.persistence.Transient; 15 | 16 | @MappedSuperclass 17 | public abstract class Base { 18 | 19 | @Transient private boolean newRecord = true; 20 | 21 | @Transient private boolean destroyed = false; 22 | 23 | @Transient private boolean readOnly = false; 24 | 25 | public abstract ID getId(); 26 | 27 | public abstract void setId(ID value); 28 | 29 | public boolean isNewRecord() { 30 | return newRecord; 31 | } 32 | 33 | public boolean isDestroyed() { 34 | return destroyed; 35 | } 36 | 37 | public boolean isPersisted() { 38 | return !(newRecord || destroyed); 39 | } 40 | 41 | public void setReadOnly(boolean readOnly) { 42 | this.readOnly = readOnly; 43 | } 44 | 45 | public void setCreatedAt(LocalDateTime createdAt) { 46 | // If you want timestamp it should be overridden on child. 47 | } 48 | 49 | public void setUpdatedAt(LocalDateTime updatedAt) { 50 | // If you want timestamp it should be overridden on child. 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | return Objects.hashCode(getId()); 56 | } 57 | 58 | @Override 59 | public boolean equals(Object other) { 60 | if (this == other) return true; 61 | if (other == null) return false; 62 | if (getClass() != other.getClass()) return false; 63 | 64 | return Objects.equals(getId(), ((Base) other).getId()); 65 | } 66 | 67 | @PrePersist 68 | private void prePersist() { 69 | if (readOnly) { 70 | raiseReadOnlyRecordError(); 71 | } else { 72 | setCreatedAt(LocalDateTime.now()); 73 | setUpdatedAt(LocalDateTime.now()); 74 | } 75 | } 76 | 77 | @PreUpdate 78 | private void preUpdate() { 79 | if (readOnly) { 80 | raiseReadOnlyRecordError(); 81 | } else { 82 | setUpdatedAt(LocalDateTime.now()); 83 | } 84 | } 85 | 86 | @PreRemove 87 | private void preRemove() { 88 | if (readOnly) raiseReadOnlyRecordError(); 89 | } 90 | 91 | @PostPersist 92 | private void postPersist() { 93 | newRecord = false; 94 | } 95 | 96 | @PostUpdate 97 | private void postUpdate() { 98 | newRecord = false; 99 | } 100 | 101 | @PostRemove 102 | private void postRemove() { 103 | destroyed = true; 104 | } 105 | 106 | @PostLoad 107 | private void postLoad() { 108 | newRecord = false; 109 | } 110 | 111 | private void raiseReadOnlyRecordError() { 112 | throw new ReadOnlyRecord(getClass().getSimpleName() + " is marked as readonly"); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/relation/SpawnMethodsTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases.relation; 2 | 3 | import com.activepersistence.IntegrationTest; 4 | import com.activepersistence.repository.models.CommentRepository; 5 | import com.activepersistence.repository.models.PostRepository; 6 | import static com.activepersistence.repository.relation.ValueMethods.ORDER; 7 | import javax.inject.Inject; 8 | import org.jboss.arquillian.persistence.UsingDataSet; 9 | import static org.junit.Assert.assertEquals; 10 | import static org.junit.Assert.assertNotEquals; 11 | import org.junit.Test; 12 | 13 | @UsingDataSet({"posts.xml", "comments.xml"}) 14 | public class SpawnMethodsTest extends IntegrationTest { 15 | 16 | @Inject 17 | private PostRepository postRepository; 18 | 19 | @Inject 20 | private CommentRepository commentRepository; 21 | 22 | @Test 23 | public void testSpawn() { 24 | var relation = postRepository.all(); 25 | assertNotEquals(relation, relation.spawn()); 26 | } 27 | 28 | @Test 29 | public void testMerge() { 30 | assertEquals("SELECT post FROM Post post WHERE (1=0)", 31 | postRepository.merge(postRepository.oneEqZero()).toJpql()); 32 | } 33 | 34 | @Test 35 | public void testMergeTwice() { 36 | assertEquals("SELECT post FROM Post post WHERE (1=0) AND (2=0)", 37 | postRepository.all().merge(postRepository.oneEqZero()).merge(postRepository.twoEqZero()).toJpql()); 38 | } 39 | 40 | @Test 41 | public void testMergeWithExcept() { 42 | assertEquals("SELECT post FROM Post post ORDER BY post.id", 43 | postRepository.order("post.id").merge(postRepository.except(ORDER)).toJpql()); 44 | } 45 | 46 | @Test 47 | public void testMergeWithUnscope() { 48 | assertEquals("SELECT post FROM Post post", 49 | postRepository.order("post.id").merge(postRepository.unscope(ORDER)).toJpql()); 50 | } 51 | 52 | @Test 53 | public void testOnly() { 54 | assertEquals("SELECT post FROM Post post ORDER BY post.id", 55 | postRepository.where("1=0").order("post.id").only(ORDER).toJpql()); 56 | } 57 | 58 | @Test 59 | public void testExcept() { 60 | assertEquals("SELECT post FROM Post post WHERE (1=0)", 61 | postRepository.where("1=0").order("post.id").except(ORDER).toJpql()); 62 | } 63 | 64 | @Test 65 | public void testMergeAnotherRelation() { 66 | assertEquals("SELECT comment FROM Comment comment INNER JOIN comment.post post WHERE (post.id = 1)", 67 | commentRepository.joins("INNER JOIN comment.post post").merge(postRepository.where("post.id = 1")).toJpql()); 68 | } 69 | 70 | @Test 71 | public void testMergeFromAnotherRelation() { 72 | assertEquals("SELECT comment FROM Comment comment INNER JOIN comment.post post WHERE (post.id = 1)", 73 | commentRepository.joins("INNER JOIN comment.post post").merge(postRepository.from("Post post").where("post.id = 1")).toJpql()); 74 | } 75 | 76 | @Test 77 | public void testMergeOrder() { 78 | assertEquals("SELECT post FROM Post post ORDER BY post.id, post.title", postRepository.order("post.id").merge(postRepository.order("post.title")).toJpql()); 79 | } 80 | 81 | @Test 82 | public void testMergeReOrder() { 83 | assertEquals("SELECT post FROM Post post ORDER BY post.title", postRepository.order("post.id").merge(postRepository.reorder("post.title")).toJpql()); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/Sanitization.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository; 2 | 3 | import com.activepersistence.PreparedStatementInvalid; 4 | import static com.activepersistence.repository.connectionadapters.Quoting.quote; 5 | import static com.activepersistence.repository.relation.ValueMethods.CONSTRUCTOR; 6 | import static java.lang.String.format; 7 | import java.util.ArrayList; 8 | import static java.util.Arrays.asList; 9 | import static java.util.Arrays.stream; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.function.Supplier; 13 | import static java.util.regex.Matcher.quoteReplacement; 14 | import static java.util.regex.Pattern.compile; 15 | import static java.util.stream.Collectors.joining; 16 | import java.util.stream.Stream; 17 | 18 | public interface Sanitization { 19 | 20 | public default String sanitizeJpql(String statement, Object... values) { 21 | if (statement.isEmpty()) return null; 22 | 23 | if (values != null && values.length > 0) { 24 | if (values[0] instanceof Map && compile(":\\w+").matcher(statement).find()) { 25 | return replaceNamedBindVariables(statement, (Map) values[0]); 26 | } else if (statement.contains("?")) { 27 | return replaceBindVariables(statement, values); 28 | } else if (statement.isBlank()) { 29 | return statement; 30 | } else { 31 | return format(statement, stream(values).map(v -> quote(v)).toArray()); 32 | } 33 | } else { 34 | return statement; 35 | } 36 | } 37 | 38 | private String replaceNamedBindVariables(String statement, Map bindVars) { 39 | return compile("(:?):([a-zA-Z]\\w*)").matcher(statement).replaceAll(match -> { 40 | if (match.group(1).equals(":")) { 41 | return match.group(); 42 | } else if (bindVars.keySet().contains(match.group(2))) { 43 | return replaceBindVariable(bindVars.get(match.group(2))); 44 | } else { 45 | throw new PreparedStatementInvalid("missing value for :" + match + " in " + statement); 46 | } 47 | }); 48 | }; 49 | 50 | private String replaceBindVariables(String statement, Object[] values) { 51 | var bindCount = (int) statement.chars().filter(ch -> ch == '?').count(); 52 | var valuesCount = (int) values.length; 53 | 54 | if (bindCount == valuesCount) { 55 | var bound = new ArrayList(asList(values)); return compile("\\?").matcher(statement).replaceAll(match -> quoteReplacement(replaceBindVariable(bound.remove(0)))); 56 | } else { 57 | throw new PreparedStatementInvalid("wrong number of bind variables (" + bindCount + " for " + valuesCount + ") in: " + statement); 58 | } 59 | }; 60 | 61 | private String replaceBindVariable(Object value) { 62 | if (value instanceof Relation) { 63 | return ((Relation) value).except(CONSTRUCTOR).toJpql(); 64 | } else { 65 | return quoteBoundValue(value); 66 | } 67 | } 68 | 69 | private String quoteBoundValue(Object value) { 70 | if (value instanceof List) { 71 | Supplier> values = () -> ((List) value).stream().map(v -> quote(v)); 72 | 73 | if (values.get().findFirst().isEmpty()) { 74 | return quote(null); 75 | } else { 76 | return values.get().collect(joining(",")); 77 | } 78 | } else { 79 | return quote(value); 80 | } 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/connectionadapters/Quoting.java: -------------------------------------------------------------------------------- 1 | 2 | package com.activepersistence.repository.connectionadapters; 3 | 4 | import com.activepersistence.model.Base; 5 | import java.time.LocalDate; 6 | import java.time.LocalDateTime; 7 | import java.time.LocalTime; 8 | import java.time.format.DateTimeFormatter; 9 | import static java.time.format.DateTimeFormatter.ofPattern; 10 | 11 | public class Quoting { 12 | 13 | public static final DateTimeFormatter DATE_FORMAT = ofPattern("yyyy-MM-dd"); 14 | public static final DateTimeFormatter TIME_FORMAT = ofPattern("HH:mm:ss"); 15 | public static final DateTimeFormatter DATE_TIME_FORMAT = ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSSSS"); 16 | 17 | public static String quote(Object value) { 18 | if (value instanceof Base) { 19 | return _quote(idValueForDatabase((Base) value)); 20 | } else { 21 | return _quote(value); 22 | } 23 | } 24 | 25 | private static Object idValueForDatabase(Base value) { 26 | return value.getId(); 27 | } 28 | 29 | private static String _quote(Object value) { 30 | if (value == null) { 31 | return "NULL"; 32 | } else if (value instanceof String) { 33 | return quote((String) value); 34 | } else if (value instanceof Long) { 35 | return quote((Long) value); 36 | } else if (value instanceof Float) { 37 | return quote((Float) value); 38 | } else if (value instanceof Double) { 39 | return quote((Double) value); 40 | } else if (value instanceof Number) { 41 | return quote((Number) value); 42 | } else if (value instanceof Boolean) { 43 | return quote((Boolean) value); 44 | } else if (value instanceof LocalDate) { 45 | return quote((LocalDate) value); 46 | } else if (value instanceof LocalTime) { 47 | return quote((LocalTime) value); 48 | } else if (value instanceof LocalDateTime) { 49 | return quote((LocalDateTime) value); 50 | } else if (value instanceof Enum) { 51 | return quote((Enum) value); 52 | } else if (value instanceof Class) { 53 | return quote((Class) value); 54 | } else { 55 | throw new RuntimeException("can't quote: " + value.getClass().getName()); 56 | } 57 | } 58 | 59 | private static String quote(String value) { 60 | return "'" + value.replaceAll("'", "''") + "'"; 61 | } 62 | 63 | private static String quote(Boolean value) { 64 | return value ? "TRUE" : "FALSE"; 65 | } 66 | 67 | private static String quote(Long value) { 68 | return value + "L"; 69 | } 70 | 71 | private static String quote(Float value) { 72 | return value + "F"; 73 | } 74 | 75 | private static String quote(Double value) { 76 | return value + "D"; 77 | } 78 | 79 | private static String quote(Number value) { 80 | return value.toString(); 81 | } 82 | 83 | private static String quote(LocalDate value) { 84 | return "{d '" + value.format(DATE_FORMAT) + "'}"; 85 | } 86 | 87 | private static String quote(LocalTime value) { 88 | return "{t '" + value.format(TIME_FORMAT) + "'}"; 89 | } 90 | 91 | private static String quote(LocalDateTime value) { 92 | return "{ts '" + value.format(DATE_TIME_FORMAT) + "'}"; 93 | } 94 | 95 | private static String quote(Class value) { 96 | return value.getSimpleName(); 97 | } 98 | 99 | private static String quote(Enum value) { 100 | return value.getClass().getName() + "." + value.name(); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/PersistenceTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases; 2 | 3 | import com.activepersistence.IntegrationTest; 4 | import com.activepersistence.repository.models.Post; 5 | import com.activepersistence.repository.models.PostRepository; 6 | import javax.inject.Inject; 7 | import org.jboss.arquillian.persistence.UsingDataSet; 8 | import static org.junit.Assert.*; 9 | import org.junit.Test; 10 | 11 | @UsingDataSet({"posts.xml", "comments.xml"}) 12 | public class PersistenceTest extends IntegrationTest { 13 | 14 | @Inject 15 | private PostRepository postRepository; 16 | 17 | @Test 18 | public void testDestroy() { 19 | var post = postRepository.find(1L); 20 | var count = (long) postRepository.count(); 21 | 22 | postRepository.destroy(post); 23 | 24 | assertEquals(count -1, postRepository.count()); 25 | assertTrue(post.isDestroyed()); 26 | } 27 | 28 | @Test 29 | public void testCreate() { 30 | var post = new Post("new post", "body", 0); 31 | var count = (long) postRepository.count(); 32 | 33 | postRepository.save(post); 34 | 35 | assertEquals(count + 1, postRepository.count()); 36 | assertTrue(post.isPersisted()); 37 | } 38 | 39 | @Test 40 | public void testUpdate() { 41 | var post = postRepository.find(1L); 42 | post.setTitle("changed"); 43 | 44 | var count = (long) postRepository.count(); 45 | 46 | postRepository.save(post); 47 | 48 | postRepository.reload(post); 49 | 50 | assertEquals(count, postRepository.count()); 51 | assertEquals("changed", post.getTitle()); 52 | } 53 | 54 | @Test 55 | public void testBeforeSave() { 56 | var post = new Post("new post", "execBeforeSave", 0); 57 | postRepository.save(post); 58 | 59 | assertEquals("OK", post.getBody()); 60 | } 61 | 62 | @Test 63 | public void testAfterSave() { 64 | var post = new Post("new post", "execAfterSave", 0); 65 | postRepository.save(post); 66 | 67 | assertEquals("OK", post.getBody()); 68 | } 69 | 70 | @Test 71 | public void testBeforeCreate() { 72 | var post = new Post("new post", "execBeforeCreate", 0); 73 | postRepository.save(post); 74 | 75 | assertEquals("OK", post.getBody()); 76 | } 77 | 78 | @Test 79 | public void testAfterCreate() { 80 | var post = new Post("new post", "execAfterCreate", 0); 81 | postRepository.save(post); 82 | 83 | assertEquals("OK", post.getBody()); 84 | } 85 | 86 | @Test 87 | public void testBeforeUpdate() { 88 | var post = postRepository.find(1L); 89 | post.setBody("execBeforeUpdate"); 90 | 91 | postRepository.save(post); 92 | 93 | assertEquals("OK", post.getBody()); 94 | } 95 | 96 | @Test 97 | public void testAfterUpdate() { 98 | var post = postRepository.find(1L); 99 | post.setBody("execAfterUpdate"); 100 | 101 | postRepository.save(post); 102 | 103 | assertEquals("OK", post.getBody()); 104 | } 105 | 106 | @Test 107 | public void testBeforeDestroy() { 108 | var post = postRepository.find(1L); 109 | post.setBody("execBeforeDestroy"); 110 | 111 | postRepository.destroy(post); 112 | 113 | assertEquals("OK", post.getBody()); 114 | } 115 | 116 | @Test 117 | public void testAfterDestroy() { 118 | var post = postRepository.find(1L); 119 | post.setBody("execAfterDestroy"); 120 | 121 | postRepository.destroy(post); 122 | 123 | assertEquals("OK", post.getBody()); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/SelectManager.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel; 2 | 3 | import static com.activepersistence.repository.Arel.createStringJoin; 4 | import static com.activepersistence.repository.Arel.jpql; 5 | import static com.activepersistence.repository.Arel.jpqlList; 6 | import com.activepersistence.repository.arel.nodes.And; 7 | import com.activepersistence.repository.arel.nodes.Join; 8 | import com.activepersistence.repository.arel.nodes.JpqlLiteral; 9 | import com.activepersistence.repository.arel.nodes.SelectCore; 10 | import com.activepersistence.repository.arel.nodes.SelectStatement; 11 | import com.activepersistence.repository.arel.visitors.Visitable; 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import javax.persistence.LockModeType; 15 | 16 | public class SelectManager extends TreeManager { 17 | 18 | private final SelectStatement ast; 19 | 20 | private final SelectCore ctx; 21 | 22 | public SelectManager(Entity entity) { 23 | this.ast = new SelectStatement(entity); 24 | this.ctx = this.ast.getCore(); 25 | } 26 | 27 | public SelectManager project(String... projections) { 28 | ctx.getProjections().addAll(jpqlList(projections)); return this; 29 | } 30 | 31 | public SelectManager constructor(boolean value) { 32 | ctx.setConstructor(value); return this; 33 | } 34 | 35 | public SelectManager distinct(boolean value) { 36 | ctx.setDistinct(value); return this; 37 | } 38 | 39 | public SelectManager from(String from) { 40 | ctx.getSource().setSource(jpql(from)); return this; 41 | } 42 | 43 | public SelectManager join(String join) { 44 | ctx.getSource().getJoins().add(createStringJoin(join)); return this; 45 | } 46 | 47 | public SelectManager where(String condition) { 48 | ctx.getWheres().add(jpql(condition)); return this; 49 | } 50 | 51 | public SelectManager group(String... fields) { 52 | ctx.getGroups().addAll(jpqlList(fields)); return this; 53 | } 54 | 55 | public SelectManager having(String condition) { 56 | ctx.getHavings().add(jpql(condition)); return this; 57 | } 58 | 59 | public SelectManager order(String... expr) { 60 | ast.getOrders().addAll(jpqlList(expr)); return this; 61 | } 62 | 63 | public SelectManager limit(int limit) { 64 | ast.setLimit(limit); return this; 65 | } 66 | 67 | public SelectManager offset(int offset) { 68 | ast.setOffset(offset); return this; 69 | } 70 | 71 | public SelectManager lock(LockModeType lock) { 72 | ast.setLock(lock); return this; 73 | } 74 | 75 | public void setHints(HashMap hints) { 76 | ast.setHints(hints); 77 | } 78 | 79 | public int getLimit() { 80 | return ast.getLimit(); 81 | } 82 | 83 | public int getOffset() { 84 | return ast.getOffset(); 85 | } 86 | 87 | public LockModeType getLock() { 88 | return ast.getLock(); 89 | } 90 | 91 | public HashMap getHints() { 92 | return ast.getHints(); 93 | } 94 | 95 | public List getConstraints() { 96 | return ctx.getWheres(); 97 | } 98 | public List getOrders() { 99 | return ast.getOrders(); 100 | } 101 | 102 | public List getJoinSources() { 103 | return ctx.getSource().getJoins(); 104 | } 105 | 106 | public JpqlLiteral whereJpql() { 107 | if (ctx.getWheres().isEmpty()) { 108 | return null; 109 | } else { 110 | return new JpqlLiteral("WHERE " + new And(ctx.getWheres()).toJpql()); 111 | } 112 | } 113 | 114 | @Override 115 | public SelectStatement getAst() { 116 | return ast; 117 | } 118 | 119 | @Override 120 | public SelectCore getCtx() { 121 | return ctx; 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/RelationTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases; 2 | 3 | import com.activepersistence.ActivePersistenceError; 4 | import com.activepersistence.IntegrationTest; 5 | import com.activepersistence.repository.models.PostRepository; 6 | import static com.activepersistence.repository.relation.ValueMethods.ORDER; 7 | import java.util.Map; 8 | import javax.inject.Inject; 9 | import org.jboss.arquillian.persistence.UsingDataSet; 10 | import static org.junit.Assert.*; 11 | import org.junit.Test; 12 | 13 | @UsingDataSet({"posts.xml", "comments.xml"}) 14 | public class RelationTest extends IntegrationTest { 15 | 16 | @Inject 17 | private PostRepository postRepository; 18 | 19 | @Test 20 | public void testScopingBlock() { 21 | assertEquals("SELECT post FROM Post post WHERE (1=0)", 22 | postRepository.all().scoping(() -> postRepository.oneEqZero()).toJpql()); 23 | } 24 | 25 | @Test 26 | public void testScopingBlockTwice() { 27 | assertEquals("SELECT post FROM Post post WHERE (1=0) AND (2=0)", 28 | postRepository.all().scoping(() -> postRepository.oneEqZero()).scoping(() -> postRepository.twoEqZero()).toJpql()); 29 | } 30 | 31 | @Test 32 | public void testScopingWithExcept() { 33 | assertEquals("SELECT post FROM Post post", 34 | postRepository.order("post.id").scoping(() -> postRepository.except(ORDER)).toJpql()); 35 | } 36 | 37 | @Test 38 | public void testScopingWithUnscope() { 39 | assertEquals("SELECT post FROM Post post", 40 | postRepository.order("post.id").scoping(() -> postRepository.unscope(ORDER)).toJpql()); 41 | } 42 | 43 | @Test 44 | public void testDestroyAll() { 45 | var count = (long) postRepository.count(); 46 | postRepository.where("post.id IN (1,2,3)").destroyAll(); 47 | assertEquals(count - 3, postRepository.count()); 48 | } 49 | 50 | @Test 51 | public void testDestroyBy() { 52 | var count = (long) postRepository.count(); 53 | postRepository.destroyBy("post.id IN (1,2,3)"); 54 | assertEquals(count - 3, postRepository.count()); 55 | } 56 | 57 | @Test 58 | public void testDeleteAll() { 59 | var count = (long) postRepository.count(); 60 | assertEquals(3, postRepository.where("post.id IN (1,2,3)").deleteAll()); 61 | assertEquals(count - 3, postRepository.count()); 62 | } 63 | 64 | @Test 65 | public void testDeleteBy() { 66 | var count = (long) postRepository.count(); 67 | assertEquals(3, postRepository.deleteBy("post.id IN (1,2,3)")); 68 | assertEquals(count - 3, postRepository.count()); 69 | } 70 | 71 | @Test 72 | public void testDeleteAllWithDistinct() { 73 | assertThrows(ActivePersistenceError.class,() -> postRepository.distinct().deleteAll()); 74 | } 75 | 76 | @Test 77 | public void testeDeleteAllWithoutConditions() { 78 | postRepository.deleteAll(); 79 | assertEquals(0L, postRepository.count()); 80 | } 81 | 82 | @Test 83 | public void testUpdateAll() { 84 | assertEquals(3, postRepository.where("post.id IN (1,2,3)").updateAll("post.title = 'testing'")); 85 | assertEquals("testing", postRepository.find(1L).getTitle()); 86 | } 87 | 88 | @Test 89 | public void testUpdateAllMap() { 90 | assertEquals(3, postRepository.where("post.id IN (1,2,3)").updateAll(Map.of("post.title", "testing"))); 91 | assertEquals("testing", postRepository.find(1L).getTitle()); 92 | } 93 | 94 | @Test 95 | public void testUpdateAllWithDistinct() { 96 | assertThrows(ActivePersistenceError.class, () -> postRepository.distinct().updateAll("post.name = 'testing'")); 97 | } 98 | 99 | @Test 100 | public void testSize() { 101 | assertEquals(2L, postRepository.where("post.id IN (1, 2)").size()); 102 | } 103 | 104 | @Test 105 | public void testIsEmpty() { 106 | assertTrue(postRepository.where("post.title = 'not found'").isEmpty()); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/SanitizationTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases; 2 | 3 | import com.activepersistence.IntegrationTest; 4 | import com.activepersistence.PreparedStatementInvalid; 5 | import com.activepersistence.repository.models.ClientRepository; 6 | import static com.activepersistence.repository.models.Gender.MALE; 7 | import com.activepersistence.repository.models.Post; 8 | import com.activepersistence.repository.models.PostRepository; 9 | import java.math.BigDecimal; 10 | import java.time.LocalDate; 11 | import java.time.LocalDateTime; 12 | import java.time.LocalTime; 13 | import javax.inject.Inject; 14 | import org.jboss.arquillian.persistence.UsingDataSet; 15 | import static org.junit.Assert.assertEquals; 16 | import static org.junit.Assert.assertThrows; 17 | import org.junit.Test; 18 | 19 | @UsingDataSet({"posts.xml", "comments.xml", "clients.xml"}) 20 | public class SanitizationTest extends IntegrationTest { 21 | 22 | @Inject 23 | private PostRepository postRepository; 24 | 25 | @Inject 26 | private ClientRepository clientRepository; 27 | 28 | @Test 29 | public void testBindWrongNumberVariables() { 30 | assertThrows(PreparedStatementInvalid.class, () -> postRepository.sanitizeJpql("post.id = ? AND post.id = ?", 1)); 31 | } 32 | 33 | @Test 34 | public void testBindSubQuery() { 35 | var subquery = postRepository.where("post.title = 'flood'").select("post.id"); 36 | assertEquals("post.id IN (SELECT post.id FROM Post post WHERE (post.title = 'flood'))", postRepository.sanitizeJpql("post.id IN (?)", subquery)); 37 | } 38 | 39 | @Test 40 | public void testAssociate() { 41 | var query = postRepository.sanitizeJpql("comment.post.id = ?", postRepository.find(1L)); 42 | assertEquals("comment.post.id = 1L", query); 43 | } 44 | 45 | @Test 46 | public void testQuoteNull() { 47 | assertEquals("post.id = NULL", postRepository.sanitizeJpql("post.id = ?", (Object) null)); 48 | } 49 | 50 | @Test 51 | public void testQuoteClass() { 52 | assertEquals("post.type = Post", postRepository.sanitizeJpql("post.type = ?", Post.class)); 53 | } 54 | 55 | @Test 56 | public void testQuoteEnum() { 57 | assertEquals("client.gender = com.activepersistence.repository.models.Gender.MALE", clientRepository.sanitizeJpql("client.gender = ?", MALE)); 58 | } 59 | 60 | @Test 61 | public void testQuoteString() { 62 | assertEquals("client.name = 'Nixon'", clientRepository.sanitizeJpql("client.name = ?", "Nixon")); 63 | } 64 | 65 | @Test 66 | public void testQuoteStringQuote() { 67 | assertEquals("client.name = 'Ni''xon'", clientRepository.sanitizeJpql("client.name = ?", "Ni'xon")); 68 | } 69 | 70 | @Test 71 | public void testQuoteInteger() { 72 | assertEquals("client.id = 1234", clientRepository.sanitizeJpql("client.id = ?", 1234)); 73 | } 74 | 75 | @Test 76 | public void testQuoteLong() { 77 | assertEquals("client.id = 1234L", clientRepository.sanitizeJpql("client.id = ?", 1234L)); 78 | } 79 | 80 | @Test 81 | public void testQuoteFloat() { 82 | assertEquals("client.weight = 64.14F", clientRepository.sanitizeJpql("client.weight = ?", 64.14F)); 83 | } 84 | 85 | @Test 86 | public void testQuoteDouble() { 87 | assertEquals("client.ratio = 3.14D", clientRepository.sanitizeJpql("client.ratio = ?", 3.14D)); 88 | } 89 | 90 | @Test 91 | public void testQuoteBoolean() { 92 | assertEquals("client.active = FALSE", clientRepository.sanitizeJpql("client.active = ?", false)); 93 | } 94 | 95 | @Test 96 | public void testQuoteLocalDate() { 97 | assertEquals("post.createdAt = {d '2020-01-04'}", postRepository.sanitizeJpql("post.createdAt = ?", LocalDate.of(2020, 01, 04))); 98 | } 99 | 100 | @Test 101 | public void testQuoteLocalDateTime() { 102 | assertEquals("post.createdAt = {ts '2020-01-04 00:00:00.000000000'}", postRepository.sanitizeJpql("post.createdAt = ?", LocalDateTime.of(2020, 01, 04, 0, 0, 0, 0))); 103 | } 104 | 105 | @Test 106 | public void testQuoteLocalTime() { 107 | assertEquals("post.createdAt = {t '00:00:00'}", postRepository.sanitizeJpql("post.createdAt = ?", LocalTime.of(0, 0, 0))); 108 | } 109 | 110 | @Test 111 | public void testQuoteRecord() { 112 | assertEquals("client.id = 1L", clientRepository.sanitizeJpql("client.id = ?", clientRepository.first())); 113 | } 114 | 115 | @Test 116 | public void testQuoteBigDecimal() { 117 | assertEquals("client.salary = 8500.25", clientRepository.sanitizeJpql("client.salary = ?", BigDecimal.valueOf(8500.25))); 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/arel/visitors/ToJpqlTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases.arel.visitors; 2 | 3 | import static com.activepersistence.repository.Arel.createStringJoin; 4 | import static com.activepersistence.repository.Arel.jpql; 5 | import com.activepersistence.repository.arel.Entity; 6 | import com.activepersistence.repository.arel.nodes.Assignment; 7 | import com.activepersistence.repository.arel.nodes.Constructor; 8 | import com.activepersistence.repository.arel.nodes.DeleteStatement; 9 | import com.activepersistence.repository.arel.nodes.JoinSource; 10 | import com.activepersistence.repository.arel.nodes.SelectCore; 11 | import com.activepersistence.repository.arel.nodes.SelectStatement; 12 | import com.activepersistence.repository.arel.nodes.UpdateStatement; 13 | import com.activepersistence.repository.arel.visitors.Visitable; 14 | import com.activepersistence.repository.models.Post; 15 | import static java.util.Arrays.asList; 16 | import static org.junit.Assert.assertEquals; 17 | import org.junit.Test; 18 | 19 | public class ToJpqlTest { 20 | 21 | @Test 22 | public void testVisitDeleteStatement() { 23 | var entity = new Entity(Post.class, "post"); 24 | var statement = new DeleteStatement(); 25 | statement.setRelation(entity); 26 | assertEquals("DELETE FROM Post post", compile(statement)); 27 | } 28 | 29 | @Test 30 | public void testVisitUpdateStatement() { 31 | var entity = new Entity(Post.class, "post"); 32 | var statement = new UpdateStatement(); 33 | statement.setRelation(entity); 34 | statement.setValues(asList(jpql("post.name = 'test'"))); 35 | assertEquals("UPDATE Post post SET post.name = 'test'", compile(statement)); 36 | } 37 | 38 | @Test 39 | public void testVisitSelectStatement() { 40 | var entity = new Entity(Post.class, "post"); 41 | var statement = new SelectStatement(entity); 42 | assertEquals("SELECT FROM Post post", compile(statement)); 43 | } 44 | 45 | @Test 46 | public void testVisitSelectCore() { 47 | var entity = new Entity(Post.class, "post"); 48 | var core = new SelectCore(entity); 49 | assertEquals("SELECT FROM Post post", compile(core)); 50 | } 51 | 52 | @Test 53 | public void testVisitDistinct() { 54 | var entity = new Entity(Post.class, "post"); 55 | var core = new SelectCore(entity); 56 | core.setDistinct(true); 57 | core.getProjections().addAll(asList(jpql("post"))); 58 | assertEquals("SELECT DISTINCT post FROM Post post", compile(core)); 59 | } 60 | 61 | @Test 62 | public void testVisitConstructor() { 63 | var constructor = new Constructor("java.lang.Object", asList(jpql("post.id, post.title"))); 64 | assertEquals(" NEW java.lang.Object(post.id, post.title)", compile(constructor)); 65 | } 66 | 67 | @Test 68 | public void testVisitEntity() { 69 | assertEquals("Post post", compile(new Entity(Post.class, "post"))); 70 | } 71 | 72 | @Test 73 | public void testVisitJoinSource() { 74 | var entity = new Entity(Post.class, "post"); 75 | var joinsource = new JoinSource(entity); 76 | joinsource.getJoins().add(createStringJoin("JOIN post.client client")); 77 | assertEquals("Post post JOIN post.client client", compile(joinsource)); 78 | } 79 | 80 | @Test 81 | public void testVisitStringJoin() { 82 | assertEquals("JOIN Client c", compile(createStringJoin("JOIN Client c"))); 83 | } 84 | 85 | @Test 86 | public void testVisitCount() { 87 | assertEquals("COUNT(post)", compile(jpql("post").count())); 88 | } 89 | 90 | @Test 91 | public void testVisitCountDistinct() { 92 | assertEquals("COUNT(DISTINCT post)", compile(jpql("post").count(true))); 93 | } 94 | 95 | @Test 96 | public void testVisitCountAlias() { 97 | assertEquals("COUNT(post) AS count_post", compile(jpql("post").count().as("count_post"))); 98 | } 99 | 100 | @Test 101 | public void testVisitSum() { 102 | assertEquals("SUM(post.likesCount)", compile(jpql("post.likesCount").sum())); 103 | } 104 | 105 | @Test 106 | public void testVisitMaximum() { 107 | assertEquals("MAX(post.id)", compile(jpql("post.id").maximum())); 108 | } 109 | 110 | @Test 111 | public void testVisitMinimum() { 112 | assertEquals("MIN(post.id)", compile(jpql("post.id").minimum())); 113 | } 114 | 115 | @Test 116 | public void testVisitAverage() { 117 | assertEquals("AVG(post.id)", compile(jpql("post.id").average())); 118 | } 119 | 120 | @Test 121 | public void testVisitAssignment() { 122 | assertEquals("field = 'value'", compile(new Assignment(jpql("field"), "value"))); 123 | } 124 | 125 | private String compile(Visitable node) { 126 | return Entity.visitor.accept(node, new StringBuilder()).toString(); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/relation/MutationTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases.relation; 2 | 3 | import com.activepersistence.IntegrationTest; 4 | import com.activepersistence.repository.models.PostRepository; 5 | import static java.util.Arrays.asList; 6 | import javax.inject.Inject; 7 | import javax.persistence.LockModeType; 8 | import static org.junit.Assert.assertEquals; 9 | import org.junit.Test; 10 | 11 | public class MutationTest extends IntegrationTest { 12 | 13 | @Inject 14 | private PostRepository postRepository; 15 | 16 | @Test 17 | public void testNotMutating() { 18 | var relation = postRepository.where("1=1"); 19 | var relation2 = relation.where("2=2"); 20 | 21 | assertEquals("SELECT post FROM Post post WHERE (1=1)", relation.toJpql()); 22 | assertEquals("SELECT post FROM Post post WHERE (1=1) AND (2=2)", relation2.toJpql()); 23 | } 24 | 25 | @Test 26 | public void testSelect$() { 27 | var relation = postRepository.all(); 28 | var relation2 = relation.select2$("foo"); 29 | assertEquals(relation, relation2); 30 | assertEquals(asList("foo"), relation2.getValues().getSelect()); 31 | } 32 | 33 | @Test 34 | public void testJoin$() { 35 | var relation = postRepository.all(); 36 | var relation2 = relation.joins$("INNER JOIN foo"); 37 | assertEquals(relation, relation2); 38 | } 39 | 40 | @Test 41 | public void testWhere$() { 42 | var relation = postRepository.all(); 43 | var relation2 = relation.where$("foo"); 44 | assertEquals(relation, relation2); 45 | assertEquals(asList("(foo)"), relation2.getValues().getWhere()); 46 | } 47 | 48 | @Test 49 | public void testGroup$() { 50 | var relation = postRepository.all(); 51 | var relation2 = relation.group$("foo"); 52 | assertEquals(relation, relation2); 53 | assertEquals(asList("foo"), relation2.getValues().getGroup()); 54 | } 55 | 56 | @Test 57 | public void testHaving$() { 58 | var relation = postRepository.all(); 59 | var relation2 = relation.having$("foo"); 60 | assertEquals(relation, relation2); 61 | assertEquals(asList("(foo)"), relation2.getValues().getHaving()); 62 | } 63 | 64 | @Test 65 | public void testOrder$() { 66 | var relation = postRepository.all(); 67 | var relation2 = relation.order$("foo"); 68 | assertEquals(relation, relation2); 69 | assertEquals(asList("foo"), relation2.getValues().getOrder()); 70 | } 71 | 72 | @Test 73 | public void testLimit$() { 74 | var relation = postRepository.all(); 75 | var relation2 = relation.limit$(999); 76 | assertEquals(relation, relation2); 77 | assertEquals(999, relation2.getValues().getLimit()); 78 | } 79 | 80 | @Test 81 | public void testDistinct$() { 82 | var relation = postRepository.all(); 83 | var relation2 = relation.distinct$(true); 84 | assertEquals(relation, relation2); 85 | assertEquals(true, relation2.getValues().isDistinct()); 86 | } 87 | 88 | @Test 89 | public void testIncludes$() { 90 | var relation = postRepository.all(); 91 | var relation2 = relation.includes$("foo"); 92 | assertEquals(relation, relation2); 93 | assertEquals(asList("foo"), relation2.getValues().getIncludes()); 94 | } 95 | 96 | @Test 97 | public void testEagerLoads$() { 98 | var relation = postRepository.all(); 99 | var relation2 = relation.eagerLoad$("foo"); 100 | assertEquals(relation, relation2); 101 | assertEquals(asList("foo"), relation2.getValues().getEagerLoad()); 102 | } 103 | 104 | @Test 105 | public void testLock$() { 106 | var relation = postRepository.all(); 107 | var relation2 = relation.lock$(LockModeType.WRITE); 108 | assertEquals(relation, relation2); 109 | assertEquals(LockModeType.WRITE, relation2.getValues().getLock()); 110 | } 111 | 112 | @Test 113 | public void testFrom$() { 114 | var relation = postRepository.all(); 115 | var relation2 = relation.from$("foo"); 116 | assertEquals(relation, relation2); 117 | assertEquals("foo", relation2.getValues().getFrom()); 118 | } 119 | 120 | @Test 121 | public void testMerge$() { 122 | var relation = postRepository.where("1=1"); 123 | var relation2 = relation.merge$(postRepository.where("1=2")); 124 | assertEquals(relation, relation2); 125 | assertEquals(asList("(1=1)", "(1=2)"), relation2.getValues().getWhere()); 126 | } 127 | 128 | @Test 129 | public void testReOrder$() { 130 | var relation = postRepository.order("foo"); 131 | var relation2 = relation.reorder$("buzz"); 132 | assertEquals(relation, relation2); 133 | assertEquals(asList("buzz"), relation2.getValues().getOrder()); 134 | } 135 | 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/relation/FinderMethods.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.relation; 2 | 3 | import static com.activepersistence.repository.Arel.jpql; 4 | import com.activepersistence.repository.Relation; 5 | import com.activepersistence.repository.arel.SelectManager; 6 | import com.activepersistence.repository.arel.nodes.And; 7 | import com.activepersistence.repository.arel.visitors.Visitable; 8 | import com.activepersistence.repository.connectionadapters.JpaAdapter; 9 | import static com.activepersistence.repository.relation.ValueMethods.DISTINCT; 10 | import static com.activepersistence.repository.relation.ValueMethods.ORDER; 11 | import static com.activepersistence.repository.relation.ValueMethods.SELECT; 12 | import static java.beans.Introspector.decapitalize; 13 | import static java.lang.String.format; 14 | import static java.util.Arrays.asList; 15 | import static java.util.Arrays.stream; 16 | import java.util.List; 17 | import static java.util.Optional.ofNullable; 18 | import java.util.function.Function; 19 | import static java.util.stream.Collectors.toList; 20 | import javax.persistence.EntityNotFoundException; 21 | 22 | public interface FinderMethods { 23 | 24 | public static final String ONE_AS_ONE = "1 AS one"; 25 | 26 | public Class getEntityClass(); 27 | 28 | public String getPrimaryKeyAttr(); 29 | 30 | public String getAlias(); 31 | 32 | public Values getValues(); 33 | 34 | public SelectManager getArel(); 35 | 36 | public JpaAdapter getConnection(); 37 | 38 | public boolean isLoaded(); 39 | 40 | public List getRecords(); 41 | 42 | public default T take() { 43 | return first(thiz().limit(1)); 44 | } 45 | 46 | public default List take(int limit) { 47 | return thiz().limit(limit); 48 | } 49 | 50 | public default T take$() { 51 | return ofNullable(take()).orElseGet(() -> raiseEntityNotFoundException()); 52 | } 53 | 54 | public default T first() { 55 | return thiz().order(getPrimaryKeyAttr()).take(); 56 | } 57 | 58 | public default T first$() { 59 | return thiz().order(getPrimaryKeyAttr()).take$(); 60 | } 61 | 62 | public default List first(int limit) { 63 | return thiz().order(getPrimaryKeyAttr()).take(limit); 64 | } 65 | 66 | public default T last() { 67 | return thiz().order(getPrimaryKeyAttr() + " DESC").take(); 68 | } 69 | 70 | public default T last$() { 71 | return thiz().order(getPrimaryKeyAttr() + " DESC").take$(); 72 | } 73 | 74 | public default List last(int limit) { 75 | return thiz().order(getPrimaryKeyAttr() + " DESC").take(limit); 76 | } 77 | 78 | public default T find(Object id) { 79 | return thiz().where(getPrimaryKeyAttr() + " = ?", id).take$(); 80 | } 81 | 82 | public default List find(Object... ids) { 83 | return thiz().where(getPrimaryKeyAttr() + " IN (?)", asList(ids)); 84 | } 85 | 86 | public default T findBy(String conditions, Object... params) { 87 | return thiz().where(conditions, params).take(); 88 | } 89 | 90 | public default T findBy$(String conditions, Object... params) { 91 | return thiz().where(conditions, params).take$(); 92 | } 93 | 94 | public default T findByExpression(String expression, Object... params) { 95 | return findBy(expressionToJpql(expression), params); 96 | } 97 | 98 | public default T findByExpression$(String expression, Object... params) { 99 | return findBy$(expressionToJpql(expression), params); 100 | } 101 | 102 | public default boolean exists(String conditions, Object... params) { 103 | return thiz().where(conditions, params).exists(); 104 | } 105 | 106 | public default boolean exists() { 107 | return getConnection().selectAll(constructRelationForExists().getArel()).size() == 1; 108 | } 109 | 110 | private Relation constructRelationForExists() { 111 | if (getValues().isDistinct() && getValues().getOffset() > 0) { 112 | return thiz().except(ORDER).limit(1); 113 | } else { 114 | return thiz().except(SELECT, DISTINCT, ORDER).select2(ONE_AS_ONE).limit(1); 115 | } 116 | } 117 | 118 | private T raiseEntityNotFoundException() { 119 | if (getValues().getWhere().isEmpty()) { 120 | throw new EntityNotFoundException(format("Couldn't find %s", className())); 121 | } else { 122 | throw new EntityNotFoundException(format("Couldn't find %s with %s", className(), getConditions())); 123 | } 124 | } 125 | 126 | private String expressionToJpql(String expression) { 127 | return new And(conditionsFor(expression)).toJpql(); 128 | } 129 | 130 | private List conditionsFor(String expression) { 131 | return stream(expression.split("And")).map(toConditions()).collect(toList()); 132 | } 133 | 134 | private Function toConditions() { 135 | return attr -> jpql(getAlias() + "." + decapitalize(attr) + " = ?"); 136 | } 137 | 138 | private String className() { 139 | return getEntityClass().getSimpleName(); 140 | } 141 | 142 | private String getConditions() { 143 | return "[" + getArel().whereJpql() + "]"; 144 | } 145 | 146 | private Relation thiz() { 147 | return (Relation) this; 148 | } 149 | 150 | private T first(List items) { 151 | return items.stream().findFirst().orElse(null); 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /src/test/java/com/activepersistence/repository/cases/relation/QueryMethodsTest.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.cases.relation; 2 | 3 | import com.activepersistence.IntegrationTest; 4 | import com.activepersistence.ReadOnlyRecord; 5 | import com.activepersistence.repository.models.PostRepository; 6 | import static com.activepersistence.repository.relation.ValueMethods.FROM; 7 | import static com.activepersistence.repository.relation.ValueMethods.ORDER; 8 | import java.util.Map; 9 | import javax.inject.Inject; 10 | import org.jboss.arquillian.persistence.UsingDataSet; 11 | import static org.junit.Assert.*; 12 | import org.junit.Test; 13 | 14 | @UsingDataSet({"posts.xml", "comments.xml", "clients.xml"}) 15 | public class QueryMethodsTest extends IntegrationTest { 16 | 17 | @Inject 18 | private PostRepository postRepository; 19 | 20 | @Test 21 | public void testSelect() { 22 | assertEquals("SELECT NEW com.activepersistence.repository.models.Post(post.id, post.title) FROM Post post", 23 | postRepository.select("post.id", "post.title").toJpql()); 24 | } 25 | 26 | @Test 27 | public void testSelectScalar() { 28 | assertEquals("SELECT post.id, post.title FROM Post post", 29 | postRepository.select2("post.id", "post.title").toJpql()); 30 | } 31 | 32 | @Test 33 | public void testDistinct() { 34 | assertEquals("SELECT DISTINCT NEW com.activepersistence.repository.models.Post(post.id, post.title) FROM Post post", 35 | postRepository.select("post.id", "post.title").distinct().toJpql()); 36 | } 37 | 38 | @Test 39 | public void testJoins() { 40 | assertEquals("SELECT post FROM Post post JOIN post.comments comments", 41 | postRepository.joins("JOIN post.comments comments").toJpql()); 42 | } 43 | 44 | @Test 45 | public void testWhere() { 46 | assertEquals("SELECT post FROM Post post WHERE (post.title = 'nixon')", 47 | postRepository.where("post.title = 'nixon'").toJpql()); 48 | } 49 | 50 | @Test 51 | public void testGroup() { 52 | assertEquals("SELECT NEW com.activepersistence.repository.models.Post(post.id, post.title) FROM Post post GROUP BY post.id, post.title", 53 | postRepository.select("post.id, post.title").group("post.id, post.title").toJpql()); 54 | } 55 | 56 | @Test 57 | public void testHaving() { 58 | assertEquals("SELECT NEW com.activepersistence.repository.models.Post(post.id) FROM Post post GROUP BY post.id HAVING (COUNT(post.title) > 0)", 59 | postRepository.select("post.id").group("post.id").having("COUNT(post.title) > 0").toJpql()); 60 | } 61 | 62 | @Test 63 | public void testOrder() { 64 | assertEquals("SELECT post FROM Post post ORDER BY post.id", postRepository.order("post.id").toJpql()); 65 | } 66 | 67 | @Test 68 | public void testNone() { 69 | assertEquals("", postRepository.none().toJpql()); 70 | } 71 | 72 | @Test 73 | public void testFrom() { 74 | assertEquals("SELECT post FROM Topic post", postRepository.from("Topic post").toJpql()); 75 | } 76 | 77 | @Test 78 | public void testUnscope() { 79 | assertEquals("SELECT post FROM Post post WHERE (1=0)", postRepository.where("1=0").order("post.id").unscope(ORDER).toJpql()); 80 | } 81 | 82 | @Test 83 | public void testUnscopeFrom() { 84 | assertEquals("SELECT post FROM Post post WHERE (1=0)", postRepository.where("1=0").from("Teste teste").unscope(FROM).toJpql()); 85 | } 86 | 87 | @Test 88 | public void testReSelect() { 89 | assertEquals("SELECT NEW com.activepersistence.repository.models.Post(post.title) FROM Post post", postRepository.select("post.id").reselect("post.title").toJpql()); 90 | } 91 | 92 | @Test 93 | public void testReWhere() { 94 | assertEquals("SELECT post FROM Post post WHERE (1=0)", postRepository.where("1=0").rewhere("1=0").toJpql()); 95 | } 96 | 97 | @Test 98 | public void testReOrder() { 99 | assertEquals("SELECT post FROM Post post ORDER BY post.id", postRepository.order("post.title").reorder("post.id").toJpql()); 100 | } 101 | 102 | @Test 103 | public void testLimit() { 104 | assertEquals(2, postRepository.limit(2).size()); 105 | } 106 | 107 | @Test 108 | public void testOffset() { 109 | assertEquals(1, postRepository.where("post.id IN (1, 2, 3)").limit(2).offset(2).size()); 110 | } 111 | 112 | @Test 113 | public void testIncludes() { 114 | var posts = postRepository.includes("post.comments"); 115 | var comments = posts.stream().flatMap(p -> p.getComments().stream()); 116 | assertTrue(comments.findFirst().isPresent()); 117 | } 118 | 119 | @Test 120 | public void testEagerLoads() { 121 | var posts = postRepository.eagerLoad("post.comments"); 122 | var comments = posts.stream().flatMap(p -> p.getComments().stream()); 123 | assertTrue(comments.findFirst().isPresent()); 124 | } 125 | 126 | @Test 127 | public void testLock() { 128 | assertNotNull(postRepository.lock()); 129 | } 130 | 131 | @Test 132 | public void testUpdateReadOnly() { 133 | var post = postRepository.readonly().first(); 134 | post.setTitle("change"); 135 | 136 | assertThrows(ReadOnlyRecord.class,() -> postRepository.save(post)); 137 | } 138 | 139 | @Test 140 | public void testDestroyReadOnly() { 141 | var post = postRepository.readonly().first(); 142 | assertThrows(ReadOnlyRecord.class,() -> postRepository.destroy(post)); 143 | } 144 | 145 | @Test 146 | public void testBind() { 147 | assertEquals("SELECT post FROM Post post WHERE (post.id = 1)", postRepository.where("post.id = ?", 1).toJpql()); 148 | } 149 | 150 | @Test 151 | public void testBindNamed() { 152 | assertEquals("SELECT post FROM Post post WHERE (post.id = 1)", postRepository.where("post.id = :id", Map.of("id", 1)).toJpql()); 153 | } 154 | 155 | @Test 156 | public void testBindFormat() { 157 | assertEquals("SELECT post FROM Post post WHERE (post.id = 1)", postRepository.where("post.id = %s", 1).toJpql()); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/relation/Calculation.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.relation; 2 | 3 | import static com.activepersistence.repository.Arel.jpql; 4 | import com.activepersistence.repository.Relation; 5 | import com.activepersistence.repository.arel.SelectManager; 6 | import com.activepersistence.repository.arel.nodes.Function; 7 | import com.activepersistence.repository.connectionadapters.JpaAdapter; 8 | import static com.activepersistence.repository.relation.Calculation.Operations.*; 9 | import static com.activepersistence.repository.relation.FinderMethods.ONE_AS_ONE; 10 | import static com.activepersistence.repository.relation.ValueMethods.ORDER; 11 | import java.util.ArrayList; 12 | import static java.util.Arrays.asList; 13 | import static java.util.Arrays.copyOfRange; 14 | import java.util.List; 15 | import java.util.Objects; 16 | import static java.util.stream.Collectors.toMap; 17 | 18 | public interface Calculation { 19 | 20 | public enum Operations { COUNT, MIN, MAX, AVG, SUM } 21 | 22 | public Values getValues(); 23 | 24 | public String getAlias(); 25 | 26 | public String getPrimaryKeyAttr(); 27 | 28 | public JpaAdapter getConnection(); 29 | 30 | public Relation spawn(); 31 | 32 | public Boolean hasLimitOrOffset(); 33 | 34 | public default Object count() { 35 | return count(getAlias()); 36 | } 37 | 38 | public default Object count(String field) { 39 | return calculate(COUNT, field); 40 | } 41 | 42 | public default Object minimum(String field) { 43 | return calculate(MIN, field); 44 | } 45 | 46 | public default Object maximum(String field) { 47 | return calculate(MAX, field); 48 | } 49 | 50 | public default Object average(String field) { 51 | return calculate(AVG, field); 52 | } 53 | 54 | public default Object sum(String field) { 55 | return calculate(SUM, field); 56 | } 57 | 58 | public default List ids() { 59 | return pluck(getPrimaryKeyAttr()); 60 | } 61 | 62 | public default List pluck(String... fields) { 63 | var relation = spawn(); 64 | relation.getValues().setConstructor(false); 65 | relation.getValues().setSelect(asList(fields)); 66 | 67 | return getConnection().selectAll(relation.getArel()); 68 | } 69 | 70 | public default Object calculate(Operations operation, String field) { 71 | var distinct = getValues().isDistinct(); 72 | 73 | if (getValues().getGroup().isEmpty()) { 74 | return executeSimpleCalculation(operation, field, distinct); 75 | } else { 76 | return executeGroupedCalculation(operation, field, distinct); 77 | } 78 | } 79 | 80 | private Object executeSimpleCalculation(Operations operation, String field, boolean distinct) { 81 | SelectManager queryBuilder; 82 | 83 | if (operation == COUNT && (field.equals(getAlias()) && distinct || hasLimitOrOffset())) { 84 | queryBuilder = buildCountSubquery(spawn(), field, distinct); 85 | } else { 86 | var relation = thiz().unscope(ORDER).distinct(false); 87 | 88 | var selectValue = operationOverAggregateColumn(operation, field, distinct); 89 | relation.getValues().setConstructor(false); 90 | relation.getValues().setSelect(asList(selectValue.toJpql())); 91 | 92 | queryBuilder = relation.getArel(); 93 | } 94 | 95 | if (operation == COUNT && (field.equals(getAlias()) && distinct || hasLimitOrOffset())) { 96 | return count(getConnection().selectAll(queryBuilder)); 97 | } else { 98 | return first(getConnection().selectAll(queryBuilder)); 99 | } 100 | } 101 | 102 | private SelectManager buildCountSubquery(Relation relation, String field, boolean distinct) { 103 | if (Objects.equals(field, getAlias())) { 104 | relation.getValues().setConstructor(false); 105 | if (!distinct) relation.getValues().setSelect(asList(ONE_AS_ONE)); 106 | } else { 107 | relation.getValues().setConstructor(false); 108 | relation.getValues().setSelect(asList(field)); 109 | } 110 | return relation.getArel(); 111 | } 112 | 113 | private Object executeGroupedCalculation(Operations operation, String field, boolean distinct) { 114 | var groupFields = getValues().getGroup(); 115 | 116 | var selectValue = operationOverAggregateColumn(operation, field, distinct); 117 | 118 | var selectValues = new ArrayList(); 119 | selectValues.add(selectValue.toJpql()); 120 | selectValues.addAll(getValues().getSelect()); 121 | 122 | var relation = thiz().distinct(false); 123 | relation.getValues().setConstructor(false); 124 | relation.getValues().setSelect(selectValues); 125 | relation.getValues().getSelect().addAll(groupFields); 126 | 127 | List calculatedData = getConnection().selectAll(relation.getArel()); 128 | 129 | if (groupFields.size() > 1) { 130 | return calculatedData.stream().collect(toMap(v -> copyOfRange(v, 1, v.length), v -> v[0])); 131 | } else { 132 | return calculatedData.stream().collect(toMap(v -> v[1], v -> v[0])); 133 | } 134 | } 135 | 136 | private Function operationOverAggregateColumn(Operations operation, String field, boolean distinct) { 137 | switch (operation) { 138 | case COUNT: 139 | return jpql(field).count(distinct); 140 | case MIN: 141 | return jpql(field).minimum(); 142 | case MAX: 143 | return jpql(field).maximum(); 144 | case AVG: 145 | return jpql(field).average(); 146 | case SUM: 147 | return jpql(field).sum(); 148 | default: 149 | throw new RuntimeException("Operation not supported: " + operation); 150 | } 151 | } 152 | 153 | private Long count(List items) { 154 | return items.stream().filter(Objects::nonNull).count(); 155 | } 156 | 157 | private Object first(List items) { 158 | return items.stream().findFirst().orElse(null); 159 | } 160 | 161 | private Relation thiz() { 162 | return (Relation) this; 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.github.lazaronixon 6 | active-persistence 7 | jar 8 | 0.0.32-SNAPSHOT 9 | 10 | ActivePersistence 11 | https://github.com/lazaronixon/active-persistence 12 | 13 | 14 | Active Persistence is a implementation of Active Record Query Interface for JPA 15 | 16 | 17 | 18 | UTF-8 19 | 11 20 | 11 21 | 22 | 23 | 24 | 25 | 26 | org.jboss.arquillian 27 | arquillian-bom 28 | 1.6.0.Final 29 | pom 30 | import 31 | 32 | 33 | 34 | 35 | 36 | 37 | jakarta.persistence 38 | jakarta.persistence-api 39 | 2.2.3 40 | provided 41 | 42 | 43 | 44 | jakarta.transaction 45 | jakarta.transaction-api 46 | 1.3.3 47 | provided 48 | 49 | 50 | 51 | 52 | junit 53 | junit 54 | 4.13.1 55 | test 56 | 57 | 58 | 59 | 60 | org.jboss.arquillian.junit 61 | arquillian-junit-container 62 | test 63 | 64 | 65 | 66 | 67 | org.jboss.arquillian.extension 68 | arquillian-persistence-dbunit 69 | 1.0.0.Alpha7 70 | test 71 | 72 | 73 | 74 | 75 | fish.payara.arquillian 76 | arquillian-payara-server-remote 77 | 2.3.3 78 | test 79 | 80 | 81 | 82 | 83 | 84 | ossrh 85 | https://oss.sonatype.org/content/repositories/snapshots 86 | 87 | 88 | ossrh 89 | https://oss.sonatype.org/service/local/staging/deploy/maven2 90 | 91 | 92 | 93 | 94 | 95 | MIT License 96 | http://www.opensource.org/licenses/mit-license.php 97 | repo 98 | 99 | 100 | 101 | 102 | scm:git:https://github.com/lazaronixon/active-persistence.git 103 | scm:git:https://github.com/lazaronixon/active-persistence.git 104 | https://github.com/lazaronixon/active-persistence 105 | 106 | 107 | 108 | 109 | lazaronixon 110 | Lazaro Nixon 111 | 112 | 113 | 114 | 115 | 116 | release 117 | 118 | 119 | 120 | org.apache.maven.plugins 121 | maven-source-plugin 122 | 3.2.1 123 | 124 | 125 | attach-sources 126 | 127 | jar-no-fork 128 | 129 | 130 | 131 | 132 | 133 | org.apache.maven.plugins 134 | maven-javadoc-plugin 135 | 3.2.0 136 | 137 | 138 | attach-javadocs 139 | 140 | jar 141 | 142 | 143 | 144 | 145 | 146 | org.apache.maven.plugins 147 | maven-gpg-plugin 148 | 1.6 149 | 150 | 151 | sign-artifacts 152 | verify 153 | 154 | sign 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/relation/Values.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.relation; 2 | 3 | import static com.activepersistence.repository.relation.ValueMethods.values; 4 | import java.util.ArrayList; 5 | import static java.util.Arrays.asList; 6 | import static java.util.Arrays.stream; 7 | import java.util.List; 8 | import java.util.function.Predicate; 9 | import javax.persistence.LockModeType; 10 | 11 | public class Values { 12 | 13 | private String from = null; 14 | 15 | private int limit = 0; 16 | private int offset = 0; 17 | private boolean distinct = false; 18 | private boolean reordering = false; 19 | private boolean constructor = false; 20 | private boolean readonly = false; 21 | 22 | private List select = new ArrayList(); 23 | private List where = new ArrayList(); 24 | private List group = new ArrayList(); 25 | private List having = new ArrayList(); 26 | private List order = new ArrayList(); 27 | private List joins = new ArrayList(); 28 | private List includes = new ArrayList(); 29 | private List eagerLoad = new ArrayList(); 30 | private List unscope = new ArrayList(); 31 | 32 | private LockModeType lock = LockModeType.NONE; 33 | 34 | public Values() {} 35 | 36 | public Values(Values other) { 37 | constructor = other.constructor; 38 | from = other.from; 39 | limit = other.limit; 40 | offset = other.offset; 41 | lock = other.lock; 42 | distinct = other.distinct; 43 | reordering = other.reordering; 44 | readonly = other.readonly; 45 | select = new ArrayList(other.select); 46 | where = new ArrayList(other.where); 47 | group = new ArrayList(other.group); 48 | having = new ArrayList(other.having); 49 | order = new ArrayList(other.order); 50 | joins = new ArrayList(other.joins); 51 | includes = new ArrayList(other.includes); 52 | eagerLoad = new ArrayList(other.eagerLoad); 53 | unscope = new ArrayList(other.unscope); 54 | } 55 | 56 | public String getFrom() { 57 | return from; 58 | } 59 | 60 | public List getSelect() { 61 | return select; 62 | } 63 | 64 | public List getWhere() { 65 | return where; 66 | } 67 | 68 | public List getGroup() { 69 | return group; 70 | } 71 | 72 | public List getHaving() { 73 | return having; 74 | } 75 | 76 | public List getOrder() { 77 | return order; 78 | } 79 | 80 | public List getJoins() { 81 | return joins; 82 | } 83 | 84 | public List getIncludes() { 85 | return includes; 86 | } 87 | 88 | public List getEagerLoad() { 89 | return eagerLoad; 90 | } 91 | 92 | public List getUnscope() { 93 | return unscope; 94 | } 95 | 96 | public int getLimit() { 97 | return limit; 98 | } 99 | 100 | public int getOffset() { 101 | return offset; 102 | } 103 | 104 | public LockModeType getLock() { 105 | return lock; 106 | } 107 | 108 | public boolean isDistinct() { 109 | return distinct; 110 | } 111 | 112 | public boolean isConstructor() { 113 | return constructor; 114 | } 115 | 116 | public boolean isReordering() { 117 | return reordering; 118 | } 119 | 120 | public boolean isReadonly() { 121 | return readonly; 122 | } 123 | 124 | public void setSelect(List select) { 125 | this.select = select; 126 | } 127 | 128 | public void setFrom(String from) { 129 | this.from = from; 130 | } 131 | 132 | public void setLimit(int limit) { 133 | this.limit = limit; 134 | } 135 | 136 | public void setOffset(int offset) { 137 | this.offset = offset; 138 | } 139 | 140 | public void setDistinct(boolean distinct) { 141 | this.distinct = distinct; 142 | } 143 | 144 | public void setLock(LockModeType lock) { 145 | this.lock = lock; 146 | } 147 | 148 | public void setConstructor(boolean constructor) { 149 | this.constructor = constructor; 150 | } 151 | 152 | public void setOrder(List order) { 153 | this.order = order; 154 | } 155 | 156 | public void setReordering(boolean reordering) { 157 | this.reordering = reordering; 158 | } 159 | 160 | public void setReadonly(boolean readonly) { 161 | this.readonly = readonly; 162 | } 163 | 164 | public void setGroup(List group) { 165 | this.group = group; 166 | } 167 | 168 | public Values except(ValueMethods... skips) { 169 | return dup().except$(skips); 170 | } 171 | 172 | public Values except$(ValueMethods... skips) { 173 | stream(skips).forEach(this::reset); return this; 174 | } 175 | 176 | public Values slice(ValueMethods... onlies) { 177 | return dup().slice$(onlies); 178 | } 179 | 180 | public Values slice$(ValueMethods... onlies) { 181 | stream(values()).filter(notContains(onlies)).forEach(this::reset); return this; 182 | } 183 | 184 | private Predicate notContains(ValueMethods[] onlies) { 185 | return not(asList(onlies)::contains); 186 | } 187 | 188 | private Predicate not(Predicate t) { 189 | return t.negate(); 190 | } 191 | 192 | private Values dup() { 193 | return new Values(this); 194 | } 195 | 196 | private void reset(ValueMethods value) { 197 | switch (value) { 198 | case FROM: from = null; break; 199 | 200 | case LOCK: lock = LockModeType.NONE; break; 201 | 202 | case LIMIT: limit = 0; break; 203 | case OFFSET: offset = 0; break; 204 | case DISTINCT: distinct = false; break; 205 | case REORDERING: reordering = false; break; 206 | case CONSTRUCTOR: constructor = false; break; 207 | case READONLY: readonly = false; break; 208 | 209 | case SELECT: select.clear(); break; 210 | case WHERE: where.clear(); break; 211 | case GROUP: group.clear(); break; 212 | case HAVING: having.clear(); break; 213 | case ORDER: order.clear(); break; 214 | case JOINS: joins.clear(); break; 215 | case INCLUDES: includes.clear(); break; 216 | case EAGER_LOAD: eagerLoad.clear(); break; 217 | case UNSCOPE: unscope.clear(); break; 218 | } 219 | } 220 | 221 | } 222 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/arel/visitors/ToJpql.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.arel.visitors; 2 | 3 | import com.activepersistence.repository.arel.Entity; 4 | import com.activepersistence.repository.arel.nodes.And; 5 | import com.activepersistence.repository.arel.nodes.Assignment; 6 | import com.activepersistence.repository.arel.nodes.Avg; 7 | import com.activepersistence.repository.arel.nodes.Constructor; 8 | import com.activepersistence.repository.arel.nodes.Count; 9 | import com.activepersistence.repository.arel.nodes.DeleteStatement; 10 | import com.activepersistence.repository.arel.nodes.Distinct; 11 | import com.activepersistence.repository.arel.nodes.Function; 12 | import com.activepersistence.repository.arel.nodes.Grouping; 13 | import com.activepersistence.repository.arel.nodes.JoinSource; 14 | import com.activepersistence.repository.arel.nodes.JpqlLiteral; 15 | import com.activepersistence.repository.arel.nodes.Max; 16 | import com.activepersistence.repository.arel.nodes.Min; 17 | import com.activepersistence.repository.arel.nodes.SelectCore; 18 | import com.activepersistence.repository.arel.nodes.SelectStatement; 19 | import com.activepersistence.repository.arel.nodes.StringJoin; 20 | import com.activepersistence.repository.arel.nodes.Sum; 21 | import com.activepersistence.repository.arel.nodes.UpdateStatement; 22 | import com.activepersistence.repository.connectionadapters.Quoting; 23 | import java.util.List; 24 | 25 | public class ToJpql extends Visitor { 26 | 27 | @Override 28 | public String compile(Visitable node, StringBuilder collector) { 29 | return accept(node, collector).toString(); 30 | } 31 | 32 | public StringBuilder visitDeleteStatement(DeleteStatement o, StringBuilder collector) { 33 | collector.append("DELETE FROM "); 34 | collector = visitEntity(o.getRelation(), collector); 35 | collectNodesFor(o.getWheres(), collector, " WHERE ", " AND "); 36 | collectNodesFor(o.getOrders(), collector, " ORDER BY "); 37 | return collector; 38 | } 39 | 40 | public StringBuilder visitUpdateStatement(UpdateStatement o, StringBuilder collector) { 41 | collector.append("UPDATE "); 42 | collector = visitEntity(o.getRelation(), collector); 43 | collectNodesFor(o.getValues(), collector, " SET "); 44 | collectNodesFor(o.getWheres(), collector, " WHERE ", " AND "); 45 | collectNodesFor(o.getOrders(), collector, " ORDER BY "); 46 | return collector; 47 | } 48 | 49 | public StringBuilder visitSelectStatement(SelectStatement o, StringBuilder collector) { 50 | collector = visitSelectCore(o.getCore(), collector); 51 | 52 | if (!o.getOrders().isEmpty()) { 53 | collector.append(" ORDER BY "); 54 | collectNodesFor(o.getOrders(), collector, ""); 55 | } 56 | 57 | return collector; 58 | } 59 | 60 | public StringBuilder visitSelectCore(SelectCore o, StringBuilder collector) { 61 | collector.append("SELECT"); 62 | 63 | collector = maybeVisit(o.getSetQuantifier(), collector); 64 | 65 | if (o.getConstructor() != null) { 66 | collector = visitConstructor(o.getConstructor(), collector); 67 | } else { 68 | collectNodesFor(o.getProjections(), collector, " "); 69 | } 70 | 71 | collector.append(" FROM "); 72 | collector = visit(o.getSource(), collector); 73 | 74 | collectNodesFor(o.getWheres(), collector, " WHERE ", " AND "); 75 | collectNodesFor(o.getGroups(), collector, " GROUP BY "); 76 | collectNodesFor(o.getHavings(), collector, " HAVING ", " AND "); 77 | 78 | return collector; 79 | } 80 | 81 | public StringBuilder visitConstructor(Constructor o, StringBuilder collector) { 82 | collector.append(" NEW "); 83 | collector.append(o.getName()).append("("); 84 | collectNodesFor(o.getProjections(), collector, ""); 85 | collector.append(")"); 86 | 87 | return collector; 88 | } 89 | 90 | public StringBuilder visitEntity(Entity o, StringBuilder collector) { 91 | return collector.append(o.getSimpleName()).append(" ").append(o.getAlias()); 92 | } 93 | 94 | public StringBuilder visitJoinSource(JoinSource o, StringBuilder collector) { 95 | collector = visit(o.getSource(), collector); 96 | 97 | if (!o.getJoins().isEmpty()) { 98 | collector.append(" "); 99 | injectJoin(o.getJoins(), collector, " "); 100 | } 101 | 102 | return collector; 103 | } 104 | 105 | public StringBuilder visitStringJoin(StringJoin o, StringBuilder collector) { 106 | return visit(o.getPath(), collector); 107 | } 108 | 109 | public StringBuilder visitDistinct(Distinct o, StringBuilder collector) { 110 | return collector.append("DISTINCT"); 111 | } 112 | 113 | public StringBuilder visitJpqlLiteral(JpqlLiteral o, StringBuilder collector) { 114 | return collector.append(o.toString()); 115 | } 116 | 117 | public StringBuilder visitCount(Count o, StringBuilder collector) { 118 | return aggregate("COUNT", o, collector); 119 | } 120 | 121 | public StringBuilder visitSum(Sum o, StringBuilder collector) { 122 | return aggregate("SUM", o, collector); 123 | } 124 | 125 | public StringBuilder visitMax(Max o, StringBuilder collector) { 126 | return aggregate("MAX", o, collector); 127 | } 128 | 129 | public StringBuilder visitMin(Min o, StringBuilder collector) { 130 | return aggregate("MIN", o, collector); 131 | } 132 | 133 | public StringBuilder visitAvg(Avg o, StringBuilder collector) { 134 | return aggregate("AVG", o, collector); 135 | } 136 | 137 | public StringBuilder visitAssignment(Assignment o, StringBuilder collector) { 138 | collector = visit(o.getField(), collector); 139 | collector.append(" = "); 140 | collector.append(quote(o.getValue())); 141 | return collector; 142 | } 143 | 144 | public StringBuilder visitAnd(And o, StringBuilder collector) { 145 | return injectJoin(o.getChildren(), collector, " AND "); 146 | } 147 | 148 | public StringBuilder visitGrouping(Grouping o, StringBuilder collector) { 149 | if (o.getValue() instanceof Grouping) { 150 | return visit(o.getValue(), collector); 151 | } else { 152 | collector.append("("); 153 | return visit(o.getValue(), collector).append(")"); 154 | } 155 | } 156 | 157 | private StringBuilder maybeVisit(Visitable node, StringBuilder collector) { 158 | return node != null ? visit(node, collector.append(" ")) : collector; 159 | } 160 | 161 | private void collectNodesFor(List nodes, StringBuilder collector, String spacer, String connector) { 162 | if (!nodes.isEmpty()) collector.append(spacer); injectJoin(nodes, collector, connector); 163 | } 164 | 165 | private void collectNodesFor(List nodes, StringBuilder collector, String spacer) { 166 | collectNodesFor(nodes, collector, spacer, ", "); 167 | } 168 | 169 | private StringBuilder injectJoin(List list, StringBuilder collector, String joinStr) { 170 | for (var i = 0; i < list.size(); i++) { if (i != 0) collector.append(joinStr); collector = visit(list.get(i), collector); } return collector; 171 | } 172 | 173 | private StringBuilder aggregate(String name, Function o, StringBuilder collector) { 174 | collector.append(name).append("("); 175 | if (o.isDistinct()) collector.append("DISTINCT "); 176 | collector.append(o.getExpression()).append(")"); 177 | if (o.getAlias() != null) { collector.append(" AS "); visitJpqlLiteral(o.getAlias(), collector); } 178 | return collector; 179 | } 180 | 181 | private String quote(Object value) { 182 | if (value instanceof JpqlLiteral) { 183 | return value.toString(); 184 | } else { 185 | return Quoting.quote(value); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/relation/QueryMethods.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository.relation; 2 | 3 | import com.activepersistence.repository.NullRelation; 4 | import com.activepersistence.repository.Relation; 5 | import com.activepersistence.repository.arel.nodes.Grouping; 6 | import static com.activepersistence.repository.relation.ValueMethods.*; 7 | import static java.util.Arrays.asList; 8 | import static java.util.Arrays.stream; 9 | import javax.persistence.LockModeType; 10 | 11 | public interface QueryMethods { 12 | 13 | public Values getValues(); 14 | 15 | public Relation spawn(); 16 | 17 | public String sanitizeJpql(String statement, Object... values); 18 | 19 | public default Relation select(String... fields) { 20 | return spawn().select$(fields); 21 | } 22 | 23 | public default Relation select$(String... fields) { 24 | getValues().setConstructor(true); getValues().getSelect().addAll(asList(fields)); return thiz(); 25 | } 26 | 27 | public default Relation select2(String... fields) { 28 | return spawn().select2$(fields); 29 | } 30 | 31 | public default Relation select2$(String... fields) { 32 | getValues().setConstructor(false); getValues().getSelect().addAll(asList(fields)); return thiz(); 33 | } 34 | 35 | public default Relation joins(String value) { 36 | return spawn().joins$(value); 37 | } 38 | 39 | public default Relation joins$(String value) { 40 | getValues().getJoins().add(value); return thiz(); 41 | } 42 | 43 | public default Relation where(String conditions, Object... params) { 44 | return spawn().where$(conditions, params); 45 | } 46 | 47 | public default Relation where$(String conditions, Object... params) { 48 | getValues().getWhere().add(buildWhere(conditions, params)); return thiz(); 49 | } 50 | 51 | public default Relation group(String... fields) { 52 | return spawn().group$(fields); 53 | } 54 | 55 | public default Relation group$(String... fields) { 56 | getValues().getGroup().addAll(asList(fields)); return thiz(); 57 | } 58 | 59 | public default Relation having(String conditions, Object... params) { 60 | return spawn().having$(conditions, params); 61 | } 62 | 63 | public default Relation having$(String conditions, Object... params) { 64 | getValues().getHaving().add(buildWhere(conditions, params)); return thiz(); 65 | } 66 | 67 | public default Relation order(String... fields) { 68 | return spawn().order$(fields); 69 | } 70 | 71 | public default Relation order$(String... fields) { 72 | getValues().getOrder().addAll(asList(fields)); return thiz(); 73 | } 74 | 75 | public default Relation limit(int limit) { 76 | return spawn().limit$(limit); 77 | } 78 | 79 | public default Relation limit$(int limit) { 80 | getValues().setLimit(limit); return thiz(); 81 | } 82 | 83 | public default Relation offset(int offset) { 84 | return spawn().offset$(offset); 85 | } 86 | 87 | public default Relation offset$(int offset) { 88 | getValues().setOffset(offset); return thiz(); 89 | } 90 | 91 | public default Relation distinct() { 92 | return spawn().distinct$(); 93 | } 94 | 95 | public default Relation distinct(boolean value) { 96 | return spawn().distinct$(value); 97 | } 98 | 99 | public default Relation distinct$() { 100 | getValues().setDistinct(true); return thiz(); 101 | } 102 | 103 | public default Relation distinct$(boolean value) { 104 | getValues().setDistinct(value); return thiz(); 105 | } 106 | 107 | public default Relation readonly() { 108 | return spawn().readonly$(); 109 | } 110 | 111 | public default Relation readonly(boolean value) { 112 | return spawn().readonly$(value); 113 | } 114 | 115 | public default Relation readonly$() { 116 | getValues().setReadonly(true); return thiz(); 117 | } 118 | 119 | public default Relation readonly$(boolean value) { 120 | getValues().setReadonly(value); return thiz(); 121 | } 122 | 123 | public default NullRelation none() { 124 | return new NullRelation(where$("1=0")); 125 | } 126 | 127 | public default Relation includes(String... includes) { 128 | return spawn().includes$(includes); 129 | } 130 | 131 | public default Relation includes$(String... includes) { 132 | getValues().getIncludes().addAll(asList(includes)); return thiz(); 133 | } 134 | 135 | public default Relation eagerLoad(String... eagerLoads) { 136 | return spawn().eagerLoad$(eagerLoads); 137 | } 138 | 139 | public default Relation eagerLoad$(String... eagerLoads) { 140 | getValues().getEagerLoad().addAll(asList(eagerLoads)); return thiz(); 141 | } 142 | 143 | public default Relation lock() { 144 | return spawn().lock$(LockModeType.PESSIMISTIC_WRITE); 145 | } 146 | 147 | public default Relation lock(boolean value) { 148 | return spawn().lock$(value ? LockModeType.PESSIMISTIC_WRITE : LockModeType.NONE); 149 | } 150 | 151 | public default Relation lock(LockModeType value) { 152 | return spawn().lock$(value); 153 | } 154 | 155 | public default Relation lock$(LockModeType value) { 156 | getValues().setLock(value); return thiz(); 157 | } 158 | 159 | public default Relation from(String value) { 160 | return spawn().from$(value); 161 | } 162 | 163 | public default Relation from$(String value) { 164 | getValues().setFrom(value); return thiz(); 165 | } 166 | 167 | public default Relation unscope(ValueMethods... values) { 168 | return spawn().unscope$(values); 169 | } 170 | 171 | public default Relation unscope$(ValueMethods... args) { 172 | getValues().getUnscope().addAll(asList(args)); stream(args).forEach(this::unscoping); return thiz(); 173 | } 174 | 175 | public default Relation reselect(String... fields) { 176 | return spawn().except(SELECT, CONSTRUCTOR).select(fields); 177 | } 178 | 179 | public default Relation rewhere(String conditions, Object... params) { 180 | return spawn().except(WHERE).where(conditions, params); 181 | } 182 | 183 | public default Relation reorder(String... fields) { 184 | return spawn().reorder$(fields); 185 | } 186 | 187 | public default Relation reorder$(String... fields) { 188 | getValues().setReordering(true); getValues().setOrder(asList(fields)); return thiz(); 189 | } 190 | 191 | private void unscoping(ValueMethods scope) { 192 | switch (scope) { 193 | case FROM: getValues().except$(FROM); break; 194 | case WHERE: getValues().except$(WHERE); break; 195 | case HAVING: getValues().except$(HAVING); break; 196 | case SELECT: getValues().except$(SELECT, CONSTRUCTOR); break; 197 | case GROUP: getValues().except$(GROUP); break; 198 | case ORDER: getValues().except$(ORDER); break; 199 | case LOCK: getValues().except$(LOCK); break; 200 | case READONLY: getValues().except$(READONLY); break; 201 | case LIMIT: getValues().except$(LIMIT); break; 202 | case OFFSET: getValues().except$(OFFSET); break; 203 | case JOINS: getValues().except$(JOINS); break; 204 | case INCLUDES: getValues().except$(INCLUDES); break; 205 | default: throw new RuntimeException("invalid unscoping value: " + scope); 206 | } 207 | } 208 | 209 | private String buildWhere(String conditions, Object[] params) { 210 | return new Grouping(sanitizeJpql(conditions, params)).toJpql(); 211 | } 212 | 213 | private Relation thiz() { 214 | return (Relation) this; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/Querying.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository; 2 | 3 | import com.activepersistence.repository.relation.ValueMethods; 4 | import java.util.List; 5 | import java.util.Map; 6 | import javax.persistence.EntityManager; 7 | import javax.persistence.LockModeType; 8 | import javax.persistence.Query; 9 | import javax.persistence.TypedQuery; 10 | 11 | public interface Querying { 12 | 13 | public EntityManager getEntityManager(); 14 | 15 | public Class getEntityClass(); 16 | 17 | public Relation getRelation(); 18 | 19 | public Relation all(); 20 | 21 | // 22 | public default Relation merge(Relation other) { 23 | return all().merge(other); 24 | } 25 | 26 | public default Relation except(ValueMethods... skips) { 27 | return all().except(skips); 28 | } 29 | 30 | public default Relation only(ValueMethods... onlies) { 31 | return all().only(onlies); 32 | } 33 | // 34 | 35 | // 36 | public default Object count() { 37 | return all().count(); 38 | } 39 | 40 | public default Object count(String field) { 41 | return all().count(field); 42 | } 43 | 44 | public default Object minimum(String field) { 45 | return all().minimum(field); 46 | } 47 | 48 | public default Object maximum(String field) { 49 | return all().maximum(field); 50 | } 51 | 52 | public default Object average(String field) { 53 | return all().average(field); 54 | } 55 | 56 | public default Object sum(String field) { 57 | return all().sum(field); 58 | } 59 | 60 | public default List pluck(String... field) { 61 | return all().pluck(field); 62 | } 63 | 64 | public default List ids() { 65 | return all().ids(); 66 | } 67 | // 68 | 69 | // 70 | public default T take() { 71 | return all().take(); 72 | } 73 | 74 | public default T take$() { 75 | return all().take$(); 76 | } 77 | 78 | public default T first() { 79 | return all().first(); 80 | } 81 | 82 | public default T first$() { 83 | return all().first$(); 84 | } 85 | 86 | public default T last() { 87 | return all().last(); 88 | } 89 | 90 | public default T last$() { 91 | return all().last$(); 92 | } 93 | 94 | public default List take(int limit) { 95 | return all().take(limit); 96 | } 97 | 98 | public default List first(int limit) { 99 | return all().first(limit); 100 | } 101 | 102 | public default List last(int limit) { 103 | return all().last(limit); 104 | } 105 | 106 | public default T find(Object id) { 107 | return all().find(id); 108 | } 109 | 110 | public default List find(Object... ids) { 111 | return all().find(ids); 112 | } 113 | 114 | public default T findBy(String conditions, Object... params) { 115 | return all().findBy(conditions, params); 116 | } 117 | 118 | public default T findBy$(String conditions, Object... params) { 119 | return all().findBy$(conditions, params); 120 | } 121 | 122 | public default T findByExpression(String expression, Object... params) { 123 | return all().findByExpression(expression, params); 124 | } 125 | 126 | public default T findByExpression$(String expression, Object... params) { 127 | return all().findByExpression$(expression, params); 128 | } 129 | 130 | public default boolean exists(String conditions, Object... params) { 131 | return all().exists(conditions, params); 132 | } 133 | 134 | public default boolean exists() { 135 | return all().exists(); 136 | } 137 | // 138 | 139 | // 140 | public default Relation where(String conditions, Object... params) { 141 | return all().where(conditions, params); 142 | } 143 | 144 | public default Relation order(String... values) { 145 | return all().order(values); 146 | } 147 | 148 | public default Relation limit(int value) { 149 | return all().limit(value); 150 | } 151 | 152 | public default Relation offset(int value) { 153 | return all().offset(value); 154 | } 155 | 156 | public default Relation select(String... values) { 157 | return all().select(values); 158 | } 159 | 160 | public default Relation select2(String... values) { 161 | return all().select2(values); 162 | } 163 | 164 | public default Relation joins(String value) { 165 | return all().joins(value); 166 | } 167 | 168 | public default Relation group(String... values) { 169 | return all().group(values); 170 | } 171 | 172 | public default Relation having(String conditions, Object... params) { 173 | return all().having(conditions, params); 174 | } 175 | 176 | public default Relation distinct() { 177 | return all().distinct(true); 178 | } 179 | 180 | public default Relation distinct(boolean value) { 181 | return all().distinct(value); 182 | } 183 | 184 | public default Relation readonly() { 185 | return all().readonly(); 186 | } 187 | 188 | public default Relation readonly(boolean value) { 189 | return all().readonly(value); 190 | } 191 | 192 | public default Relation includes(String... values) { 193 | return all().includes(values); 194 | } 195 | 196 | public default Relation eagerLoad(String... values) { 197 | return all().eagerLoad(values); 198 | } 199 | 200 | public default Relation unscope(ValueMethods... values) { 201 | return all().unscope(values); 202 | } 203 | 204 | public default Relation reselect(String... values) { 205 | return all().reselect(values); 206 | } 207 | 208 | public default Relation rewhere(String conditions, Object... params) { 209 | return all().rewhere(conditions, params); 210 | } 211 | 212 | public default Relation reorder(String... fields) { 213 | return all().reorder(fields); 214 | } 215 | 216 | public default Relation lock() { 217 | return all().lock(); 218 | } 219 | 220 | public default Relation lock(boolean value) { 221 | return all().lock(value); 222 | } 223 | 224 | public default Relation lock(LockModeType value) { 225 | return all().lock(value); 226 | } 227 | 228 | public default Relation from(String from) { 229 | return all().from(from); 230 | } 231 | 232 | public default Relation none() { 233 | return all().none(); 234 | } 235 | // 236 | 237 | // 238 | public default List destroyAll() { 239 | return all().destroyAll(); 240 | } 241 | 242 | public default List destroyBy(String conditions, Object... params) { 243 | return all().destroyBy(conditions, params); 244 | } 245 | 246 | public default int deleteAll() { 247 | return all().deleteAll(); 248 | } 249 | 250 | public default int deleteBy(String conditions, Object... params) { 251 | return all().deleteBy(conditions, params); 252 | } 253 | 254 | public default int updateAll(String updates) { 255 | return all().updateAll(updates); 256 | } 257 | 258 | public default int updateAll(Map updates) { 259 | return all().updateAll(updates); 260 | } 261 | // 262 | 263 | // 264 | public default Query findBySql(String sqlString) { 265 | return getEntityManager().createNativeQuery(sqlString, getEntityClass()); 266 | } 267 | 268 | public default TypedQuery findByJpql(String qlString) { 269 | return getEntityManager().createQuery(qlString, getEntityClass()); 270 | } 271 | // 272 | } 273 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | pom.xml 4 | ```xml 5 | 6 | com.github.lazaronixon 7 | active-persistence 8 | 0.0.31 9 | 10 | ``` 11 | 12 | models/User.java 13 | ```java 14 | @Entity 15 | public class User extends BaseIdentity { 16 | 17 | private String name; 18 | 19 | private String occupation; 20 | 21 | private LocalDateTime createdAt; 22 | 23 | private LocalDateTime updatedAt; 24 | 25 | // Get/Set omitted by brevity 26 | } 27 | ``` 28 | 29 | repositories/UserRepository.java 30 | ```java 31 | @RequestScoped 32 | public class UserRepository extends Base { 33 | 34 | } 35 | ``` 36 | 37 | ## CRUD: Reading and Writing Data 38 | 39 | ### Create 40 | ```java 41 | User user = new User(); 42 | user.name = "David"; 43 | user.occupation = "Code Artist"; 44 | 45 | userRepository.save(user); 46 | ``` 47 | 48 | ### Read 49 | ```java 50 | // return a collection with all users 51 | List users = userRepository.all(); 52 | 53 | // return the first user 54 | User user = userRepository.first(); 55 | 56 | // return the first user named David 57 | User david = userRepository.findBy("user.name = ?", "David"); 58 | 59 | // find all users named David who are Code Artists and sort by createdAt in reverse chronological order 60 | List users = userRepository.where("user.name = 'David' AND user.occupation = 'Code Artist'").order("user.createdAt DESC"); 61 | ``` 62 | 63 | ### Update 64 | ```java 65 | User user = userRepository.findBy("user.name = ?", "David"); 66 | user.name = "Dave"; 67 | userRepository.save(user); 68 | //OR 69 | userRepository.updateAll("user.maxLoginAttempts = 3, user.mustChangePassword = 'true'"); 70 | ``` 71 | 72 | ### Delete 73 | ```java 74 | User user = userRepository.findBy("user.name = ?", "David"); 75 | userRepository.destroy(user); 76 | //OR 77 | userRepository.destroyBy("user.name = ?", "David"); 78 | userRepository.destroyAll(); 79 | ``` 80 | 81 | ### Callbacks 82 | 83 | ```java 84 | public class ClientRepository extends Base { 85 | 86 | @Override 87 | public void beforeSave(Client client) { 88 | // implementation here 89 | } 90 | 91 | @Override 92 | public void afterSave(Client client) { 93 | // implementation here 94 | } 95 | 96 | @Override 97 | public void beforeCreate(Client client) { 98 | // implementation here 99 | } 100 | 101 | @Override 102 | public void afterCreate(Client client) { 103 | // implementation here 104 | } 105 | 106 | @Override 107 | public void beforeUpdate(Client client) { 108 | // implementation here 109 | } 110 | 111 | @Override 112 | public void afterUpdate(Client client) { 113 | // implementation here 114 | } 115 | 116 | @Override 117 | public void beforeDestroy(Client client) { 118 | // implementation here 119 | } 120 | 121 | @Override 122 | public void afterDestroy(Client client) { 123 | // implementation here 124 | } 125 | 126 | } 127 | ``` 128 | 129 | ### TimeStamps 130 | ```java 131 | public class Post extends BaseIdentity { 132 | 133 | private LocalDateTime createdAt; 134 | 135 | private LocalDateTime updatedAt; 136 | 137 | @Override 138 | public void setCreatedAt(LocalDateTime createdAt) { 139 | this.createdAt = createdAt; 140 | } 141 | 142 | @Override 143 | public void setUpdatedAt(LocalDateTime updatedAt) { 144 | this.updatedAt = updatedAt; 145 | } 146 | } 147 | ``` 148 | 149 | ## Retrieving Objects from the Database 150 | 151 | ### Retrieving a Single Object 152 | ```java 153 | // Find the client with primary key (id) 10. 154 | Client client = clientRepository.find(10); 155 | 156 | // The take method retrieves a record without any implicit ordering 157 | Client client = clientRepository.take(); 158 | List clients = clientRepository.take(2); 159 | 160 | // The first method finds the first record ordered by primary key (default) 161 | Client client = clientRepository.first(); 162 | Client client = clientRepository.order("client.firstName").first(); 163 | List clients = clientRepository.first(3); 164 | 165 | // The last method finds the last record ordered by primary key (default) 166 | Client client = clientRepository.last(); 167 | Client client = clientRepository.order("client.firstName").last(); 168 | List clients = clientRepository.last(3); 169 | 170 | // The findBy method finds the first record matching some conditions 171 | Client client = clientRepository.findBy("client.firstName = ?", "Lifo"); // # 172 | Client client = clientRepository.findBy("client.firstName = ?", "Jon"); // null 173 | 174 | Client client = clientRepository.findBy$("client.firstName = ?", "does not exist"); // EntityNotFoundException 175 | ``` 176 | 177 | ### Conditions 178 | ```java 179 | //Ordinal Conditions 180 | clientRepository.where("client.ordersCount = ?", 10); 181 | clientRepository.where("client.ordersCount = ? AND client.locked = ?", 10, false); 182 | 183 | //Placeholder Conditions 184 | clientRepository.where("client.ordersCount = :count", Map.of("count", 10)); 185 | clientRepository.where("client.ordersCount = :count AND client.locked = :locked", Map.of("count", 10, "locked", false)); 186 | 187 | //SubQuery Conditions 188 | var subquery = clientRepository.select("order.client.id"); 189 | clientRepository.where("client.id IN (?), subquery); 190 | ``` 191 | 192 | ### Ordering 193 | ```java 194 | clientRepository.order("client.createdAt"); 195 | clientRepository.order("client.createdAt DESC"); 196 | clientRepository.order("client.createdAt ASC"); 197 | ``` 198 | 199 | ### Selecting Specific Fields 200 | ```java 201 | List client = clientRepository.select("client.viewableBy", "client.locked"); 202 | List client = clientRepository.select("client.name").distinct(); 203 | ``` 204 | 205 | ### Limit and Offset 206 | ```java 207 | clientRepository.limit(5); 208 | clientRepository.limit(5).offset(30); 209 | ``` 210 | 211 | ### Group 212 | ```java 213 | List orders = orderRepository.select("date(order.createdAt)", "sum(order.price)").group("date(order.createdAt)"); 214 | ``` 215 | 216 | ### Total of grouped items 217 | ```java 218 | Map result = (Map) orderRepository.group("order.status").count(); // => { 'awaiting_approval' => 7, 'paid' => 12 } 219 | ``` 220 | 221 | ### Having 222 | ```java 223 | List orders = orderRepository.select("date(order.createdAt)", "sum(order.price)").group("date(order.createdAt)").having("sum(order.price) > 100"); 224 | ``` 225 | 226 | ## Overriding Conditions 227 | 228 | ### Unscope 229 | ```java 230 | orderRepository.where("order.id > 10").limit(20).order("order.id asc").unscope(ORDER); 231 | ``` 232 | 233 | ### Only 234 | ```java 235 | orderRepository.where("order.id > 10").limit(20).order("order.id asc").only(ORDER); 236 | ``` 237 | 238 | ### Reselect 239 | ```java 240 | postRepository.select("post.title", "post.body").reselect("post.createdAt"); 241 | ``` 242 | 243 | ### Reorder 244 | ```java 245 | postRepository.order("post.title").reorder("post.createdAt"); 246 | ``` 247 | 248 | ### Rewhere 249 | ```java 250 | articleRepository.where("article.trashed = true").rewhere("article.trashed = false"); 251 | ``` 252 | 253 | ## Null Relation 254 | ```java 255 | studentRepository.none(); // returns an empty Relation and fires where 1=0. 256 | ``` 257 | 258 | ## Locking Records for Update 259 | ```java 260 | Client client = clientRepository.lock().first(); 261 | // OR 262 | Client client = clientRepository.lock(true).first(); 263 | // OR 264 | Client client = clientRepository.lock(PESSIMISTIC_WRITE).first(); 265 | ``` 266 | 267 | ## Readonly Objects 268 | ```java 269 | Client client = clientRepository.readonly().first(); 270 | client.visits = 1; 271 | 272 | clientRepository.save(); // ReadOnlyRecord Exception 273 | ``` 274 | 275 | ## Joining Tables 276 | ```java 277 | authorRepository.joins("JOIN author.post post"); 278 | ``` 279 | 280 | ## Eager Loading Associations (EclipseLink only) 281 | ```java 282 | clientRepository.includes("client.address").limit(10); 283 | clientRepository.eagerLoads("client.address").limit(10); 284 | ``` 285 | 286 | ## Scopes 287 | 288 | ### Applying a default scope 289 | ```java 290 | public class ClientRepository extends Base { 291 | 292 | @Override 293 | public Relation defaultScope() { 294 | return where("client.name = 'nixon'"); 295 | } 296 | 297 | } 298 | 299 | clientRepository.all(); // SELECT client FROM Client client WHERE client.name = 'nixon' 300 | clientRepository.unscoped().all(); // SELECT client FROM Client client 301 | ``` 302 | 303 | ### Merging of scopes 304 | ```java 305 | userRepository.merge(active()); 306 | ``` 307 | 308 | ### Removing All Scoping 309 | ```java 310 | clientRepository.unscoped(); 311 | clientRepository.where("client.published = false").unscoped(); 312 | ``` 313 | 314 | ## Dynamic Finders 315 | ```java 316 | Client client = clientRepository.findByExpression("Name", "Nixon"); 317 | Client client = clientRepository.findByExpression("NameAndLocked", "Nixon", true); 318 | // OR 319 | Client client = clientRepository.findByExpression$("Name", "not found"); // EntityNotFoundException 320 | ``` 321 | 322 | ## Finding by SQL/JPQL 323 | ```java 324 | List posts = postRepository.findBySql("SELECT id, title FROM Post WHERE id = 5").getResultList(); 325 | List posts = postRepository.findByJpql("SELECT p FROM Post p WHERE p.id = 5").getResultList(); 326 | // OR 327 | List posts = postRepository.getConnection().selectAll("SELECT id, title FROM Post WHERE id = 5", QueryType.SQL).getResultList(); 328 | List posts = postRepository.getConnection().selectAll("SELECT p FROM Post p WHERE p.id = 5", QueryType.JPQL).getResultList(); 329 | ``` 330 | 331 | ## Existence of Objects 332 | ```java 333 | boolean exists = studentRepository.exists("student.name = 'Lifo'"); 334 | boolean exists = studentRepository.where("student.name = 'Lifo'").exists(); 335 | ``` 336 | 337 | ## Pluck 338 | ```java 339 | List ids = clientRepository.where("client.active = true").pluck("client.id"); //[1, 2, 3] 340 | List ids = clientRepository.where("client.active = true").ids; //[1, 2, 3] 341 | ``` 342 | 343 | ## Calculations 344 | ```java 345 | long count = (long) clientRepository.count(); 346 | long count = (long) clientRepository.count("client.age"); 347 | int minimum = (int) clientRepository.minimum("client.age"); 348 | int maximum = (int) clientRepository.maximum("client.age"); 349 | long total = (long) clientRepository.sum("client.ordersCount"); 350 | double average = (double) clientRepository.average("client.ordersCount"); 351 | ``` 352 | 353 | ## Recommended Environment 354 | * Java 9 355 | * JakartaEE 8 356 | * Payara Server 357 | 358 | ## Testing 359 | * Install Payara Server >= 5.201 360 | * Add Resources - ./asadmin add-resources path_to/payara-resources.xml 361 | 362 | ## More info 363 | * https://github.com/lazaronixon/jsf-perfect-crud/tree/active-persistence 364 | * https://www.youtube.com/watch?v=Hw8iKrZQk4o 365 | * https://guides.rubyonrails.org/active_record_querying.html 366 | -------------------------------------------------------------------------------- /src/main/java/com/activepersistence/repository/Relation.java: -------------------------------------------------------------------------------- 1 | package com.activepersistence.repository; 2 | 3 | import com.activepersistence.ActivePersistenceError; 4 | import com.activepersistence.repository.arel.DeleteManager; 5 | import com.activepersistence.repository.arel.Entity; 6 | import com.activepersistence.repository.arel.SelectManager; 7 | import com.activepersistence.repository.arel.UpdateManager; 8 | import com.activepersistence.repository.connectionadapters.JpaAdapter; 9 | import com.activepersistence.repository.relation.Calculation; 10 | import com.activepersistence.repository.relation.FinderMethods; 11 | import com.activepersistence.repository.relation.QueryMethods; 12 | import com.activepersistence.repository.relation.SpawnMethods; 13 | import com.activepersistence.repository.relation.Values; 14 | import java.util.Collection; 15 | import static java.util.Collections.emptyList; 16 | import java.util.HashMap; 17 | import java.util.Iterator; 18 | import java.util.List; 19 | import java.util.ListIterator; 20 | import java.util.Map; 21 | import static java.util.Optional.ofNullable; 22 | import java.util.function.Supplier; 23 | import static java.util.stream.Collectors.toList; 24 | 25 | public class Relation implements List, FinderMethods, QueryMethods, Calculation, SpawnMethods { 26 | 27 | private final Base repository; 28 | 29 | private final Class entityClass; 30 | 31 | private final Entity entity; 32 | 33 | private final Values values; 34 | 35 | private SelectManager arel; 36 | 37 | private List records; 38 | 39 | private String toJpql; 40 | 41 | private boolean loaded; 42 | 43 | public Relation(Base repository, Values values) { 44 | this.entityClass = repository.getEntityClass(); 45 | this.entity = repository.getArelEntity(); 46 | this.repository = repository; 47 | this.values = values; 48 | this.loaded = false; 49 | } 50 | 51 | public Relation(Relation other) { 52 | this.entityClass = other.entityClass; 53 | this.entity = other.entity; 54 | this.repository = other.repository; 55 | this.loaded = other.loaded; 56 | this.values = new Values(other.values); 57 | reset(); 58 | } 59 | 60 | public Relation unscoped() { 61 | return repository.unscoped(); 62 | } 63 | 64 | public Relation unscoped(Supplier yield) { 65 | return repository.unscoped(yield); 66 | } 67 | 68 | public Relation scoping(Supplier yield) { 69 | var previous = Scoping.getCurrentScope(); 70 | try { 71 | Scoping.setCurrentScope(this); return yield.get(); 72 | } finally { 73 | Scoping.setCurrentScope(previous); 74 | } 75 | } 76 | 77 | public List destroyAll() { 78 | return getRecords().stream().map(this::destroy).collect(toList()); 79 | } 80 | 81 | public List destroyBy(String conditions, Object... params) { 82 | return where(conditions, params).destroyAll(); 83 | } 84 | 85 | public int deleteAll() { 86 | if (isValidRelationForUpdateOrDelete()) { 87 | var stmt = new DeleteManager(); 88 | stmt.from(entity); 89 | stmt.limit(getArel().getLimit()); 90 | stmt.offset(getArel().getOffset()); 91 | stmt.setWheres(getArel().getConstraints()); 92 | stmt.setOrders(getArel().getOrders()); 93 | 94 | var affected = getConnection().delete(stmt); 95 | 96 | reset(); 97 | 98 | return affected; 99 | } else { 100 | throw new ActivePersistenceError("deleteAll doesn't support this relation"); 101 | } 102 | } 103 | 104 | public int deleteBy(String conditions, Object... params) { 105 | return where(conditions, params).deleteAll(); 106 | } 107 | 108 | public int updateAll(String updates) { 109 | return _updateAll(updates); 110 | } 111 | 112 | public int updateAll(Map updates) { 113 | return _updateAll(updates); 114 | } 115 | 116 | public Relation load() { 117 | if (loaded) { 118 | return this; 119 | } else { 120 | records = execQueries(); loaded = true; return this; 121 | } 122 | } 123 | 124 | public Relation reload() { 125 | reset(); load(); return this; 126 | } 127 | 128 | public List execQueries() { 129 | var records = (List) getConnection().selectAll(getArel()); 130 | 131 | if (getValues().isReadonly()) { 132 | records.forEach(r -> r.setReadOnly(true)); return records; 133 | } else { 134 | return records; 135 | } 136 | } 137 | 138 | public final Relation reset() { 139 | toJpql = null; arel = null; loaded = false; records = emptyList(); return this; 140 | } 141 | 142 | public String toJpql() { 143 | return ofNullable(toJpql).orElseGet(() -> toJpql = getConnection().toJpql(getArel())); 144 | } 145 | 146 | @Override 147 | public List getRecords() { 148 | load(); return records; 149 | } 150 | 151 | @Override 152 | public Class getEntityClass() { 153 | return entityClass; 154 | } 155 | 156 | @Override 157 | public Base getRepository() { 158 | return repository; 159 | } 160 | 161 | @Override 162 | public Values getValues() { 163 | return values; 164 | } 165 | 166 | @Override 167 | public boolean isLoaded() { 168 | return loaded; 169 | } 170 | 171 | @Override 172 | public Relation spawn() { 173 | return SpawnMethods.super.spawn(); 174 | } 175 | 176 | @Override 177 | public String getPrimaryKeyAttr() { 178 | return repository.getPrimaryKeyAttr(); 179 | } 180 | 181 | @Override 182 | public String getAlias() { 183 | return repository.getAlias(); 184 | } 185 | 186 | @Override 187 | public JpaAdapter getConnection() { 188 | return repository.getConnection(); 189 | } 190 | 191 | @Override 192 | public String sanitizeJpql(String statement, Object... values) { 193 | return repository.sanitizeJpql(statement, values); 194 | } 195 | 196 | @Override 197 | public Boolean hasLimitOrOffset() { 198 | return values.getLimit() != 0 || values.getOffset() != 0; 199 | } 200 | 201 | @Override 202 | public SelectManager getArel() { 203 | return ofNullable(arel).orElseGet(() -> arel = buildArel()); 204 | } 205 | 206 | // 207 | @Override 208 | public int size() { 209 | if (getValues().getGroup().isEmpty()) { 210 | return loaded ? records.size() : ((Long) count()).intValue(); 211 | } else { 212 | throw new UnsupportedOperationException(); 213 | } 214 | } 215 | 216 | @Override 217 | public boolean isEmpty() { 218 | return loaded ? records.isEmpty() : !exists(); 219 | } 220 | 221 | @Override 222 | public boolean contains(Object o) { 223 | return getRecords().contains(o); 224 | } 225 | 226 | @Override 227 | public boolean containsAll(Collection c) { 228 | return getRecords().containsAll(c); 229 | } 230 | 231 | @Override 232 | public Object[] toArray() { 233 | return getRecords().toArray(); 234 | } 235 | 236 | @Override 237 | public T[] toArray(T[] a) { 238 | return getRecords().toArray(a); 239 | } 240 | 241 | @Override 242 | public T get(int index) { 243 | return getRecords().get(index); 244 | } 245 | 246 | @Override 247 | public int indexOf(Object o) { 248 | return getRecords().indexOf(o); 249 | } 250 | 251 | @Override 252 | public int lastIndexOf(Object o) { 253 | return getRecords().lastIndexOf(o); 254 | } 255 | 256 | @Override 257 | public Iterator iterator() { 258 | return getRecords().iterator(); 259 | } 260 | 261 | @Override 262 | public ListIterator listIterator() { 263 | return getRecords().listIterator(); 264 | } 265 | 266 | @Override 267 | public ListIterator listIterator(int index) { 268 | return getRecords().listIterator(index); 269 | } 270 | 271 | @Override 272 | public List subList(int fromIndex, int toIndex) { 273 | return getRecords().subList(fromIndex, toIndex); 274 | } 275 | 276 | @Override 277 | public T set(int index, T element) { 278 | throw new UnsupportedOperationException(); 279 | } 280 | 281 | @Override 282 | public boolean add(T e) { 283 | throw new UnsupportedOperationException(); 284 | } 285 | 286 | @Override 287 | public void add(int index, T element) { 288 | throw new UnsupportedOperationException(); 289 | } 290 | 291 | @Override 292 | public boolean remove(Object o) { 293 | throw new UnsupportedOperationException(); 294 | } 295 | 296 | @Override 297 | public T remove(int index) { 298 | throw new UnsupportedOperationException(); 299 | } 300 | 301 | @Override 302 | public boolean addAll(Collection c) { 303 | throw new UnsupportedOperationException(); 304 | } 305 | 306 | @Override 307 | public boolean addAll(int index, Collection c) { 308 | throw new UnsupportedOperationException(); 309 | } 310 | 311 | @Override 312 | public boolean removeAll(Collection c) { 313 | throw new UnsupportedOperationException(); 314 | } 315 | 316 | @Override 317 | public boolean retainAll(Collection c) { 318 | throw new UnsupportedOperationException(); 319 | } 320 | 321 | @Override 322 | public void clear() { 323 | throw new UnsupportedOperationException(); 324 | } 325 | // 326 | 327 | // 328 | private SelectManager buildArel() { 329 | var result = new SelectManager(entity); 330 | 331 | result.constructor(values.isConstructor()); 332 | result.distinct(values.isDistinct()); 333 | 334 | buildSelect(result); 335 | buildFrom(result); 336 | 337 | values.getJoins().forEach(result::join); 338 | values.getWhere().forEach(result::where); 339 | values.getGroup().forEach(result::group); 340 | values.getHaving().forEach(result::having); 341 | values.getOrder().forEach(result::order); 342 | 343 | result.limit(values.getLimit()); 344 | result.offset(values.getOffset()); 345 | result.lock(values.getLock()); 346 | result.setHints(hints()); 347 | 348 | return result; 349 | } 350 | 351 | private void buildSelect(SelectManager arel) { 352 | if (values.getSelect().isEmpty()) { 353 | arel.project(entity.getAlias()); 354 | } else { 355 | values.getSelect().forEach(arel::project); 356 | } 357 | } 358 | 359 | private void buildFrom(SelectManager arel) { 360 | if (values.getFrom() != null) arel.from(values.getFrom()); 361 | } 362 | 363 | private void addDefaultHints(HashMap hints) { 364 | hints.put("eclipselink.batch.type", "IN"); 365 | } 366 | 367 | private void addBatchHints(HashMap hints) { 368 | values.getIncludes().forEach(value -> hints.put("eclipselink.batch", value)); 369 | values.getEagerLoad().forEach(value -> hints.put("eclipselink.left-join-fetch", value)); 370 | } 371 | 372 | private HashMap hints() { 373 | var hints = new HashMap(); addDefaultHints(hints); addBatchHints(hints); return hints; 374 | } 375 | 376 | private int _updateAll(Object updates) { 377 | if (isValidRelationForUpdateOrDelete()) { 378 | var stmt = new UpdateManager(); 379 | stmt.entity(entity); 380 | stmt.limit(getArel().getLimit()); 381 | stmt.offset(getArel().getOffset()); 382 | stmt.setWheres(getArel().getConstraints()); 383 | stmt.setOrders(getArel().getOrders()); 384 | 385 | if (updates instanceof Map) { 386 | stmt.set((Map) updates); 387 | } else { 388 | stmt.set((String) updates); 389 | } 390 | 391 | return getConnection().update(stmt); 392 | } else { 393 | throw new ActivePersistenceError("updateAll doesn't support this relation"); 394 | } 395 | } 396 | 397 | private boolean isValidRelationForUpdateOrDelete() { 398 | return values.isDistinct() == false && values.getJoins().isEmpty() && values.getGroup().isEmpty(); 399 | } 400 | 401 | private T destroy(T entity) { 402 | repository.destroy((com.activepersistence.model.Base) entity); return entity; 403 | } 404 | // 405 | 406 | } 407 | --------------------------------------------------------------------------------