unable to test UUID - spring-data-jpa

I have a spring boot 2.6.7 app, using Liquibase, Gradle and Spock. I have a class that uses a guid format string as the ID:
#Entity
#Table(name = "devices")
public class Device {
#Id
#NotNull
#Pattern(regexp = "^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$",
message = "Invalid id received")
private String deviceId;
#NotNull
private DeviceType deviceType;
#JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd")
private LocalDate manufactureDate;
#JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd")
private LocalDate activationDate;
#JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd")
private LocalDate deactivationDate;
private Status deviceStatus;
I have several endpoint that take this entity and persist/find/update it. When I look at the database, I see the columns are properly populated. So it appears to be working.
Here is my liquibase for this table:
- changeSet:
id: 7
preConditions:
- onFail: MARK_RAN
not:
tableExists:
tableName:
devices
changes:
- createTable:
tableName: devices
columns:
nullable: false
- column:
name: device_id
type: varchar(255)
constraints:
primaryKey: true
nullable: false
- column:
name: device_type
type: varchar(255)
constraints:
nullable: true
- column:
name: manufacture_date
type: date
constraints:
nullable: true
- column:
name: activation_date
type: date
constraints:
nullable: true
- column:
name: deactivation_date
type: date
constraints:
nullable: true
- column:
name: internal_id
type: varchar(255)
constraints:
nullable: true
- column:
name: device_status
type: varchar(255)
constraints:
nullable: true
However, I am now trying to wrap my code in tests and I keep running into issues that a string cannot be cast as UUID. Here is the test:
given:
def device = new Device(internalId: internalId, deviceId: deviceId,
deviceType: deviceType, deviceStatus: deviceStatus, role: role, activationDate: activationDate, deactivationDate: deactivationDate, manufactureDate: manufactureDate)
def result = deviceRepository.save(device)
when:
def isValid = deviceServices.validateDevice(result)
then:
isValid == testResult
where:
deviceId | deviceType | deviceStatus | manufactureDate | activationDate | deactivationDate | || testResult
UUID.randomUUID() | DeviceType.EQUIPMENT | Status.ACTIVATED | LocalDate.now() | LocalDate.now() | null || true
And here is the error when the test starts by trying to insert into H2. The field is declared as a varchar, but for some reason the stack trace mentions NumberFormat and Long values.
SQL Error: 22018, SQLState: 22018
2022-05-12 06:24:34.083 ERROR 42198 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Data conversion error converting "faf837fa-8584-4dff-a8d1-bd4d9a8af74c"; SQL statement:
select identity0_.device_id as scanned_1_6_, identity0_.internal_id as internal2_6_ from identities identity0_ where identity0_.device_id=? [22018-212]
...
Caused by: org.h2.message.DbException: Data conversion error converting "9bbb114a-bb99-443c-9106-dd28210c4e7b" [22018-212]
at org.h2.message.DbException.get(DbException.java:212)
at org.h2.value.ValueStringBase.getLong(ValueStringBase.java:142)
at org.h2.value.Value.convertToBigint(Value.java:1645)
at org.h2.value.Value.convertTo(Value.java:1137)
at org.h2.value.Value.convertForAssignTo(Value.java:1092)
at org.h2.table.Column.validateConvertUpdateSequence(Column.java:369)
... 53 more
Caused by: org.h2.jdbc.JdbcSQLDataException: Data conversion error converting "9bbb114a-bb99-443c-9106-dd28210c4e7b" [22018-212]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:506)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:477)
... 59 more
Caused by: java.lang.NumberFormatException: For input string: "9bbb114a-bb99-443c-9106-dd28210c4e7b"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Long.parseLong(Long.java:692)
at java.base/java.lang.Long.parseLong(Long.java:817)
at org.h2.value.ValueStringBase.getLong(ValueStringBase.java:140)
... 57 more
I feel like I have coded myself into a corner by not making the primary key a UUID, or by not having a regular ID and then using the deviceId as a UUID. However, this is production code and I am hesitant to change the table structure via Liquibase.
Is there a way to make Liquibase or Spock work around this issue? Again, it works fine in production, I just can't do an integration test on it (which might be a H2 limitation?)
Update:
I have another test that has the same behavior - unable to convert a UUID to a string attribute of the class upon persistence. If I change the "UUID.randomUUID()" to "123" it works as expected.
Because I am seeing this only with my tests and the exception is SqlExceptionHelper I have to wonder if test H2 just can't handle the conversion that production Postgres does? I changed H2 for HSQL and get the same type of error.

The type of Device.deviceID is String, but the type of deviceId in your Spock spec is UUID. So you either need to use
def device = new Device(internalId: internalId, deviceId: deviceId.toString(), ...
or
where:
deviceId | ...
UUID.randomUUID().toString() | ...
It really is as trivial as that.

Related

typeorm relation in embedded column

I'm working with NestJs, Typeorm and Postgresql.
I'm trying to use ManyToOne relation in embedded entity. I need to load foreign key column in node environment, so added one additional column(createdById column below). This makes problem.
Here is my code.
A.ts
#Entity()
export class A {
#PrimaryGeneratedColumn()
id!: number;
#Column(() => Embed, { prefix: false })
embed!: Embed;
#CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
}
Embed.ts
export class Embed {
#Column()
x!: number;
#Column()
y!: number;
#ManyToOne(() => B)
#JoinColumn({ name: 'created_by_id' })
createdBy?: B;
#Column({ name: 'created_by_id' })
createdById!: number;
}
B.ts
#Entity()
export class B {
#PrimaryGeneratedColumn()
id!: number;
#CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
}
When I run the app with option TYPEORM_SYNCHRONIZE=true and TYPEORM_LOGGING=true, I get error messages like query failed: CREATE TABLE "a" ("id" SERIAL NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "created_by_id" integer NOT NULL, "created_by_id" integer NOT NULL, "x" integer NOT NULL, "y" integer NOT NULL, CONSTRAINT "PK_684f21444e543375e4c2e6f27fe" PRIMARY KEY ("id")), Message: column \"created_by_id\" specified more than once.. Typeorm trying to create created_by_id column twice. (I applied custom NamingStrategy so that column of embedded entity's name to be snake_case)
If I place createdBy and createdById column to A directly, then it makes no error. Is it problem of typeorm version? Or any other solutions?
package version:
"dependencies": {
"#nestjs/typeorm": "7.1.0,
"typeorm": "0.2.31",
}
run with docker container,
node image: 16.14.2-alpine3.15,
postgres image: mdillon/postgis:11-alpine
TypeORM docs say you don't need to decorate embed class with #Entity().
See Name class in https://orkhan.gitbook.io/typeorm/docs/embedded-entities

Cannot set values on uuid or enum fields in Postgres using TypeORM

I've been stuck on this for a good hour, and unfortunately can't find anything relevant in the docs.
I'm unable to set values on fields that use types like enum or uuid. For instance:
export enum PhotoStatus {
WORKING = 'WORKING',
DONE = 'DONE'
}
export class Photo {
#PrimaryGeneratedColumn()
id: number:
#Column()
size: string;
#Column({
type: 'enum',
enum: PhotoStatus,
default: PhotoStatus.WORKING,
})
status: PhotoStatus;
}
But when I try to create a new record, like so:
const photo = new Photo();
photo.size = 'huge';
photo.status = PhotoStatus.DONE;
await connection.manager.save(photo);
It throws an error:
BadRequestException: ERROR: column "status" is of type content_status_enum but expression is of type character varying
Hint: You will need to rewrite or cast the expression.
This happens for uuid columns too.

TypeORM not setting NULL as DateString

I have the follwing property in my Customer entity (using a Postgres DB):
#Column({
type: "timestamptz",
nullable: true
})
#IsDateString()
cancelationDate: Date | string;
I want to be able to change the date (which works) but also to set it null (which does not work)
Works:
customer.cancelationDate = moment().add({years:100}).toISOString();
Throws error:
customer.cancelationDate = null;
It says property cancelationDate has failed the following constraints: isDateString
Is there any way to update this with null? Removing the #IsDateString() results in not updating the column at all.
Any hint is highly appreciated

Why Spring Boot/JPA creates constraints names like this fkm5pcdf557o18ra19dajf7u26a?

I have an entity called City:
#Entity
#Table (name = "cities")
public class City implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(updatable = false, nullable = false, columnDefinition = "serial")
private Long id;
#Column(nullable = false)
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(nullable = false)
private Department department;
#Column(nullable = true)
private Integer code;
}
In this case if I not specify a name for the field Department it creates a field called department_id in my table "cities", that's Ok but when I see the Constraints created appears a constraint with name fkcl2xocc3mnys8b84bw2dog35e. Why this name? Does it make any sense?
This is my yaml jpa configuration:
spring:
profiles: development
datasource:
url: jdbc:postgresql://localhost:5432/lalala
username: postgres
password: postgres
sql-script-encoding: UTF-8
driver-class-name: org.postgresql.Driver
data.jpa.repositories.enabled: true
jpa:
generate-ddl: true
hibernate:
ddl-auto: update
show_sql: true
use_sql_comments: true
format_sql: true
type: trace
jdbc.lob.non_contextual_creation: true
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
properties:
hibernate:
jdbc.lob.non_contextual_creation: true
dialect: org.hibernate.dialect.PostgreSQLDialect
Hibernate generates a constraint name by concatenating table and properties names and convert the result to MD5. It is needed, because of the constraint names length restriction in some databases. For an example, in the Oracle database, a foreign key name length can't be more than 30 symbols length.
This code snippet from Hibernate source org.hibernate.mapping.Constraint
/**
* If a constraint is not explicitly named, this is called to generate
* a unique hash using the table and column names.
* Static so the name can be generated prior to creating the Constraint.
* They're cached, keyed by name, in multiple locations.
*
* #return String The generated name
*/
public static String generateName(String prefix, Table table, Column... columns) {
// Use a concatenation that guarantees uniqueness, even if identical names
// exist between all table and column identifiers.
StringBuilder sb = new StringBuilder( "table`" + table.getName() + "`" );
// Ensure a consistent ordering of columns, regardless of the order
// they were bound.
// Clone the list, as sometimes a set of order-dependent Column
// bindings are given.
Column[] alphabeticalColumns = columns.clone();
Arrays.sort( alphabeticalColumns, ColumnComparator.INSTANCE );
for ( Column column : alphabeticalColumns ) {
String columnName = column == null ? "" : column.getName();
sb.append( "column`" ).append( columnName ).append( "`" );
}
return prefix + hashedName( sb.toString() );
}
/**
* Hash a constraint name using MD5. Convert the MD5 digest to base 35
* (full alphanumeric), guaranteeing
* that the length of the name will always be smaller than the 30
* character identifier restriction enforced by a few dialects.
*
* #param s
* The name to be hashed.
* #return String The hased name.
*/
public static String hashedName(String s) {
try {
MessageDigest md = MessageDigest.getInstance( "MD5" );
md.reset();
md.update( s.getBytes() );
byte[] digest = md.digest();
BigInteger bigInt = new BigInteger( 1, digest );
// By converting to base 35 (full alphanumeric), we guarantee
// that the length of the name will always be smaller than the 30
// character identifier restriction enforced by a few dialects.
return bigInt.toString( 35 );
}
catch ( NoSuchAlgorithmException e ) {
throw new HibernateException( "Unable to generate a hashed Constraint name!", e );
}
}
You can generate your own constraint names (unique and foreign key) using ImplicitNamingStrategy. You can refer Hibernate5NamingStrategy , as an example.
This might be the default name generated by the provider. You can use ForeignKey annotation to specify the name
#JoinColumn(foreignKey = #ForeignKey(name = "FK_NAME"))

How to avoid DROP DEFAULT statements with Doctrine 2 Migrations diff on first run?

I had an existing PostgreSQL database with a table created like this:
CREATE TABLE product (id SERIAL PRIMARY KEY, name VARCHAR(100) DEFAULT NULL)
This table is described in a YML Doctrine2 file within a Symfony2 project:
Acme\DemoBundle\Entity\Product:
type: entity
table: product
fields:
id:
id: true
type: integer
nullable: false
generator:
strategy: SEQUENCE
name:
type: string
length: 100
nullable: true
When I run for the first time the Doctrine Migrations diff task, I should get a versioning file with no data in the up and down methods. But what I get instead is this :
// ...
class Version20120807125808 extends AbstractMigration
{
public function up(Schema $schema)
{
// this up() migration is autogenerated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != "postgresql");
$this->addSql("ALTER TABLE product ALTER id DROP DEFAULT");
}
public function down(Schema $schema)
{
// this down() migration is autogenerated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != "postgresql");
$this->addSql("CREATE SEQUENCE product_id_seq");
$this->addSql("SELECT setval('product_id_seq', (SELECT MAX(id) FROM product))");
$this->addSql("ALTER TABLE product ALTER id SET DEFAULT nextval('product_id_seq')");
}
}
Why are differences detected? How can I avoid this? I tried several sequence strategies with no success.
A little update on this question.
Using Doctrine 2.4, the solution is to use the IDENTITY generator strategy :
Acme\DemoBundle\Entity\Product:
type: entity
table: product
id:
type: integer
generator:
strategy: IDENTITY
fields:
name:
type: string
length: 100
nullable: true
To avoid DROP DEFAULT on fields that have a default value in the database, the default option on the field is the way to go. Of course this can be done with lifecycle callbacks, but it's necessary to keep the default value in the database if this database is used by other apps.
For a "DEFAULT NOW()" like default value, the solution is the following one:
Acme\DemoBundle\Entity\Product:
type: entity
table: product
id:
type: integer
generator:
strategy: IDENTITY
fields:
creation_date:
type: datetime
nullable: false
options:
default: CURRENT_TIMESTAMP
Doctrine 2.0 does not support the SQL DEFAULT keyword, and will always try to drop a postgres default value.
I have found no solution to this problem, I just let doctrine handle the sequences itself.
This is a opened bug registered here :
http://www.doctrine-project.org/jira/browse/DBAL-903