Blob of contradictions

Migrating Content Part 1: Users


Last week, I posted about performing a content/user migration from Drupal 6 to Drupal 7 using the migrate module (instead of going via the upgrade route or the 'use the interns at work and make them undergo hell for a few days' of copy content and users one-by-one for migration).

UPDATE - March 13, 2012: Added in findings by Andre and my thoughts.

I posted how to get the migrate module to know that there is a new migration that can be performed and now, we will define how to perform a basic mapping of users with roles from Drupal 6 to Drupal 7 ( as a note, this is a mapping that could be performed against any database or files; the migrate module comes with an example of how to do mappings with files). As a note, the entire code example in full can be found at the bottom of the page (or click here)

We first define our migration class:

  1. class RedcatUserMigration extends Migration {
  2. ...
  3. }

The above will let migrate know that there is a new Migration class (called MyUserMigration) from which content will require migration. It is now time to define what goes inside the class. For this, we must define the class constructor:

  1. public function __construct() {
  2. parent::__construct();
  3. $this->description = t('Migrate legacy users');
  4. }

We define the source fields (these would be the primary keys and any fields that may not be found in the initial mapping query. More on that at the bottom:

  1. $source_fields = array(
  2. 'uid' => t('User ID'),
  3. 'roles' => t('The set of roles assigned to a user.'),
  4. );

We need to define where to get the data from and set the Source and Destination values. In my scenario, I am dealing with a mysql database that is on the same server as my destination database so I directly refer to that database. In other circumstances, you should follow part of the documentation here and here.

  1. $query = db_select(MY_MIGRATION_DATABASE_NAME .'.users', 'u')
  2. ->fields('u', array('uid', 'name', 'pass', 'mail', 'created', 'access', 'login', 'status', 'init'))
  3. ->condition('u.uid', 1, '>');
  4. $this->source = new MigrateSourceSQL($query, $source_fields);
  5. $this->destination = new MigrateDestinationUser();

About the query that gets defined above: please note that all the fields that are defined in there are automatically included as fields that can be mapped to a destination field (so we have defined that there are source mappings for: uid, name, pass, mail, created, access, login, status, and init). You can add any additional conditions that must pass to import a limited set of users (I only want to import users with a uid > 1; yours might have conditions on emails or users).

Also note that if your passwords were previously in md5 format, you can have them imported and automatically converted by having the following as your destination:

  1. $this->destination = new MigrateDestinationUser(array('md5_passwords' => TRUE));

We now need to create a mapping to track the relationship between the source rows from the source database and their resulting Drupal objects; we pass along the schema definitions for the primary key of our source and destination databases (we have to be specific for our source).

  1. $this->map = new MigrateSQLMap($this->machineName,
  2. 'uid' => array(
  3. 'type' => 'int',
  4. 'unsigned' => TRUE,
  5. 'not null' => TRUE,
  6. 'description' => 'D6 Unique User ID',
  7. 'alias' => 'u',
  8. )
  9. ),
  10. MigrateDestinationUser::getKeySchema()
  11. );

From the above, we have now defined for the RedcatUserMigration to run the constructor of the parent class and provide a descriptive name and mapping for the Migration so that when we go from admin->content->migrate, we will now see:

With that out of the way, we are finally ready to move on to the actual mapping. The syntax of this consists of defining the destination field you wish to map against the source field($this->addFieldMapping('destination', 'source')). You can define mappings whereby to check that there are no duplicates (in case you have usernames or emails in your new database that were also in your previous site - an example could be the admin account)($this->addFieldMapping('uniques', 'duplicates')->dedupe('table_name', 'uniques');).
You can even provide default values for fields that don't necessarily require a mapping ($this->addFieldMapping('signature_format')->defaultValue('filtered_html');) or say the value of a field does not matter and you are not going to be migrating anything related to that field ($this->addFieldMapping('path')->issueGroup(t('DNM')); - DNM means Do Not Migrate).

So our primary mapping looks like:

  1. $this->addFieldMapping('name', 'name')->dedupe('users', 'name');
  2. $this->addFieldMapping('pass', 'pass');
  3. $this->addFieldMapping('mail', 'mail')->dedupe('users', 'mail');
  4. $this->addFieldMapping('language')->defaultValue('');
  5. $this->addFieldMapping('theme')->defaultValue('');
  6. $this->addFieldMapping('signature')->defaultValue('');
  7. $this->addFieldMapping('signature_format')->defaultValue('filtered_html');
  8. $this->addFieldMapping('created', 'created');
  9. $this->addFieldMapping('access', 'access');
  10. $this->addFieldMapping('login', 'login');
  11. $this->addFieldMapping('status', 'status');
  12. $this->addFieldMapping('picture')->defaultValue(0);
  13. $this->addFieldMapping('init', 'init');
  14. $this->addFieldMapping('timezone')->defaultValue(NULL);
  15. $this->addFieldMapping('path')->issueGroup(t('DNM'));
  16. $this->addFieldMapping('pathauto_perform_alias')->defaultValue('1');
  17. $this->addFieldMapping('roles', 'roles');

So far, we have defined a ton of mappings and for the most part, we are through with this.

On a side note, in my code, I did not do a mapping of the user id from the old site to the new one. If you wanted to do that as well, then you would have the following in your mappings:

  1. $this->addFieldMapping('is_new')->defaultValue(TRUE);
  2. $this->addFieldMapping('uid', 'uid');

This will create new users in your system with their old user ids. The biggest gain from this would be that doing a mapping from the old system to the new one is much easier (you don't have to do any additional querying to figure out how the old user id maps out to the new one when migrating other content). However, the downside will be that if you are introducing new users in your system prior to this migration and the usernames from old uid and new uid don't match up, that user with the uid conflict with not migrate over into your site. In my scenario, it was easy enough to let the user ids roll forward (its actually not difficult to have a userid dependency from another migration) so I have not bothers with retaining the old user id. Nonetheless, something to think about.

However, we have not covered a mapping for when we have data that covers multiple roles. In the scenario we have above, that data would be roles. And the migrate module will warn that such a mapping has not been created (in the above scenario, your users will migrate; they just won't have any roles attached to them). So the way to do this is by preparing a row and massaging in any new data (or existing data if we want to clean it up or replace it). This is just by implementing prepareRow()

  1. public function prepareRow($current_row) {
  2. $source_id = $current_row->uid;
  3. $query = db_select(MY_MIGRATION_DATABASE_NAME .'.users_roles', 'r')
  4. ->fields('r', array('uid', 'rid'))
  5. ->condition('r.uid', $source_id, '=');
  6. $results = $query->execute();
  7. $roles = array('2' => '2');
  8. foreach ($results as $row) {
  9. $roles[$row->rid] = $row->rid;
  10. }
  11. $current_row->roles = $roles;
  12. return TRUE;
  13. // return FALSE if you wish to skip a particular row
  14. }

There is a lot happening above so let me explain the short of it. In my scenario, I had created my roles and their role IDs happened to match the role IDs I had in my old site. So I perform a query to get the roles a particular user has (defined by $current_row which has all of the mappings defined from above so we use the uid). We then create a roles variable to which we attach all the role IDs that it can find. And that's all!

Once all of this is set up, we are ready to run drush migrate-import RedcatUser and all your users will now be imported along with their correct roles into the new website!

As a final code mashup, all of the code from above looks like the following:

  1. class RedcatUserMigration extends Migration {
  2. public function __construct() {
  3. parent::__construct();
  4. $this->description = t('Migrate REDCAT users');
  6. $source_fields = array(
  7. 'uid' => t('User ID'),
  8. 'roles' => t('The set of roles assigned to a user.'),
  9. );
  11. $query = db_select(REDCAT_MIGRATION_DATABASE_NAME .'.users', 'u')
  12. ->fields('u', array('uid', 'name', 'pass', 'mail', 'created', 'access', 'login', 'status', 'init'))
  13. ->condition('', array('admin', 'rgates'), 'NOT IN')
  14. ->condition('u.uid', 0, '>');
  15. $this->source = new MigrateSourceSQL($query, $source_fields);
  16. $this->destination = new MigrateDestinationUser();
  18. $this->map = new MigrateSQLMap($this->machineName,
  19. 'uid' => array(
  20. 'type' => 'int',
  21. 'unsigned' => TRUE,
  22. 'not null' => TRUE,
  23. 'description' => 'D6 Unique User ID',
  24. 'alias' => 'u',
  25. )
  26. ),
  27. MigrateDestinationUser::getKeySchema()
  28. );
  30. // Make the mappings
  31. $this->addFieldMapping('name', 'name')->dedupe('users', 'name');
  32. $this->addFieldMapping('pass', 'pass');
  33. $this->addFieldMapping('mail', 'mail')->dedupe('users', 'mail');
  34. $this->addFieldMapping('language')->defaultValue('');
  35. $this->addFieldMapping('theme')->defaultValue('');
  36. $this->addFieldMapping('signature')->defaultValue('');
  37. $this->addFieldMapping('signature_format')->defaultValue('filtered_html');
  38. $this->addFieldMapping('created', 'created');
  39. $this->addFieldMapping('access', 'access');
  40. $this->addFieldMapping('login', 'login');
  41. $this->addFieldMapping('status', 'status');
  42. $this->addFieldMapping('picture')->defaultValue(0);
  43. $this->addFieldMapping('init', 'init');
  44. $this->addFieldMapping('timezone')->defaultValue(NULL);
  45. $this->addFieldMapping('path')->issueGroup(t('DNM'));
  46. $this->addFieldMapping('pathauto_perform_alias')->defaultValue('1');
  47. $this->addFieldMapping('roles', 'roles');
  48. }
  50. public function prepareRow($current_row) {
  51. $source_id = $current_row->uid;
  52. $query = db_select(REDCAT_MIGRATION_DATABASE_NAME .'.users_roles', 'r')
  53. ->fields('r', array('uid', 'rid'))
  54. ->condition('r.uid', $source_id, '=');
  55. $results = $query->execute();
  56. $roles = array('2' => '2');
  57. foreach ($results as $row) {
  58. $roles[$row->rid] = $row->rid;
  59. }
  60. $current_row->roles = $roles;
  61. return TRUE;
  62. // return FALSE if you wish to skip a particular row
  63. }
  64. }

I hope all of this is helpful. If something doesn't make a lot of sense, leave a comment! I'll try and improve this documentation. And next time, I'll post how to migrate a basic page with taxonomy terms and attached files.