spring_boot_starter_data 0.6.1
spring_boot_starter_data: ^0.6.1 copied to clipboard
Spring Data for spring_boot_dart: entities, repositories, derived and @Query methods, transactions, optimistic and pessimistic locks; no driver inside.
0.6.1 #
Changes #
PageableandSortcontroller parameters (withspring_boot_starter_web): the glue the generator writes (templatelib/src/web/paging_argument_resolver.dart.tmpl, the beanpagingArgumentResolvers, aWebMvcConfigurerthat registers aHandlerMethodArgumentResolver) fills them from?page=&size=&sort=prop,desc&sort=otheras Spring Data Web does, withspring.data.web.pageable.default-page-size(20),max-page-size(2000),one-indexed-parameters,page-parameter,size-parameter,prefix,qualifier-delimiterandspring.data.web.sort.sort-parameter;@PageableDefaultand@SortDefaulton the parameter,@Qualifierfor several. The logic isWebPaging/WebPagingProperties(no web types). The size is cut at the maximum, a negative or non-numeric page is 0, a size below one is the default; two unqualifiedPageableparameters stop the start; an unknown sort property is the existing 400.- A returned
Page<T>(PageImpl.toJson) is written as Spring Data 3.3's stable JSON:contentandpage: {size, number, totalElements, totalPages}. It could not be written as JSON before. rollbackForandnoRollbackForfollow Spring'sRuleBasedTransactionAttribute: when both match, the entry closest to the exception's class decides (noRollbackFor: [Hard]beatsrollbackFor: [Soft]for aHard extends Soft); a tie goes torollbackFor. The generator writes each listed type's depth in the hierarchy next to its type test (BeanModule.typeDepths,BeanIntrospector.typeDepth); a module without depths keeps the old rule (rollbackForwins). The difference is gone from "Compared with Spring" and from the differential test's known differences. Regenerate the.spring.dartfiles.InvalidDataArgumentException(anInvalidDataAccessApiUsageException): a value of the caller the database cannot take, never in its message. A NUL character in a text argument, a list over the bound-parameter limit (a derivedIn, the PostgreSQL driver) and aLIKEpattern PostgreSQL rejects (22025,22021) throw it instead of the plain exception, and the web glue answers it with a 400Invalid request value(it was a 500); otherInvalidDataAccessApiUsageExceptions stay a 500.
Fixes #
- Case folding on SQLite:
ignoreCase,...ContainingIgnoreCase,...StartingWithIgnoreCaseandIn...IgnoreCasefold every letter (ДЮНАfindsДюна,äfindsÄ), as the in-memory store and PostgreSQL do; the driver registers Dart'stoLowerCaseasspring_lowerwhen a connection opens, also in the worker isolate. A test of Cyrillic and German titles now runs on the three stores. - A derived query's text argument ending in
\, holding\%or\\is matched literally on every store (tests on the three stores). In a@Query, PostgreSQL's22025(aLIKEpattern ending with its escape) is anInvalidDataAccessApiUsageExceptionthat says so, not aDataIntegrityViolationException;22021likewise. - A NUL character in a text argument is an
InvalidDataAccessApiUsageExceptionraised before the statement: for a derived query on every store (SQLite'sLIKEstopped reading a pattern at it), for any text parameter on PostgreSQL (a@Query, asave). It was PostgreSQL's22021, a 500 in the web glue (which maps only its few exceptions, so it stays a 500: validate request text). - A derived
In/NotInof more than 65 535 values no longer fails on PostgreSQL: the list is bound as one array parameter (= any($1)/<> all($1), null semantics unchanged). On SQLite a list past its limit of 32 766 values is anInvalidDataAccessApiUsageExceptionthat says so; a@Querywith more than 65 535 values gets that message on PostgreSQL before it is sent.SqlDialectgainsmaxBoundParametersandarrayMembership. @DataTest:@Transactional(transactionManager: 'name')on a test method, a@Nestedgroup or the test class chooses the transaction manager of that test (the generator passes it torunInTestTransaction), over@DataTest(transactionManager:). Regenerate the.test.spring.dartfiles.
0.6.0 #
First release: Spring Data for Dart, on Spring Data JDBC's model (an aggregate is loaded and saved whole; other aggregates are referenced by id; no persistence context, no lazy loading, no proxies of entities). The package has no database driver inside: the application lists sqlite3 or postgres and the generator writes the glue.
Entities and repositories #
- Entities:
@Table,@Id,@Column,@Transient,@Version(optimistic locking:OptimisticLockingFailureExceptionon a conflict),@CreatedDate,@LastModifiedDate,@CreatedBy,@LastModifiedBy(@EnableDataAuditing,AuditorAware),@MappedCollectionchildren saved and deleted with their root,NamingStrategy. Default values of constructor parameters and fields are written into theDEFAULTclauses of created schemas. - References between aggregates:
@Reference(Author) int authorIdnext toAggregateReference<Author, int>(the id type is checked against the target's@Id; a field of an entity type without@MappedCollectionis refused with a hint), foreign keys and an index for every reference in created schemas,@OnDelete(OnDeleteAction.noAction / restrict / cascade / setNull / setDefault)(setDefaultwrites no default yet: the column becomes null),@Unique()and@UniqueConstraint([...], name:). - Repositories: an interface extending
DataRepository(CrudRepository,ListCrudRepository,PagingAndSortingRepository,ListPagingAndSortingRepository) is implemented by the generator and is a bean, no annotation needed;@UseStoreand@UseDataSourcechoose its store and database. The generated class extendsSimpleRepository(the CRUD, sorting and paging methods) and writes one call onRepositoryOperationsfor each method the interface declares. - Derived queries (
findTop5ByTitleContainingIgnoreCaseOrderByYearDesc:find,read,get,query,search,stream,count,exists,delete,remove,Distinct,Top,First,And,Or,IgnoreCase,OrderBy, every keyword) are parsed and checked by the build: a wrong property is a build error naming the interface, with the reason and a suggestion (No property 'titel' found for type 'Book'; did you mean 'title'?), and so is a method that is neither CRUD nor derived or a signature that does not fit. Sort,SortOrder,Pageable,PageRequest,Page,Slice, Spring'sDataAccessExceptionhierarchy (withConcurrencyFailureExceptionas the root of the version, lock, deadlock and serialization failures, the type retry code catches).@Querywith named parameters,#{#entityName},${property}, DTO results,PageandSlice(with acountQuery),@Modifying,rowMapperandresultSetExtractor; values are always bound, never written into the SQL.{Book}and{Book.authorId}write the quoted table and column of an entity and its property through the mapping.@Queryis checked by the build, in the one error of its repository (Invalid repository X:): the method against its annotation (FutureorStream,PageableandSort,@Modifying, the mapper functions,@Lock,countQuery, the result type), the text (:nameagainst the parameters, no plain?), the entity placeholders, and misspelled tables and columns when a known name is close (b.titel: no column titel in books, did you mean 'title'?). The build knows the entities and what the migrations and init scripts oflib/resources/create; it says nothing when unsure.- The
:#{...}expressions (SpEL) of a@Queryare compiled to Dart by the build when it can (QueryMethod.expressions), so a typo is a build error and nothing is interpreted at run time; what the compiler leaves alone (T(...),new, selection,matches,instanceof, ...) is interpreted when the query runs. - Entities are described by the generator as compact
EntityIntrospections (properties, annotations, defaults, constructor role, and the functionsget,createandwrite), registered as anEntityIntrospectionsbean;RelationalMappingContextis built from them. A class a@Queryreturns is mapped byEntityRowMapper; an@Introspectedclass goes throughDataClassRowMapper, and a class described without a part that is used fails with anIntrospectionPartErrorthat says to add@Introspected().
Transactions #
@Transactionalon bean methods with Spring's seven propagations, isolation, timeout, read-only,rollbackFor/noRollbackFor(every exception rolls back by default: Dart has no checked exceptions),transactionManager:;TransactionTemplate,PlatformTransactionManager,AbstractPlatformTransactionManager,TransactionSynchronization. A@Transactionalmethod must return aFuture.- The current transaction lives in a
Zone: it followsawait, two concurrent requests never share one,requiresNewandnotSupportedset aside only their own store's transaction, and work started withoutawaitthat outlives its transaction fails instead of running outside it. @Lock(LockMode.pessimisticWrite / pessimisticRead, timeout:, skipLocked:)on derivedfindmethods andfindById, inside a transaction only. Repository writes run in a transaction:saveAlland an aggregate with its children are atomic.- The in-memory store has transactions too: read committed, row locks with deadlock detection and a lock timeout (
spring.data.memory.lock-timeout, 10s), savepoints, read-only transactions refusing writes. It enforces references and unique keys exactly as SQLite withforeign_keys = ONdoes, so a test that passes in memory does not fail in production. - Compared with Spring on PostgreSQL:
tool/data_reference(a Spring Boot 4.1 application with Spring Data JDBC and real@Transactionalservices) andtest/transaction_matrixrun the same 304 scenarios (the propagation matrix; rollback rules; read only; dirty read, non-repeatable read, phantom, lost update and write skew at five isolation levels, a deadlock;FOR UPDATE,FOR SHARE,NOWAIT,SKIP LOCKED, lock timeouts,@Versionraces; timeouts), two transactions side by side, and the CI job "Data compared with Spring Data" compares the outcomes. The deliberate differences are listed in the README ("Compared with Spring").
SQL databases #
spring.datasource.*andspring.datasources.<name>.*make a pooledDataSource(spring.datasource.pool.*), aDataSourceTransactionManager,JdbcTemplate,NamedParameterJdbcTemplateandJdbcClientbeans for the primary database and for each named one (qualified, with fallbacks); repositories use thejdbcstore once a primary data source exists. The driver is chosen by the URL's scheme among theJdbcDriverbeans the generator writes for the driver packages the application lists.- SQLite through the application's
sqlite3package: transactions, savepoints, one statement at a time per connection, error translation (duplicate key, constraint, busy, wrong SQL),spring.datasource.sqlite.busy-timeoutandbase-directory.spring.datasource.sqlite.isolate=trueopens each connection inside a worker isolate, so synchronous calls no longer hold the caller's isolate (Flutter's UI, a server's event loop); transactions, streams and errors behave as on the caller's isolate, and a worker that dies fails the pending statements with aSqlError(connectionLost) instead of hanging. - PostgreSQL through the application's
postgrespackage:spring.datasource.url=postgresql://user:password@host:5432/db?sslmode=require(alsojdbc:postgresql://…);sslmode(defaultprefer,verify-caandverify-fullcheck the certificate and host name),connect_timeout,application_nameandcurrentSchemafrom the URL orspring.datasource.postgresql.*, any other parameter an error. Values are bound ($1,$2), a generated id isBIGINT GENERATED BY DEFAULT AS IDENTITYread back withRETURNING, every isolation level (read uncommitted runs as read committed) andREAD ONLY, savepoints, statement timeouts cancelled on the server, transactional DDL, an advisory lock for the migrations of several instances.@LockisFOR UPDATE/FOR SHAREwithNOWAIT,SKIP LOCKEDandSET LOCAL lock_timeout. Errors by SQL state (23505DuplicateKeyException,40001CannotSerializeTransactionException,40P01DeadlockLoserDataAccessException,55P03CannotAcquireLockException,57014QueryTimeoutException, wrong SQL, lost connections...) with the server's message, never a bound value, theDETAILline or a password. - The schema is ready before any use: the data source holds back its first connection until the migrations are applied and the entities checked, so no repository,
JdbcTemplate, runner or@PostConstructsees an unmigrated database.spring.sql.init.*scripts, migrations (lib/resources/db/migration/V<n>__<name>.sql,R__<name>.sql,spring.sql.migration.*, history inschema_history, checksums, one instance migrates while the others wait),spring.data.jdbc.schema=validate(default; also foreign keys and unique keys) /create/create-drop/none. The names of Flyway's settings (spring.flyway.*) are not used; they stop the start and namespring.sql.migration.*. - The JDBC layer:
PooledDataSource,JdbcTemplate,NamedParameterJdbcTemplate(:name, lists expanded forIN),JdbcClient,RowMapper,DataClassRowMapper,ResultSetExtractor;JdbcConnection.updateand the templates'updateWithKeyHoldertake an optionalkeyColumn(PostgreSQL's returned key; SQLite ignores it). spring.jdbc.template.query-timeout,spring.data.jdbc.show-sql,format-sql,show-parameters,slow-query-threshold; SQL logs after Spring's loggers (spring.jdbc.core.JdbcTemplateat debug, bound values at trace inStatementCreatorUtils, masked for names that look secret), slow statements at warn.- Startup failures are explained (
APPLICATION FAILED TO START): a URL no driver handles (which package to add), two drivers for one scheme, a failed migration, a schema that does not fit the entities, a database that cannot be reached. - In an application that also depends on
spring_boot_starter_web, the generator adds aHandlerExceptionResolver(the beandataAccessExceptionResolver, replaced by a bean of that name):OptimisticLockingFailureExceptionandDuplicateKeyExceptionare answered with a 409 andEmptyResultDataAccessExceptionwith a 404, as aProblemDetailwith a fixed detail (never the exception's message); other data errors stay a 500.
Testing #
@DataTest(Spring's@DataJdbcTest): a test class that runs on the repositories of the application only (its data beans and the starter's auto-configuration, not the services, controllers, runners or other starters). A configured SQL database is replaced by an in-memory SQLite one on a pool of one connection (replaceDatabase: falsekeeps it), with the migrations or scripts applied or, without them, the tables created from the entities. Each test, with its@BeforeEachand@AfterEachmethods, runs in a transaction rolled back afterwards;@Commit()and@Rollback(false)commit, on a test, a class or a@Nestedgroup;@DataTest(include: [...])adds beans by type and@DataTest(transactionManager: 'name')runs the tests in that one manager's transaction.package:spring_boot_starter_data/testing.darthasDataTestSetup,DataTestSlice,TestDatabaseandrunInTestTransactionfor the generated test file and for tests written as functions (the starter does not depend onspring_boot_starter_test). Documented indocs/testing.md, section 19.@DataTest(replaceDatabase: false)with a@Container() @ServiceConnection()PostgresContainer(spring_boot_starter_test) runs the repositories on that PostgreSQL with the migrations applied and every test rolled back.- The starter's own tests tagged
postgresrun onSPRING_TEST_POSTGRES_URLwhen it is set (CI's service container) and otherwise on aPostgresContainereach test file starts, skipped without Docker.
Security (the 0.6.0 security check) #
- Driver settings are read through the environment:
SPRING_DATASOURCE_POSTGRESQL_SSLMODE=verify-full(and every otherpostgresql.*andsqlite.*key, for the primary and the named data sources) is no longer ignored. Each driver lists its keys (JdbcDriver.propertyNames); a name the driver does not know is still refused, in a file, an argument or a variable. - Values bound by
save, derived queries,deleteand the batch ofNamedParameterJdbcTemplatecarry the column they are bound to, so the trace log (andshow-parametersat info) maskspassword,token,secret,key... values as it does a named@Queryparameter (BoundParameters); a value bound without a name is masked at info and shown at trace. - A
SortorPageableof a@Queryis checked against what the result is mapped from: an entity's properties and columns, a DTO's properties (sorted by their snake-case columns) and the entity's; an unknown name is aPropertyReferenceException(400 through the web glue), not a database error. A single column, aMapor arowMapperresult keeps the quoted identifier, and the database decides. - A password (or any secret-looking parameter:
sslpassword,token...) in the query string of the URL is masked in the pool's log lines and in every message about a URL. A name counts as secret when it containspassword,passwd,passphrase,secret,credential,token,apikey,privatekey,accesskey,signingkeyorencryptionkey(case and separators ignored:sslpassword,dbPassword,apitoken,clientsecret,privatekey), or has the wordpwdorkey;keyword,monkey,tokenizerandsecretarydo not. - A sort property from a request is a
PropertyReferenceException(anInvalidDataAccessApiUsageException) whose message escapes the name (a line break cannot forge a log line); the web glue answers it, and anInvalidPagingRequestException, with a 400 and a fixed detail, logged at debug. PageRequest.ofrefuses a page and size whose offset would pass 2^53 - 1 (it used to wrap around to another page),Direction.fromStringthrows the same exception, and the stores refuse a negative offset instead of dropping it.@Version int? versionis new only whennull(Spring's rule forLong); a non-nullableintis still new at 0. A row stored at version 0 can be updated.sslmode=preferlogs a warning, once for each database, when it falls back to plain text.- A synchronous
runApplication(args)with a SQL data source fails with a report that says to useawait runApplicationAsync(args)(the schema is set up asynchronously).