justatester
justatester

Reputation: 391

datastore - using queries and transactions

I'm working with objectify.

In my app, i have Employee entities and Task entities. Every task is created for a given employee. Datastore is generating id for Task entities.

But i have a restriction, i can't save a task for an employee if that task overlaps with another already existing task.

I don't know how to implement that. I don't even know how to start. Should i use a transaction ?

@Entity
class Task {

    @Id
    private Long id;

    @Index
    private long startDate; // since epoch

    @Index
    private long endDate;

    @Index
    private Ref<User> createdFor;

    public Task(String id, long startDate, long endDate, User assignedTo) {

        this.id = null;
        this.startDate = startDate;
        this.endDate = endDate;
        this.createdFor = Ref.create(assignedTo);

    }

}

@Entity
class Employee {

    @Id
    private String id;

    private String name;

    public Employee(String id, String name) {

        this.id = id;
        this.name = name;

    }

}

Upvotes: 1

Views: 115

Answers (2)

Jibby
Jibby

Reputation: 1010

Ok, I hope I can be a bit more helpful. I've attempted to edit your question and change your entities to the right architecture. I've added an embedded collection of tasks and an attemptAdd method to your Employee. I've added a detectOverlap method to both your Task and your Employee. With these in place, your could use something like the transaction below. You will need to deal with the cases where you task doesn't get added because there's a conflicting task, and also the case where the add fails due to a ConcurrentModificationException. But you could make another question out of those, and you should have the start you need in the meantime.

Adapted from: https://code.google.com/p/objectify-appengine/wiki/Transactions

Task myTask = new Task(startDate,endDate,description);

public boolean assignTaskToEmployee(EmployeeId, myTask) {
    ofy().transact(new VoidWork() {
        public void vrun() {            
            Employee assignee = ofy().load().key(EmployeeId).now());
            boolean taskAdded = assignee.attemptAdd(myTask);
            ofy().save().entity(assignee);
            return taskAdded;
        }
    }
}

Upvotes: 0

Jibby
Jibby

Reputation: 1010

You can't do it with the entities you've set up, because between the time you queried for tasks, and inserted the new task, you can't guarantee that someone wouldn't have already inserted a new task that conflicts with the one you're inserting. Even if you use a transaction, any concurrently added, conflicting tasks won't be part of your transaction's entity group, so there's the potential for violating your constraint.

Can you modify your architecture so instead of each task having a ref to the employee its created for, every Employee contains a collection of tasks created for that Employee? That way when your querying the Employee's tasks for conflicts, the Employee would be timestamped in your transaction's Entity Group, and if someone else put a new task into it before you finished putting your new task, a concurrent modification exception would be thrown and you would then retry. But yes, have both your query and your put in the same Transaction.

Read here about Transactions, Entity Groups and Optimistic Concurrency: https://code.google.com/p/objectify-appengine/wiki/Concepts#Transactions

As far as ensuring your tasks don't overlap, you just need to check whether either of your new task's start of end date is within the date range of any previous Tasks for the same employee. You also need to check that your not setting a new task that starts before and ends after a previous task's date range. I suggest using a composite.and filter for for each of the tests, and then combining those three composite filters in a composite.or filter, which will be the one you finally apply. There may be a more succint way, but this is how I figure it:

Note these filters would not apply in the new architecture I'm suggesting. Maybe I'll delete them. ////////Limit to tasks assigned to the same employee Filter sameEmployeeTask = new FilterPredicate("createdFor", FilterOperator.EQUAL, thisCreatedFor);

/////////Check if new startDate is in range of the prior task
Filter newTaskStartBeforePriorTaskEnd =
  new FilterPredicate("endDate", FilterOperator.GREATER_THAN, thisStartDate);

Filter newTaskStartAfterPriorTaskStart =
  new FilterPredicate("startDate", FilterOperator.LESS_THAN, thisStartDate);

Filter newTaskStartInPriorTaskRange =
  CompositeFilterOperator.and(sameEmployeeTask, newTaskStartBeforePriorTaskEnd, newTaskStartAfterPriorTaskStart);

/////////Check if new endDate is in range of the prior task
Filter newTaskEndBeforePriorTaskEnd =
  new FilterPredicate("endDate", FilterOperator.GREATER_THAN, thisEndDate);

Filter newTaskEndAfterPriorTaskStart =
  new FilterPredicate("startDate", FilterOperator.LESS_THAN, thisEndDate);

Filter newTaskEndInPriorTaskRange =
  CompositeFilterOperator.and(sameEmployeeTask, newTaskEndBeforePriorTaskEnd, newTaskEndAfterPriorTaskStart);

/////////Check if this Task overlaps the prior one on both sides
Filter newTaskStartBeforePriorTaskStart =
  new FilterPredicate("startDate", FilterOperator.GREATER_THAN_OR_EQUAL, thisStartDate);

Filter newTaskEndAfterPriorTaskEnd =
  new FilterPredicate("endDate", FilterOperator.LESS_THAN_OR_EQUAL, thisEndDate);

Filter PriorTaskRangeWithinNewTaskStartEnd = CompositeFilterOperator.and(sameEmployeeTask ,newTaskStartBeforePriorTaskStart, newTaskEndAfterPriorTaskEnd);

/////////Combine them to test if any of the three returns one or more tasks
Filter newTaskOverlapPriorTask =    CompositeFilterOperator.or(newTaskStartInPriorTaskRange,newTaskEndInPriorTaskRange,PriorTaskRangeWithinNewTaskStartEnd);

/////////Proceed
Query q = new Query("Task").setFilter(newTaskOverlapPriorTask);  

PreparedQuery pq = datastore.prepare(q);

If you don't return any results, then you don't have any overlaps, so go ahead and save the new task.

Upvotes: 2

Related Questions