Liferay did what to my what? Part 2: Where did that come from?
Last time I wrote about tracking what liferay is doing to your database. Now it's time to bring the discussion up a layer to figure out why those specific database calls are being made.
Chances are, if you're doing anything that is involved enough to need to start tracking through a swathe of database calls, you're going to notice that multiple changes are made to your database for a given click on the control panel. For example, the action of creating a new site in liferay results in a number of changes to the database:
INSERT INTO GROUP_ VALUES(10614,10154,10196,10001,10614,0,0,'Test Site','This is a test site',2,'','/test-site','1','1') INSERT INTO LAYOUTSET VALUES(10615,10614,10154,'2013-01-09 16:56:01.504000','2013-01-09 16:56:01.504000','1','0',0,'classic','01','mobile','01','',0,'','','0') INSERT INTO LAYOUTSET VALUES(10616,10614,10154,'2013-01-09 16:56:01.506000','2013-01-09 16:56:01.506000','0','0',0,'classic','01','mobile','01','',0,'','','0') INSERT INTO RESOURCEPERMISSION VALUES(611,10154,'com.liferay.portal.model.Group',4,'10614',10163,0,2097151) INSERT INTO USERGROUPROLE VALUES(10196,10614,10171) INSERT INTO USERS_GROUPS VALUES(10196,10614) INSERT INTO ASSETENTRY VALUES(10617,10192,10154,10196,'Test Test','2013-01-09 16:56:02.502000','2013-01-09 16:56:02.502000',10001,10614,'',0,'0',NULL,NULL,NULL,NULL,'','Test Site','This is a test site','','','',0,0,0.0E0,0) COMMIT
Now, if you wanted to create a site through your own code, you might be tempted to go and start chaining together calls to various *LocalServiceUtil classes (yes, I'm ignoring the whole static methods, testability, *LocalServiceUtil.getService(), dependency injection conversation for now) to mimic that process. Perhaps something like:
public class SiteCreator { public static void createGroup() throws Exception { // Don't do this! long groupId = CounterLocalServiceUtil.increment(Group.class.getName()); Group group = GroupLocalServiceUtil.createGroup(groupId); group.setName("Test Site"); group.setDescription("This is a test site"); group.setFriendlyURL("/test-site"); // Set a bunch of other properties GroupLocalServiceUtil.addGroup(group); long layoutSetId = CounterLocalServiceUtil.increment( LayoutSet.class.getName() ); LayoutSet layoutSet = LayoutSetLocalServiceUtil.createLayoutSet(layoutSetId); layoutSet.setGroupId(groupId); layoutSet.setPrivateLayout(false); layoutSet.setThemeId("classic"); layoutSet.setColorSchemeId("01"); // Set a bunch of other properties LayoutSetLocalServiceUtil.addLayoutSet(layoutSet); long resourcePermissionId = CounterLocalServiceUtil.increment( ResourcePermission.class.getName() ); ResourcePermission resourcePermission = ResourcePermissionLocalServiceUtil.createResourcePermission( resourcePermissionId ); resourcePermission.setName(Group.class.getName()); resourcePermission.setPrimaryKey(groupId); // Set a bunch of other properties ResourcePermissionLocalServiceUtil.addResourcePermission(resourcePermission); // Continue on with other objects, properties // as needed until you get the job done... } }
Hopefully you can tell that this is a pretty bad idea. Not only are you going through and doing a bunch of work that already exists somewhere within liferay, you often need to go and start tracking through the database to find out what the various magic numbers are in the SQL you have in your hands, to try and figure out what the relationship is.
As hinted at above - there's a better way to reuse what liferay is already doing to create those entities, although it may not be immediately obvious what that way is. If you're not already aware, the vast majority of liferay's code that does real work (as opposed to routing data from the client to the services that do stuff) are generated by a nifty little tool called the Service Builder.
The service builder takes care of generating all of the generic CRUD type functionality you need for persisting and retrieval of information. This is the source of the various LocalServiceUtil, LocalService, ServiceUtil, Util, etc classes you've probably worked with when accessing liferay APIs.
However, the service builder also allows you to write custom code which it then incorporates back into those interfaces, and it is in these extensions that more complex code than "create a user" or "find a wiki page" take place. Service builder extensions take place in the *LocalServiceImpl classes - so that's where we go to look for the magical "do that again for me" entry point.
Returning to our "Create a site" example above, if we take a look through the GroupLocalServiceImpl code, the first method we find takes care of all of that for us.
/** * Adds a group. * * @param userId the primary key of the group's creator/owner * @param parentGroupId the primary key of the parent group * @param className the entity's class name * @param classPK the primary key of the entity's instance * @param liveGroupId the primary key of the live group * @param name the entity's name * @param description the group's description (optionally * <code>null</code>) * @param type the group's type. For more information see {@link * com.liferay.portal.model.GroupConstants} * @param friendlyURL the group's friendlyURL (optionally * <code>null</code>) * @param site whether the group is to be associated with a main site * @param active whether the group is active * @param serviceContext the service context to be applied (optionally * <code>null</code>). Can set asset category IDs and asset tag * names for the group, and whether the group is for staging. * @return the group * @throws PortalException if a creator could not be found, if the group's * information was invalid, if a layout could not be found, or if a * valid friendly URL could not be created for the group * @throws SystemException if a system exception occurred */ public Group addGroup( long userId, long parentGroupId, String className, long classPK, long liveGroupId, String name, String description, int type, String friendlyURL, boolean site, boolean active, ServiceContext serviceContext) throws PortalException, SystemException { // Group User user = userPersistence.findByPrimaryKey(userId); className = GetterUtil.getString(className); long classNameId = PortalUtil.getClassNameId(className); String friendlyName = name; long groupId = 0; while (true) { groupId = counterLocalService.increment(); User screenNameUser = userPersistence.fetchByC_SN( user.getCompanyId(), String.valueOf(groupId)); if (screenNameUser == null) { break; } } boolean staging = isStaging(serviceContext); long groupClassNameId = PortalUtil.getClassNameId(Group.class); if ((classNameId <= 0) || className.equals(Group.class.getName())) { className = Group.class.getName(); classNameId = groupClassNameId; classPK = groupId; } else if (className.equals(Organization.class.getName())) { name = getOrgGroupName(name); } else if (!GroupConstants.USER_PERSONAL_SITE.equals(name)) { name = String.valueOf(classPK); } if (className.equals(Organization.class.getName()) && staging) { classPK = liveGroupId; } if (className.equals(Layout.class.getName())) { Layout layout = layoutLocalService.getLayout(classPK); parentGroupId = layout.getGroupId(); } friendlyURL = getFriendlyURL( user.getCompanyId(), groupId, classNameId, classPK, friendlyName, friendlyURL); if (staging) { name = name.concat(" (Staging)"); friendlyURL = friendlyURL.concat("-staging"); } if (className.equals(Group.class.getName())) { if (!site && (liveGroupId == 0) && !name.equals(GroupConstants.CONTROL_PANEL)) { throw new IllegalArgumentException(); } } else if (!className.equals(Organization.class.getName()) && className.startsWith("com.liferay.portal.model.")) { if (site) { throw new IllegalArgumentException(); } } if ((classNameId <= 0) || className.equals(Group.class.getName())) { validateName(groupId, user.getCompanyId(), name, site); } validateFriendlyURL( user.getCompanyId(), groupId, classNameId, classPK, friendlyURL); Group group = groupPersistence.create(groupId); group.setCompanyId(user.getCompanyId()); group.setCreatorUserId(userId); group.setClassNameId(classNameId); group.setClassPK(classPK); group.setParentGroupId(parentGroupId); group.setLiveGroupId(liveGroupId); group.setName(name); group.setDescription(description); group.setType(type); group.setFriendlyURL(friendlyURL); group.setSite(site); group.setActive(active); if ((serviceContext != null) && (classNameId == groupClassNameId) && !user.isDefaultUser()) { group.setExpandoBridgeAttributes(serviceContext); } groupPersistence.update(group); // Layout sets layoutSetLocalService.addLayoutSet(groupId, true); layoutSetLocalService.addLayoutSet(groupId, false); if ((classNameId == groupClassNameId) && !user.isDefaultUser()) { // Resources resourceLocalService.addResources( group.getCompanyId(), 0, 0, Group.class.getName(), group.getGroupId(), false, false, false); // Site roles Role role = roleLocalService.getRole( group.getCompanyId(), RoleConstants.SITE_OWNER); userGroupRoleLocalService.addUserGroupRoles( userId, groupId, new long[] {role.getRoleId()}); // User userLocalService.addGroupUsers( group.getGroupId(), new long[] {userId}); // Asset if (serviceContext != null) { updateAsset( userId, group, serviceContext.getAssetCategoryIds(), serviceContext.getAssetTagNames()); } } else if (className.equals(Organization.class.getName()) && !user.isDefaultUser()) { // Resources resourceLocalService.addResources( group.getCompanyId(), 0, 0, Group.class.getName(), group.getGroupId(), false, false, false); } return group; }
So, now we can completely avoid needing to reproduce the code we were attempting to write, and instead rely on something given to us by the platform.
Rounding out - what does this all mean? In short - if you want to do something beyond simple CRUD operations, chances are the method is there - you just need to look through the *LocalServiceImpl class to track it down!