The Photofeed sample provides a useful technique in the general data model abstraction using interfaces whose implementation change depending on build time parameters, based on settings in build.properties . However, there are less obvious but important best practices that are used by Photofeed in the datastore-specific implementations.
How to do "Transactional Deletes" across Storages
In the Photofeed UI, there are no controls for deleting photos. This was
done to keep the app as simple as possible. However, the source code does
provide everything needed to delete photos. (The only part you need to add
to the Photofeed app is a delete button that sets the
Photo.isActive
property to false.)
In the App Engine datastore implementation, the deletion of a photo
requires deletions in two separate storages: the deletion of the
Photo
entity from Datastore and the deletion of the photo image
blob from Google Cloud Storage. Both of these activities are launched in
an
App Engine cron job
from
the
src.com.google.cloud.demo.CleanupCronServlet
:
public class CleanupCronServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { PhotoServiceManager manager = AppContext.getAppContext().getPhotoServiceManager(); manager.cleanDeatctivedPhotos(); } }
Looking at the configuration file for this cron job at
/war/WEB-INF/cron.xml
, we see how this job is defined:
<?xml version="1.0" encoding="UTF-8"?> <cronentries> <cron> <url>/cron/clean</url> <description>Clean up deleted photos</description> <schedule>every 5 minutes</schedule> </cron> </cronentries>The cron job invokes
PhotoServiceManager.cleanDeactivatedPhotos
every five minutes.
When this cron job runs, it gets a list of all
Photo
entities that have had their
IsActive
property set to false,
in response to a delete button click made by the user in the app UI. This list is
iterated with
PhotoServiceManager.removeDeactivedPhoto
invoked for
each inactive photo:
public void cleanDeatctivedPhotos() { Iterablephotos = photoManager.getDeactivedPhotos(); if (photos != null) { for (Photo photo : photos) { removeDeactivedPhoto(photo); } }
And here is what finally happens to each photo
private void removeDeactivedPhoto(Photo photo) { if (photo != null && !photo.isActive()) { try { FileService fileService = FileServiceFactory.getFileService(); BlobKey blobKey = photo.getBlobKey(); AppEngineFile file = fileService.getBlobFile(blobKey); FileStat stat = fileService.stat(file); if (stat != null) { logger.fine("photo:" + photo.getId() + " blob file stat is not null"); BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService(); blobstoreService.delete(blobKey); logger.info("The blob is deleted. try to delete the entity from datastore."); photoManager.deleteEntity(photo); } } catch (FileNotFoundException e) { logger.info("The blob is alrady deleted. try to delete the entity from datastore."); photoManager.deleteEntity(photo); } catch (Exception e) { logger.severe("Failed to delete the blob storge for photo " + photo.getId() + ":" + e.getMessage()); }
Notice that the blob is deleted first from Google Cloud Storage using
the
blobstoreService
. If you delete the
Photo
object from the Datastore first, you won't be able to delete the blob
because you'll have lost the blob key. After the blob is deleted, the
corresponding
Photo
entity is deleted.
Notice that although the cron job runs every five minutes, it does not
mean the UI will show stale results. Remember that in
photofeed.jsp
only
active
Photo
entities are retrieved and used to serve photo blobs. So a delete that
sets the IsActive property to false has an immediate UI effect, even
though the actual physical deletion happens later.