Spring MongoDB - update or insert array object in one operation - java

I have a data structure something like this:
{
"_id": "0123456789",
"myArray": [
{
"name": "Steve",
"data": {
"stuff": "datas"
}
]
}
Using Spring MongoDB, I'd like to have a findAndModify operation that either replaces the entire element (a big, complex object) found by the name field, or if it doesn't exist then insert it.
So far I have this:
private UpdateResult update(String id, String name, MyClass obj) {
Query query = new Query();
query.addCriteria(
Criteria.where("_id").is(id).andOperator(
Criteria.where("myArray.name").is(name)));
Update update = new Update();
update.set("myArray.$[]", obj);
return mongoTemplate.upsert(query, update, MyWrapperClass.class);
}
That updates the field, but it won't push a new one if it's not found
Error:
Cannot apply array updates to non-array element
Is there a way to do this in one operation without querying first?

Related

Update / delete multiple objects using Jongo

I have a method which takes in a Collection of Objects that are to be deleted.
This is the way I am deleting them now
public void deleteAll(Collection<Object> objs){
for(Object obj : objs) {
collection.remove("{ _id: # }", obj.getId());
}
}
I am doing something very similar for update where I am looping through the passed collection of Objects. This seems to be very time consuming.
Is there a better way of doing the update/delete?
It's possible to both remove and update multiple documents with a single query.
remove
You need to use a query with a selector using $in, and an array of _id values to match.
With Jongo, you can build the list to match with $in into the query in a couple of different ways
// pass an array of ids
ObjectId[] ids = {id1, id2, id3};
collection.remove("{ _id: { $in: # } }", ids);
// or pass each id separately
collection.remove("{ _id: { $in:[#, #, #] }}", id1, id2, id3);
update
Exact same concept as above using $in to select the objects you want to update, however you also have to set the multi option, so that the update applies to all the documents it matches against, not just the first.
With Jongo this is done like so
ObjectId[] ids = {id1, id2, id3};
collection
.update("{ _id: { $in: # } }", ids)
.multi()
.with({ $set: { foo: "bar" });

How to convert Neo4j JSON to Java Object

I've been playing with Neo4j 2.0 RC1 for a couple of weeks. I'm writing a Spring Security implementation using Neo4j as the database. When I load a user, the response I get from Neo4j looks like this:
{
"columns" : [ "username", "password", "accountNonExpired","accountNonLocked", "credentialsNonExpired", "enabled" ],
"data" : [ [ "admin", "admin", true, false, true, false ]
}
Originally the only fields returned where username and password (both strings) and I was able to do this:
class Result
{
private List<String> columns = new ArrayList<String>();
private List<ArrayList<String>> data = new ArrayList<ArrayList<String>>();
};
ClientRespose resp = webResource.path(path).type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).post(ClientResponse.class, body );
String s = response.getEntity(String.class);
Result r = new Gson().fromJson(s, Result.class);
Of course when I added the other fields (booleans) I needed to change Result to look like this:
class Result
{
private List<String> columns = new ArrayList<String>();
private List<ArrayList<Object>> data = new ArrayList<ArrayList<Object>>();
};
My code still worked, but when I tried to cast any of the data items to String or Boolean I got a 'failed to cast Object to...' exception. This is of course because there is no type information, so GSon is creating Object instances to put everything it.
So I'm guessing there must be a better way to process the JSON that comes back from Neo4j?
Can I somehow skip the JSON conversion stage and get the Jersey HTTP client to populate my User objects directly?
The problem is that the data is not returned as map.
I usually do this:
get the result as Map
get the columns list
get the data nested list
using an for loop over data to get each row
using a for loop over columns and it's index to access the data in the row
You can also add a factory method to your user Object that gets one of these results rows and constructs your User object.
e.g.
class User {
public static User from(List<Object> row) {
User u=new User((String)row.get(0),(String)row.get(1));
u.setAccountNonExpired((Boolean)row.get(2));
u.setAccountNonLocked((Boolean)row.get(3));
u.setCredentialsNonExpired((Boolean)row.get(4));
u.setEnabled((Boolean)row.get(5));
return u;
}
}
Something you can try with Neo4j 2.0 is to use the transactional endpoint and return the node object (which will return just the node-properties as a map), which you then can map directly with Gson or Jackson to an object (the row).
match (u:User {username:{username}})
return u
otherwise you can also use the literal map syntax in Neo4j 2.0
match (u:User {username:{username}})
return {username : u.username, password: u.password, ....} as user

Persisting only unique values in a Mongo collection -- sub arrays

--- Business
--- Sub-Business
--- Organization Group
I have a data structure like this that I want to persist into Mongo.
I do not want to persist duplicate sub-businesses and I don't want to persist duplicate Organization Groups. I'm using the Business as the "_id" so there will not be any duplicates there.
These are two classes, persist is in one class and the other methods are in another. I mean the code, works but its not giving my multiple groups.
I may not understand Mongo/Java too well to get it to do what I want.
Here is what I have
public void persist(Business business) {
DB db = pool.getConnection();
LOG.info("Business toString: " + business.toString()); // REMOVE AFTER
try {
db.requestStart();
DBObject dbObjSetColumns = writeConverter.convert(business);
DBCollection dbCol = db.getCollection(BUSINESS_COLLECTION);
DBObject update = new BasicDBObject("$set",dbObjSetColumns);
DBObject query = new BasicDBObject("_id", business.getName());
LOG.debug(query.toString());
LOG.debug(update.toString());
dbCol.update(query, update, true, false);
} catch (Exception ex) {
throw new RuntimeException(ex);
} finally {
if (db != null) {
db.requestDone();
}
}
}
public DBObject convert(Business business) {
if (business == null) {
return null;
}
final DBObject dbObj = new BasicDBObject();
this.addSubBusinesses(business,dbObj);
return dbObj;
}
private void addSubBusinesses(Business b, DBObject dbObj){
BasicDBObject subBusiness = new BasicDBObject();
for(SubBusiness sub : b.getSubBusinesses()){
subBusiness.put("name", sub.getName());
if(sub.getGroups() != null){
this.addGroupsForSubBusiness(sub, subBusiness);
}
}
dbObj.put("subBusiness", subBusiness);
}
private void addGroupsForSubBusiness(SubBusiness sub, DBObject dbObj){
BasicDBList groups = new BasicDBList();
final DBObject dbGroupObj = new BasicDBObject();
dbGroupObj.put("group", sub.getName());
groups.add(dbGroupObj);
dbObj.put("groups", groups);
}
Here is what I'm receiving. Fortunately it doesn't seem to be duplicating the subBusiness, but I do have multiple groups, that is not showing up here.
/* 0 */
{
"_id" : "ABC",
"subBusiness" : {
"name" : "subBus",
"groups" : [{
"group" : "Ground"
}]
}
}
/* 1 */
{
"_id" : "XYZ",
"subBusiness" : {
"name" : "Sub2",
"groups" : [{
"group" : "Air"
}]
}
}
To put it in perspective on how this works,
We persist one Business Object, that has one Sub-Business and one Group. The database may already know about the Business object, if that is the case then we do not save it twice. The same logic applies for sub business and group. The collection should only show us the unique business information.
Please let me know if there is any more information I can provide. I've been running in circles for a day now trying to get this seemingly easy task done.
Everything except for the addSubBusiness and addGroupForSubBusiness methods were already present, I'm just working on adding this new part, so I am refactoring this to meet my needs.
I'm not familiar with your classes, but it does seem like your addGroupsForSubBusiness method needs to change to something like:
private void addGroupsForSubBusiness(SubBusiness sub, DBObject dbObj){
BasicDBList groups = new BasicDBList();
for (Group group: sub.getGroups()) {
DBObject dbGroupObj = new BasicDBObject();
dbGroupObj.put("group", group.getName());
groups.add(dbGroupObj);
}
dbObj.put("groups", groups);
}
}
Similar change in your subBusiness method since as you currently have it, your method loops through the subBusinesses for your Business, but only adds one object, the last subBusiness, at end.
Will you also persist only once for the Business? Or could you call persist multiple times for a Business, as in first time for a Business "ABC", with a subBusiness "sub1", and then another time for Business "ABC", and subBusiness "sub2", in which case, you want to append to an existing document. If your Business object is complete (as in the object contains all its subBusinesses and Groups when you call persist), then I think the method changes mentioned above should help. Also, you can use the save() method since your determining existence on the _id field. http://docs.mongodb.org/manual/reference/method/db.collection.save/
If you require only one sub-business and one group, then you would be better off restructuring your document to something like this:
{_id:"business_val", sub_business:"sub_business_val", group:"group_val"}
Your code should look like:
new BasicDBObject().append("_id", "business_val").append("sub_business", "sub_business_val").append("group", "group_val");
Additionally, you need to ensure unique index on the collection using:
db.[your_collection].ensureIndex({_id:1,sub_business:1,group:1},{unique:true})

java mongo arraylist save

We are using a Java server and Mongo DB [plain Java-Mongo and not Morphia or some such tool for the CRUD].
We are having a Image Pojo class and its related Metadata like below,
public class Img{
private String name;
private List<Metadata> imgMetaList = new ArrayList<Metadata>();
//Getters, setters etc...
public List<Metadata> getImgMetaList() {
return imgMetaList;
}
}
Metadata class has some data, implementing Serializable didn't work, so
i extended ReflectionDBObject,
public class Metadata extends ReflectionDBObject{
private String tag;
private String val;
//Getters, setters etc...
}
I want to save the Img into Mongo. I used the following code and it worked.
BasicDBObject updateQuery = new BasicDBObject();
updateQuery.put("name", img.getName());
BasicDBObject updMetadata = new BasicDBObject();
updMetadata.put("$push", new BasicDBObject("imgMetaList",img.getImgMetaList()) );
collection.update(updateQuery,updMetadata, true, false);
This inserts a document in Mongo as below,
{
"_id" : ObjectId("503a1991db2e9f431cf0d162"),
"name" : "test.jpg",
"imgMetaList" : [
[
{
"Tag" : "tag1",
"Val" : "val1",
"_id" : null
}
]
]
}
Here there are 2 issues,
1. The code is inserting two square brackets instead of one to house the array
2. why isn't the _id getting generated for the list.
Please let me know.
Regards, Vish
$push appends a value to an array. If the array does not exist yet, a new array is created. The item you are pushing is an array, so you end up with an array inside an array.
An _id is not being generated for the collection because this is normal behaviour for MongoDB. When you put a sub-document in an document, the sub-document does not get an _id. Hence the array does not have an _id.
Perhaps instead you are asking why your metadata document, inside the array, has a null _id. This is a behaviour of the ReflectionDBObject class: it includes an _id field, but it is up to you to set its value. You can set this _id to a non-null value by calling set_id() on your ReflectionDBObject instance. A good place to do this might be in the constructor of your Metadata class. To generate an _id, use the ObjectId.get() static method (org.bson.types.ObjectId).
Your code for inserting the image in MongoDB is more complicated than it needs to be. The following will work (and will get rid of the nested array):
BasicDBObject imageDoc = new BasicDBObject();
imageDoc("name", img.getName());
imageDoc("imgMetaList", img.getImgMetaList());
collection.insert(imageDoc);

Retrieve sub-document in array as DBObject(s)

I'm very new to MongoDB, and I'm using it along with the Java driver. I have this document structure:
{ "_id" : ObjectId("4f7d2ba6fd5a306d82687d48"), "room" : "Den" }
{ "_id" : ObjectId("4f7d2baafd5a306d82687d49"), "room" : "Foyer" }
{ "_id" : ObjectId("4f7d2fdcfd5a306d82687d4a"), "room" : "Master Bedroom" }
{ "_id" : ObjectId("4f7d301afd5a306d82687d4b"), "room" : "Guest Bedroom" }
{ "_id" : ObjectId("4f7d2b98fd5a306d82687d47"), "code" : "A", "lights" : [ { "name" : "Overhead", "code" : "1" } ], "room" : "Kitchen" }
Where the last line is of particular interest in illustrating what I want to do. Each document is a room and may have a "lights" key corresponding to a value that is an array of sub-documents. From a modeling perspective, I have a house, which has 0-n rooms, each of which has 0-n lights in it. What I want to do in Java is take the name of the room as a parameter, and return a collection of DBObject corresponding to the sub-documents in the lights array -- "get me all lights for room 'kitchen'", for example.
So far, proceeding incrementally in TDD style, I've constructed this query:
public static final String ROOM_KEY = "room";
public static final String EQUALS_KEY = "$eq";
private BasicDBObject buildRoomNameQuery(String roomName) {
BasicDBObject myQuery = new BasicDBObject();
myQuery.put(ROOM_KEY, new BasicDBObject(EQUALS_KEY, roomName));
return myQuery;
}
I realize that this is going to get me the entire room document for the room name I pass in. I'm a bit stuck on what the best way to proceed from here is to get what I want. Is what I'm doing even possible with a simple query, or will I have to retrieve the array and iterate through it in code, casting the elements as DBObject? I'm also open to suggestions for a better document structure for my purpose -- I'm not married to this structure by any means.
For a bit of perspective, I'm quite well versed in SQL and traditional relational databases, if that helps in terms of explanatory analogies. Also, if I'm butchering the MongoDB terminology, please correct me. Thanks in advance.
So, you can do something like this:
DBCollection coll = db.getCollection("test");
BasicDBObject query = new BasicDBObject("room", "Kitchen");
// optional, limit the fields to only have the lights field
BasicDBObject fields = new BasicDBObject("lights",1).append("_id",false);
DBCursor curs = coll.find(query, fields);
while(curs.hasNext()) {
DBObject o = curs.next();
// shows the whole result document
System.out.println(o.toString());
BasicDBList lights = (BasicDBList) o.get("lights");
// shows the lights array -- this is actually a collection of DBObjects
System.out.println(lights.toString());
// optional: break it into a native java array
BasicDBObject[] lightArr = lights.toArray(new BasicDBObject[0]);
for(BasicDBObject dbObj : lightArr) {
// shows each item from the lights array
System.out.println(dbObj);
}
}
Also, I recommend using the QueryBuilder in the Java driver--it's a bit more concise than creating Queries from DBObjects. Even better, check out Morphia, which is an object mapper that uses the Java driver. It natively supports entity models that have lists in them, and serializes/deserializes them to Mongo without needing to deal with the DBObject stuff.
Look at spring mongo package. A really good way to work with mongo using POJO documents
http://www.springsource.org/spring-data/mongodb
You will not need to perform casting and work with strings
You can use an iterator for the fields
Iterator<DBObject> fields = curs.iterator();
while(fields.hasNext()){
DBObject field = (DBObject) fields.next().get("lights");
System.out.println(field.get("name"));
}
For newer versions, consider the use of the Document. To avoid unchecked casts and linter warnings, along with writing your own loop, use the libary's get(final Object key, final Class<T> clazz) method:
List<Document> comments = posts.get("comments", docClazz)
where docClazz is something that you create once:
final static Class<? extends List> docClazz = new ArrayList<Document().getClass();

Categories