📜 ⬆️ ⬇️

Using Liquibase to manage database structure in a Spring Boot application. Part 2

In the previous part, we figured out the basic features of Liquibase and wrote a basic example of a Spring boot application that uses Liquibase to initialize the database. The full code of the base application can be seen here on GitHub . In this article we will talk about liquibase-maven-plugin and those additional features that it gives us for versioning the database structure. Let's start with how to automatically create scripts using the comparison function .

Suppose we needed to make any changes to the structure of our database. For example, we want email to not be null . Of course, for such a small change, you could manually correct the code and scripts, but what if there are more changes? In this case, the ability to compare the database built into Liquibase will come to our aid. An interesting feature of it is that you can compare not only two databases, but also a database with a set of JPA entities in our application. That's exactly what we will do now!

Create a script with changes using liquibase-diff


In the plugins section of the pom.xml file, we add this rather complicated design. This is liquibase-maven-plugin , to which a dependency is connected for analyzing hibernate entities and working with YAML files. The plugin will help us automatically generate liquibase scripts by comparing structures in two databases or even by comparing data structures in a database and a set of hiberante entities in our application (this is exactly what liquibase-hibernate5 was added for ).

<plugin> <groupId>org.liquibase</groupId> <artifactId>liquibase-maven-plugin</artifactId> <version>3.5.5</version> <configuration> <propertyFile>${project.build.outputDirectory}/liquibase-maven-plugin.properties</propertyFile> <systemProperties> <user.name>your_liquibase_username</user.name> </systemProperties> <logging>info</logging> </configuration> <dependencies> <dependency> <groupId>org.liquibase.ext</groupId> <artifactId>liquibase-hibernate5</artifactId> <version>3.6</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>2.1.5.RELEASE</version> </dependency> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.24.0-GA</version> </dependency> <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>1.12</version> </dependency> </dependencies> </plugin> 

Pay attention to the user.name setting. It is optional, but without it Liquibase will indicate in the created scripts the name of the current OS user under which the plugin is launched.
')
Settings for the plugin can be written directly in pom.xml or passed as command line parameters when calling maven, but I prefer the option with a separate liquibase-maven-plugin.properties file. Its contents will look something like this.

 changeLogFile= @project.basedir@/src/main/resources/db/changelog/db.changelog-master.yaml url= jdbc:mysql://localhost:3306/geek_db?createDatabaseIfNotExist=true&allowPublicKeyRetrieval=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username= dbusername password= dbpassword driver= com.mysql.cj.jdbc.Driver referenceUrl= hibernate:spring:ru.usharik.liquibase.demo.persist.model?dialect=org.hibernate.dialect.MySQLDialect diffChangeLogFile= @project.basedir@/src/main/resources/db/changelog/db.changelog-@timestamp@.yaml ignoreClasspathPrefix= true 
Here it is worth paying attention to the url and referenceUrl parameters. The script that will create liquibase after comparison will be the difference between the base using the referenceUrl link and the base using the url link. If you later run this script on the base using the url link, then it will become the same as the one located on the referenceUrl link. Particular attention should be paid to the referenceUrl link. As you can see, it refers not to the database, but to the package of our application in which the entity classes are located. Thanks to this, we can now find a script that will add to the database the changes that were made in the code.

Now we need to configure the maven-resource-plugin to replace placeholders in the settings file, such as @project.basedir@ and @timestamp@ . To do this, add the resources section of the following form to the build section

 <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> <includes> <include>*.properties</include> </includes> </resource> <resource> <directory>src/main/resources</directory> <filtering>false</filtering> <includes> <include>**/*.*</include> </includes> </resource> </resources> 

By the way, Spring boot changes the standard format for placeholders populated using maven-resource-plugin from ${smth} to @smth@ . The fact is that @smth@ in Spring Boot are used to substitute environment variables and parameters of the Spring Boot application itself.

Also, we slightly change the properties section in pom.xml to assign the value to the timestamp variable in the format we need. Alas, the standard format may contain characters that are prohibited in file names for some operating systems.

 <properties> <java.version>1.8</java.version> <timestamp>${maven.build.timestamp}</timestamp> <maven.build.timestamp.format>yyyyMMdd-HHmmssSSS</maven.build.timestamp.format> </properties> 

Now let's change the email field in the User class

  @Column(name = "email", nullable = false) private String email; 

Finally, run the build command using liquibase-maven-plugin for comparison.

 mvn clean install liquibase:diff -DskipTests=true 

In this case, we need to completely rebuild the project, because the plugin (liquibase: diff) will use not compiled sources, but compiled entity class files from the target folder for analysis.

If everything is done correctly, then after successful execution of the command in the resources / db / changelog folder, you will see a file with the name db.changelog-20190723-100748666.yaml . Due to the fact that we use the current date and time in the file name, each time we start, we will have a new file, which is quite convenient. If you have already created a database with the table structure corresponding to the previous lesson, then the contents of the file should be like this.

 databaseChangeLog: - changeSet: id: 1563876485764-1 author: your_liquibase_username (generated) changes: - addNotNullConstraint: columnDataType: varchar(255) columnName: email tableName: users 
As you can see, this script makes exactly the change that was made in the code. As an exercise, I would recommend that you run this script against an empty database and look at the result.

Next, we can simply copy the changeSet from this file to db.changelog-master.yaml or we can connect this file to it with an instruction

  - include: file: db.changelog-20190723-100748666.yaml relativeToChangelogFile: true 

Also in this file you need to specify logicalFilePath: db/changelog/db.changelog-20190723-100748666.yaml , similar to how it was done in db.changelog-master.yaml .
This will help to cope with some problems that are possible when using the built-in liquibase bean and liquibase-maven-plugin in the application. After that, restart the application or run the command:

 mvn liquibase:update 

Let's try to make some more complex change to the code. For example, add a role table that will have a many-to-many relationship with the user table.

 @Entity @Table(name = "roles") public class Role implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "name", unique = true, nullable = false) private String name; @ManyToMany(mappedBy = "roles") private Set<User> users; //  , ,  } 

And in the Users table we add
  @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "users_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) private Set<Role> roles; 

After starting the comparison, we get a file with the following contents
 databaseChangeLog: - changeSet: id: 1563877765929-1 author: your_liquibase_username (generated) changes: - createTable: columns: - column: autoIncrement: true constraints: primaryKey: true primaryKeyName: rolesPK name: id type: BIGINT - column: constraints: nullable: false name: name type: VARCHAR(255) tableName: roles - changeSet: id: 1563877765929-2 author: your_liquibase_username (generated) changes: - createTable: columns: - column: constraints: nullable: false name: user_id type: BIGINT - column: constraints: nullable: false name: role_id type: BIGINT tableName: users_roles - changeSet: id: 1563877765929-3 author: your_liquibase_username (generated) changes: - addPrimaryKey: columnNames: user_id, role_id tableName: users_roles - changeSet: id: 1563877765929-4 author: your_liquibase_username (generated) changes: - addUniqueConstraint: columnNames: name constraintName: UC_ROLESNAME_COL tableName: roles - changeSet: id: 1563877765929-5 author: your_liquibase_username (generated) changes: - addForeignKeyConstraint: baseColumnNames: user_id baseTableName: users_roles constraintName: FK2o0jvgh89lemvvo17cbqvdxaa deferrable: false initiallyDeferred: false referencedColumnNames: id referencedTableName: users - changeSet: id: 1563877765929-6 author: your_liquibase_username (generated) changes: - addForeignKeyConstraint: baseColumnNames: role_id baseTableName: users_roles constraintName: FKj6m8fwv7oqv74fcehir1a9ffy deferrable: false initiallyDeferred: false referencedColumnNames: id referencedTableName: roles 

We can also easily add this file to working scripts.

Roll back changes


Now let's see how to roll back the changes made. For some reason, those identifiers that we specified in changeSets cannot be used to roll back to them. There are three options, specify a rollback point


The tag is set as follows.

  - changeSet: id: some_uniqui_id author: liquibase_user_name changes: - tagDatabase: tag: db_tag 

Well, the commands for the three listed ways to do rollback

 mvn liquibase:rollback -Dliquibase.rollbackTag=db_tag mvn liquibase:rollback -Dliquibase.rollbackCount=1 mvn liquibase:rollback "-Dliquibase.rollbackDate=Jun 03, 2017" 

And finally, some additional materials

  1. Code for this article
  2. About rollback in Liquibase
  3. About Migration with Liquibase
  4. Liquibase on github
  5. Very good article on various approaches to versioning a database

Of course, I will be very glad to any comments, additions, clarifications, etc.

Source: https://habr.com/ru/post/460907/


All Articles