I got a graph which is described by the following Cypher expression:
CREATE
(BMW:Brand {name: "BMW", country: "Germany"}),
(X3:Model {name: "X3", acceleration: 7.1, maxSpeed: 227.5, displacement: 1997, consumption: 6}),
(lastGen:Generation {from: 2013}),
(xDrive20i:Modification {name: "xDrive20i", maxSpeed: 210, acceleration: 8.3, consumption: 7.9}),
(X3)-[:MODEL_OF]->(BMW),
(BMW)-[:MODEL]->(X3),
(lastGen)-[:GENERATION_OF]->(X3),
(X3)-[:GENERATION]->(lastGen),
(xDrive20i)-[:MODIFICATION_OF]->(X3),
(X3)-[:MODIFICATION]->(xDrive20i),
(lastGen)-[:MODIFICATION]->(xDrive20i),
(xDrive20i)-[:MODIFICATION_OF]->(lastGen);
I described a java class matching to Brand's data structure:
#NodeEntity
#TypeAlias("Brand")
public class Brand {
#GraphId
private Long id;
#Indexed(indexType = IndexType.FULLTEXT, indexName = "brand_name")
private String name;
private String origin;
private String owner;
#RelatedTo(type = "MODEL", direction = Direction.OUTGOING)
private Set<Model> models;
//getters and setters are ommited
}
and repository:
public interface BrandRepository extends GraphRepository<Brand>{
//method's signatures are ommited
}
When I call brandRepository.count() it returns 1 as I expect. But if I call brandRepository.getOne(2249L) I get an exception:
java.lang.IllegalStateException: No primary SDN label exists .. (i.e one with starting with __TYPE__)
As I understand reading LabelBasedNodeTypeRepresentationStrategy source, a node has to have at least one label with __TYPE__ prefix.
How do I map the entity to the graph given that I may not change the graph structure?
I wouldn't mind implementing my own custom LabelBasedNodeTypeRepresentationStrategy if there is no other way. But in this case could somebody let me know why it is implemented this way (I think it is not accidentally) and how should I bind custom solution to spring-data-neo4j use it?
I use neo4j-2.0.0-M06 and spring-data-neo4j-3.0.0.M1.
SDN adds additional metadata to your graph when you store entities, that metadata is missing in your case.
You can try to add that metadata yourself by calling
neo4jTemplate.postEntityCreation(node, Brand.class);
but that for instance doesn't index your name field (manual legacy index).
Related
I'm trying to identify the best way to do mongodb object versioning.
Based on the mongodb document versioning pattern, storing revisions in a history collection and having current version in main collection is recommended. According to that, each revision contains the complete object instead of storing diffs.
Then I went through ways to implement data versioning in mongoDB where it recommends a method to store a single document containing all the revisions inside it having a separate history collection.
Therefore, I'm trying to implement my own object versioning implementation for the following document model due its complexity.
Invoice.java
public class Invoice {
private long invoiceId;
private String userName;
private String userId;
private LocalDateTime createdDate;
private LocalDateTime lastModifiedDate;
private List<String> operationalUnits;
private List<BodyModel> body;
private List<ReviewModel> reviews;
private BigDecimal changedPrice;
private String submitterId;
private LocalDateTime submittedTime;
private String approverId;
private LocalDateTime approvedTime;
}
BodyModel.java
public class BodyModel {
private String packageId;
private List<ItemModel> items;
private List<String> reviews;
}
ReviewModel.java
public class ReviewModel {
private String publishedTime;
private String authorName;
private String authorId;
private String text;
}
ItemModel.java
public class ItemModel {
private String itemNumber;
private String description;
private String brandId;
private String packId;
private List<String> reviews;
}
ER Diagram (Simplified)
At the moment, I'm using Javers library. But, Javers keeps the Invoice model as the main entity and other models such as BodyModel, ReviewModel, ItemModel as separated valueObjects. As a result, instead of creating a single revision document, it creates separated documents for the valueObjects. Additionally, it always constructs the current objects from the base version plus all changes which leads to huge read time. Addtionally, I identified a valueObjects issue that comes with javers. Refer this question for more info: MongoDB document version update issue with JaVers
Following are the issues, I've if I'm going to create my own implementation using spring boot.
If I'm going to put revisionId in each of the revisions (as shown in the below object) when mongoDB save(), how do I find the current revisionId to be included ?
{
_Id: <ObjectId>,
invoiceId: <InvoiceId>,
invoice: <Invoice>,
revisionId: <revisionId>
}
For this I can keep a field for revisionId in InvoiceModel which can be updated when saving to the main collection. And at the same time, it can be used to save the revision into history collection. Is it the best possible way to do it in this case ?
If I'm only going to store diffs, then how do I obtain the current version of the object ?
For this, it feels essential to fetch the current object from the main collection and then compare it with new version (I already have the new version, because that's what I'm going to store in main collection) to create the diff. Then diff can be stored in history collection and new version in main collection. Is it the best possible way to store diffs ?
In both scnearios, aspects coming from AOP can be used to intercept the save() method of the base repository to accomplish the task. But I don't mainly consider about coding implementation details, I would like to know which method or methods would be efficient in storing revisions for a data model such as given above (would like to discuss about methods I've mentioned as well) ?
Now, I have the next entity. This one is the m1 table of my database.
#Entity(name = "m1")
#Data
public class Information {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
private String date;
private Double weight_1;
private Double weight_2;
private Double weight_3;
private Double weight_4;
private int working;
}
So, when I do some call to the APIRest it returns me the information corresponding to the m1 table. The controller that I have is the next (simple controller that returns all the information):
#Controller
#RequestMapping(path = "/information")
public class InformationController {
#Autowired
private InformationRepository repository;
#GetMapping(path="/all")
public #ResponseBody List<Information> getAllInformations() {
// This returns a JSON or XML with the users
return repository.findAll();
}
}
The question is: There is any way to change the name of the m1 on runtime. For example can I put the name of the table in the call path and in the API Rest take it?
Maybe this is impossible and I am doing it the bad way I do not know.
EDIT: I mean, can I change the table that the API Rest is taking the data by putting the table that I want in the url/path that I call. For example: in my case the default table/entity that the APIRest take the data is m1, so can I call http://localhost:8080/information/especifictable/all/ where especific table is the table that I want the recieve the data of the database and in the API Rest take that url parameter and change the default m1 with the especifictable.
I do not know if I have explained it well, I do not know how to explain it well.
Such a design would only make sense, if there are two tables in DB, which look the same. if that is the case there is something wrong with your DB design.
Basically it is not possible, to the best of my knowledge.
I'm using elastic search for storing the data (spring data elasticsearch ) and I, need to store geolocation in my document. The class structure is in the following format.
#Document(indexName = "outlet")
public class OutletIndex implements IESMapper {
#Id
private String path;
private String name;
#GeoPointField
private GeoPoint geoPoint;
// setters and getters
}
Since there are no setters for class GeoPoint it does not work with #ModelAttribute annotation in spring MVC controller. That I need to get it from the front so I'd updated it to:
#Document(indexName = "outlet")
public class OutletIndex implements IESMapper {
#Id
private String path;
private String name;
#GeoPointField
private GeoPoint geoPoint;
private String geoLocation;
public void setGeoLocation(String geoLocation) {
this.geoLocation = geoLocation;
if (geoLocation != null && geoLocation.trim() != "") {
String[] loc = geoLocation.split(",");
this.geoPoint = new GeoPoint(Double.parseDouble(loc[0]), Double.parseDouble(loc[1]));
}
}
// setters and getters
}
An additional field which holds it's string representation and within the Setter which also updates the GeoPiont.
Is there any better approach to do this?
EDIT: One more doubt, is there any way to use the string as geopoint(comma separated value)?
It seems like you are using geo_point data type with data in Elasticsearch of the format location:"latVal,lonVal". This is allowed with Elasticsearch as one of the valid formats for geo_point.
Elasticsearch just provides the data stored in the format you've given. For the same geo_point type in your ES schema, you can store it in multiple formats in different documents and when you try to get them ES will just return them in the format you've stored.
This thing causes issues, as if you have different formats for the same type to be handled specially for a type safe language like Java. You can either do 2 things: ensure a consistent type throughout (both while indexing and retrieving), handle each corner case on application side.
To avoid all this mess, a good rule of thumb I follow is to use the same format as the one provided by the Java client. Here in this case I would not use any custom de-serialization and serialization logic. Instead it would be better to save the location in the format location:{"lat": latVal, "lon": lonVal}. (GeoPoint class expects a double lat and a double lon)
If you ensure this, you will no longer need to think multiple times over the types you'll be receiving and their corner cases while handling them and at the same time avoid a lot of confusion.
I do have a Repository
#Repository
public interface PointOfInterestRepository extends GraphRepository<Poi> {
// currently empty
}
with no custom methods defined. So I use the like of save(T... entities) which are predefined.
And I have my Poi class as follows
#NodeEntity(label = "PointOfInterest")
public class Poi {
#JsonIgnore
#GraphId
Long neo4jId;
#JsonManagedReference("node-poi")
#JsonProperty("node")
#Relationship(type = "BELONGS_TO", direction = Relationship.UNDIRECTED)
private Node node;
#JsonProperty("id")
#Property(name = "poiID")
private final String id;
#JsonProperty("uris")
#Property(name = "uris")
private final Set<URI> correspondingURIs = new HashSet<>();
/* Some more stuff I skip here*/
}
with getters for the fields.
Currently I am able to save such Pois to neo4j and retrieve them back, but when I try to work with those Nodes in the database via cypher it appears that the fields aren't mapped to neo4j properties.
I thought spring-data-neo4j would convert my class fields to neo4j graph properties. Am I wrong with that?
Note: The save calls seems to work very well. After that I can see the Nodes in the database and calling findAll() afterwards will return me all the saved Nodes (Pois) properly with all the correct values. But somehow, within the database, I cannot see any properties/fields.
The problem is the final fields. SDN would be unable to write values back to the entity when loaded from the graph because these fields are final (and SDN will use only the default no-args constructor), and as such, final fields are not supported.
Removing the final should fix this.
I have a question on using spring cassandra repository for searching query. I have a table with one partition key and one clustering key together as composite primary key. So in the mapping layer, I create a class for PrimanyKey using #PrimaryKeyClass and #PrimaryKeyColumn, then use #PrimaryKey for the mapping class
the table is created with the following cql
CREATE TABLE user_mobile (phonenum text,memberid uuid,gender text,nickname text,photo timeuuid,PRIMARY KEY (phonenum, memberid))
I create a class called PK_UserMobile to act as primary key class
#PrimaryKeyClass
public class PK_UserMobile implements Serializable {
#PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED)
private String phoneNum;
#PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.CLUSTERED)
private UUID memberId;
......
}
and my mapping class is something like this
#Table("user_mobile")
public class UserMobile extends BaseDto {
#PrimaryKey
private PK_UserMobile pk;
........
}
and it is fine to run cql like "select * from user_mobile where phonenum = 'xxxxxxxxx' ;". So I guess it is fine for search this table with partition key only. When I try to search using spring cassandra repository, seems it require me to provide both phonenum's and memberid's value for searching usage. the following exception is thrown if I only set up the value for phonenum.
"com.datastax.driver.core.exceptions.InvalidQueryException: Invalid null value in condition for column memberid. "
So I wanna know if any special null object should I pass in the clustering key as a searching condition? or should I do it in any other way? Thank you.
This is probably because of that you use the standard CRUD repository function findById. To solve the problem, declare a query method on your repository with the following pattern:
findBy{key class name}{property of the key class}
in your case your query method is:
List<UserMobile> findByPK_UserMobilePhoneNum(String phoneNum);
You can use the following way to accomplish this.
No need for a separate PrimaryKeyClass
#Table("user_mobile")
public class UserMobile extends BaseDto {
#PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED)
private String phoneNum;
#PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.CLUSTERED)
private UUID memberId;
........
}
Now, define the repository using MapIdCassandraRepository
#Repository
public interface UserMobileRepository extends MapIdCassandraRepository<UserMobile> {
#Query("SELECT * from user_mobile where phoneNum=?0")
List<UserMobile> findByPhoneNum(String phoneNum);
}
This will work!