Skip to content

Commit

Permalink
Cayenne entity ID part represented by a DB column to be prefixed wit…
Browse files Browse the repository at this point in the history
…h "db:" #521

.. in progress..
  • Loading branch information
andrus committed Jan 2, 2022
1 parent 00726e6 commit 586a819
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class EntityPathCache {
// TODO: here we are ignoring the name of the ID attribute and are using the fixed name instead.
// Same issue as the above
AgIdPart id = entity.getIdParts().iterator().next();
pathCache.put(PathConstants.ID_PK_ATTRIBUTE, new PathDescriptor(id.getType(), PathDescriptor.parsePath(id.getName()), true));
pathCache.put(PathConstants.ID_PK_ATTRIBUTE, new PathDescriptor(id.getType(), PathOps.parsePath(id.getName()), true));
}
}

Expand All @@ -49,7 +49,7 @@ private PathDescriptor computePathDescriptor(String agPath) {

if (last instanceof AgIdPart) {
AgIdPart id = (AgIdPart) last;
return new PathDescriptor(id.getType(), PathDescriptor.parsePath(id.getName()), true);
return new PathDescriptor(id.getType(), PathOps.parsePath(id.getName()), true);
}

AgRelationship relationship = (AgRelationship) last;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package io.agrest.cayenne.path;

import org.apache.cayenne.exp.parser.ASTDbPath;
import org.apache.cayenne.exp.parser.ASTObjPath;
import org.apache.cayenne.exp.parser.ASTPath;

/**
Expand All @@ -13,12 +11,6 @@ public class PathDescriptor {
private final ASTPath path;
private final Class<?> type;

public static ASTPath parsePath(String path) {
return path.startsWith(ASTDbPath.DB_PREFIX)
? new ASTDbPath(path.substring(ASTDbPath.DB_PREFIX.length()))
: new ASTObjPath(path);
}

public PathDescriptor(Class<?> type, ASTPath path, boolean attributeOrId) {
this.path = path;
this.type = type;
Expand Down
100 changes: 100 additions & 0 deletions agrest-cayenne/src/main/java/io/agrest/cayenne/path/PathOps.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package io.agrest.cayenne.path;

import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.parser.ASTDbPath;
import org.apache.cayenne.exp.parser.ASTObjPath;
import org.apache.cayenne.exp.parser.ASTPath;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.map.ObjRelationship;
import org.apache.cayenne.util.CayenneMapEntry;

import java.util.Iterator;

/**
* @since 5.0
*/
public class PathOps {

/**
* @since 5.0
*/
public static ASTPath parsePath(String path) {
return path.startsWith(ASTDbPath.DB_PREFIX)
? new ASTDbPath(path.substring(ASTDbPath.DB_PREFIX.length()))
: new ASTObjPath(path);
}


// TODO: any chance of caching resolved concat paths that is faster than rebuilding them from scratch?

/**
* @since 5.0
*/
public static ASTPath concat(ObjEntity entity, ASTPath p1, ASTPath p2) {

switch (p1.getType()) {
case Expression.DB_PATH:
return concatWithDbPath(entity, (ASTDbPath) p1, p2);
case Expression.OBJ_PATH:
return concatWithObjPath(entity, (ASTObjPath) p1, p2);
default:
throw new IllegalArgumentException("Unexpected p1 type: " + p1.getType());
}
}

/**
* @since 5.0
*/
public static ASTPath concatWithDbPath(ObjEntity entity, ASTDbPath p1, ASTPath p2) {

switch (p2.getType()) {
case Expression.DB_PATH:
return new ASTDbPath(p1.getPath() + "." + p2.getPath());
case Expression.OBJ_PATH:
ASTDbPath p2DB = resolveAsDbPath(entity, (ASTObjPath) p2);
return new ASTDbPath(p1.getPath() + "." + p2DB.getPath());
default:
throw new IllegalArgumentException("Unexpected p2 type: " + p2.getType());
}
}

/**
* @since 5.0
*/
public static ASTPath concatWithObjPath(ObjEntity entity, ASTObjPath p1, ASTPath p2) {

switch (p2.getType()) {
case Expression.DB_PATH:
return concatWithDbPath(entity, resolveAsDbPath(entity, p1), p2);
case Expression.OBJ_PATH:
return new ASTObjPath(p1.getPath() + "." + p2.getPath());
default:
throw new IllegalArgumentException("Unexpected p2 type: " + p2.getType());
}
}

private static ASTDbPath resolveAsDbPath(ObjEntity entity, ASTObjPath objPath) {

StringBuilder buffer = new StringBuilder();
Iterator<CayenneMapEntry> it = entity.resolvePathComponents(objPath);
while (it.hasNext()) {

CayenneMapEntry e = it.next();

if (buffer.length() > 0) {
buffer.append('.');
}

if (it.hasNext() || e instanceof ObjRelationship) {
ObjRelationship r = (ObjRelationship) e;
buffer.append(r.getDbRelationshipPath());
} else {
ObjAttribute a = (ObjAttribute) e;
buffer.append(a.getDbAttributePath());
}
}

return new ASTDbPath(buffer.toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,16 @@
import java.util.concurrent.ConcurrentHashMap;

/**
* A caching {@link IPathResolver} implementation. Each app only has a fixed number of paths, so this makes
* {@link PathDescriptor} lookup efficient.
* A caching {@link IPathResolver} implementation.
*
* @since 5.0
*/
public class PathResolver implements IPathResolver {

private Map<String, EntityPathCache> pathCacheByEntity;
private final Map<String, EntityPathCache> pathCaches;

public PathResolver() {
this.pathCacheByEntity = new ConcurrentHashMap<>();
this.pathCaches = new ConcurrentHashMap<>();
}

@Override
Expand All @@ -25,7 +24,9 @@ public PathDescriptor resolve(AgEntity<?> entity, String agPath) {
}

EntityPathCache entityPathCache(AgEntity<?> entity) {
return pathCacheByEntity.computeIfAbsent(entity.getName(), k -> new EntityPathCache(entity));
return pathCaches.computeIfAbsent(
entity.getName(),
k -> new EntityPathCache(entity));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@
import io.agrest.base.protocol.Dir;
import io.agrest.base.protocol.Sort;
import io.agrest.cayenne.path.IPathResolver;
import io.agrest.cayenne.path.PathOps;
import io.agrest.cayenne.persister.ICayennePersister;
import io.agrest.cayenne.qualifier.IQualifierParser;
import io.agrest.cayenne.qualifier.IQualifierPostProcessor;
import io.agrest.meta.AgEntity;
import io.agrest.meta.AgIdPart;
import io.agrest.runtime.processor.select.SelectContext;
import org.apache.cayenne.dba.TypesMapping;
import org.apache.cayenne.di.Inject;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.exp.parser.ASTDbPath;
import org.apache.cayenne.exp.parser.ASTPath;
import org.apache.cayenne.exp.property.Property;
import org.apache.cayenne.exp.property.PropertyFactory;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.EntityResolver;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.map.ObjRelationship;
Expand All @@ -35,8 +35,6 @@
import java.util.List;
import java.util.function.Consumer;

import static io.agrest.base.reflect.Types.typeForName;

/**
* @since 3.4
*/
Expand Down Expand Up @@ -76,26 +74,7 @@ public <T> SelectQuery<T> createRootQuery(SelectContext<T> context) {
public <T> SelectQuery<T> createQueryWithParentQualifier(NestedResourceEntity<T> entity) {

SelectQuery<T> query = createBaseQuery(entity);

// Use Cayenne metadata for query building. Agrest metadata may be missing some important parts like ids
// (e.g. see https://github.com/agrestio/agrest/issues/473)

ObjEntity parentObjEntity = entityResolver.getObjEntity(entity.getParent().getName());
ObjRelationship objRelationship = parentObjEntity.getRelationship(entity.getIncoming().getName());
String reversePath = objRelationship.getReverseDbRelationshipPath();

List<Property<?>> properties = new ArrayList<>();
Class entityType = entity.getType();
properties.add(PropertyFactory.createSelf(entityType));

for (DbAttribute pk : parentObjEntity.getDbEntity().getPrimaryKeys()) {
String path = reversePath + "." + pk.getName();
Expression propertyExp = ExpressionFactory.dbPathExp(path);
Class<?> pkType = typeForName(TypesMapping.getJavaBySqlType(pk.getType()));
properties.add(PropertyFactory.createBase(propertyExp, pkType));
}

query.setColumns(properties);
query.setColumns(queryColumns(entity));

// Translate expression from parent.
// Find the closest parent in the chain that has a query of its own, and use that as a base.
Expand Down Expand Up @@ -167,32 +146,18 @@ private String concatWithParentDbPath(ObjRelationship incoming, String outgoingD
public <T, P> SelectQuery<T> createQueryWithParentIdsQualifier(NestedResourceEntity<T> entity, Iterator<P> parentData) {

SelectQuery<T> query = createBaseQuery(entity);

// Use Cayenne metadata for query building. Agrest metadata may be missing some important parts like ids
// (e.g. see https://github.com/agrestio/agrest/issues/473)

ObjEntity parentObjEntity = entityResolver.getObjEntity(entity.getParent().getName());
ObjRelationship objRelationship = parentObjEntity.getRelationship(entity.getIncoming().getName());
String reversePath = objRelationship.getReverseDbRelationshipPath();

List<Property<?>> properties = new ArrayList<>();
Class entityType = entity.getType();
properties.add(PropertyFactory.createSelf(entityType));

for (DbAttribute pk : parentObjEntity.getDbEntity().getPrimaryKeys()) {
String path = reversePath + "." + pk.getName();
Expression propertyExp = ExpressionFactory.dbPathExp(path);
Class<?> pkType = typeForName(TypesMapping.getJavaBySqlType(pk.getType()));
properties.add(PropertyFactory.createBase(propertyExp, pkType));
}

query.setColumns(properties);
query.setColumns(queryColumns(entity));

// build id-based qualifier
List<Expression> qualifiers = new ArrayList<>();

// if pagination is in effect, we should only fault the requested range. It makes this particular strategy
// very efficient in case of pagination

ObjEntity parentObjEntity = entityResolver.getObjEntity(entity.getParent().getName());
ObjRelationship objRelationship = parentObjEntity.getRelationship(entity.getIncoming().getName());
String reversePath = objRelationship.getReverseDbRelationshipPath();

consumeRange(parentData, entity.getParent().getFetchOffset(), entity.getParent().getFetchLimit(),
// TODO: this only works for single column ids
p -> qualifiers.add(ExpressionFactory.matchDbExp(reversePath, p)));
Expand Down Expand Up @@ -290,4 +255,29 @@ private SortOrder toSortOrder(Dir direction) {
throw new IllegalArgumentException("Missing or unexpected sort direction: " + direction);
}
}

private List<Property<?>> queryColumns(NestedResourceEntity<?> entity) {

// Use Cayenne metadata for query building. Agrest metadata may be missing some important parts like ids
// (e.g. see https://github.com/agrestio/agrest/issues/473)

AgEntity<?> parentEntity = entity.getParent().getAgEntity();
ObjEntity parentObjEntity = entityResolver.getObjEntity(entity.getParent().getName());
ObjRelationship objRelationship = parentObjEntity.getRelationship(entity.getIncoming().getName());
ASTDbPath reversePath = new ASTDbPath(objRelationship.getReverseDbRelationshipPath());

List<Property<?>> columns = new ArrayList<>(parentEntity.getIdParts().size() + 1);

Class entityType = entity.getType();
columns.add(PropertyFactory.createSelf(entityType));

// columns must be added in the order of id parts iteration, as this is how they will be read from result
for (AgIdPart idPart : parentEntity.getIdParts()) {
ASTPath idPartPath = pathResolver.resolve(parentEntity, idPart.getName()).getPathExp();
Expression propertyExp = PathOps.concatWithDbPath(parentObjEntity, reversePath, idPartPath);
columns.add(PropertyFactory.createBase(propertyExp, idPart.getType()));
}

return columns;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import io.agrest.base.protocol.exp.NamedParamsExp;
import io.agrest.base.protocol.exp.PositionalParamsExp;
import io.agrest.base.protocol.exp.SimpleExp;
import io.agrest.cayenne.path.PathDescriptor;
import io.agrest.cayenne.path.PathOps;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.exp.parser.ASTPath;
Expand Down Expand Up @@ -84,7 +84,7 @@ public void visitCompositeExp(CompositeExp exp) {

static Expression parseKeyValueExpression(String key, String op, Object value) {

ASTPath path = PathDescriptor.parsePath(key);
ASTPath path = PathOps.parsePath(key);

switch (op) {
case "=":
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.agrest.cayenne.path;

import io.agrest.cayenne.cayenne.main.E2;
import io.agrest.cayenne.unit.CayenneNoDbTest;
import org.apache.cayenne.exp.parser.ASTDbPath;
import org.apache.cayenne.exp.parser.ASTObjPath;
import org.apache.cayenne.exp.parser.ASTPath;
import org.apache.cayenne.map.ObjEntity;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class PathOpsTest extends CayenneNoDbTest {

@Test
public void testConcat_ObjObj() {

ObjEntity e2 = getEntity(E2.class);
ASTPath p = PathOps.concat(e2, new ASTObjPath("e3s"), new ASTObjPath("name"));
assertEquals(new ASTObjPath("e3s.name"), p);
}

@Test
public void testConcat_ObjDb() {
ObjEntity e2 = getEntity(E2.class);
ASTPath p = PathOps.concat(e2, new ASTObjPath("e3s"), new ASTDbPath("name"));
assertEquals(new ASTDbPath("e3s.name"), p);
}

@Test
public void testConcat_DbDb() {
ObjEntity e2 = getEntity(E2.class);
ASTPath p = PathOps.concat(e2, new ASTDbPath("e3s"), new ASTDbPath("name"));
assertEquals(new ASTDbPath("e3s.name"), p);
}

@Test
public void testConcat_DbObj() {
ObjEntity e2 = getEntity(E2.class);
ASTPath p = PathOps.concat(e2, new ASTDbPath("e3s"), new ASTObjPath("name"));
assertEquals(new ASTDbPath("e3s.name"), p);
}

@Test
public void testConcat_ObjDb_MultiStep() {
ObjEntity e2 = getEntity(E2.class);
ASTPath p = PathOps.concat(e2, new ASTObjPath("e3s.e5"), new ASTDbPath("date"));
assertEquals(new ASTDbPath("e3s.e5.date"), p);
}

@Test
public void testConcat_ObjDb_Id() {
ObjEntity e2 = getEntity(E2.class);
ASTPath p = PathOps.concat(e2, new ASTObjPath("e3s"), new ASTDbPath("_id"));
assertEquals(new ASTDbPath("e3s._id"), p);
}

@Test
public void testConcat_ObjDb_EndsInRel() {
ObjEntity e2 = getEntity(E2.class);
ASTPath p = PathOps.concat(e2, new ASTObjPath("e3s"), new ASTDbPath("e5"));
assertEquals(new ASTDbPath("e3s.e5"), p);
}
}

0 comments on commit 586a819

Please sign in to comment.