Skip to content

Commit

Permalink
Usecase fix for insert that do not auto-generate a primary key (#7)
Browse files Browse the repository at this point in the history
* usecase fix for insert that do not auto-generate a primary key

* commit badge

* Simplify BaseRepository, add more complex test cases

* commit badge

* better readme.md

* Update readme

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
martin-jamszolik and github-actions[bot] authored Dec 18, 2022
1 parent b60d4dc commit 9db36ce
Show file tree
Hide file tree
Showing 13 changed files with 342 additions and 34 deletions.
2 changes: 1 addition & 1 deletion .github/badges/branches.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion .github/badges/jacoco.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 50 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Good Enough JDBC

[![Java CI with Gradle](https://github.com/martin-jamszolik/goodenough-jdbc/actions/workflows/gradle.yml/badge.svg)](https://github.com/martin-jamszolik/goodenough-jdbc/actions/workflows/gradle.yml)

[![Gradle CI](https://github.com/martin-jamszolik/goodenough-jdbc/actions/workflows/gradle.yml/badge.svg)](https://github.com/martin-jamszolik/goodenough-jdbc/actions/workflows/gradle.yml)
[![Coverage](.github/badges/jacoco.svg)](jacoco.svg)
[![branches](.github/badges/branches.svg)](branches.svg)

## Why?
Have you tried the latest/greatest ORM frameworks out there yet?
Expand All @@ -12,7 +12,7 @@ Most of them want to remove you from the RDBMS layer as much as possible. Some u
to generate the model in runtime, some are statically typed and require us to define the schema
in the application layer so that a rich DSL layer can be used.

Reflecting on this entire space, you may find yourself needing to be close to SQL and all that
Reflecting on this, you may find yourself needing to be close to SQL and all that
it has to offer in flexibility and transparency. Tailor each query for optimal retrieval, using
your knowledge of to put together a performant query. At the same time, have a good enough api to
help you with the boring,silly stuff. Just enough to help with the needed
Expand All @@ -28,13 +28,54 @@ prolific frameworks out there, which is `spring-jdbc`. An already slim, low leve
Between `spring-jdbc` and Springs next flagship library `spring-data`,
this little project sits right in between. We don't do any meta-programming (autogenerate interfaces),
we don't generate any clever queries. Instead, we help with the most boring parts and leave the power
and flexibility and a level of complexity to you. This project will not protect you from doing silly stuff,
but, with some good guidelines, best practices and test cases, you may find this library useful.
and flexibility and a level of complexity to you. With some good guidelines, best practices and test cases, you may find this library useful.


## Details

Best way to start, is to look at one of our test cases.
[ProposalRepositoryTest](src/test/java/org/viablespark/persistence/ProposalRepositoryTest.java)
A repository pattern demonstrating common use cases with entity storage
and foreign key relationship.
### 1) The Entity

First major component is the Entity:
```java
@PrimaryKey("t_key")
public class Task extends Model {

@Named("sc_name")
public String getName() {
return name;
}

@Ref
public Proposal getProposal(){
return proposal;
}
}
```

By convention, if your entity matches snake case names, `@Named` is not necessary. But in the real world
that is rarely true. Extending `Model` makes it convenient, but your entity might already be inheriting
from another class, so use `implements Persistable` instead.

### 2) The Repository

`BaseRepository` does most of the work to help you `create`, `remove`, `update`, `delete`, `list`.
Beyond that, extend the class and define your desired helper methods for additional queries and composites.

### 3) The Mapper

Most mapping needs can be delegated over to `PersistableRowMapper`. Only if you want to take full control of
how every column, field and foreign relationships is mapped, you will want to implement
`PersistableMapper` and go from there. For an advanced example of this, see
[ProposalMapper](src/test/java/org/viablespark/persistence/ProposalMapper.java)


### Test Cases

Have a look at the various Test Cases for common uses typically found in a real scenarios.

| Example | Reference |
|---------------------------------|:--------------------------------------------------------------------------------------------------------|
| Composite Repository (Line 74+) | [ProposalTaskRepositoryTest](src/test/java/org/viablespark/persistence/ProposalTaskRepositoryTest.java) |
| Many to One Entity Mapping | [ProposalTaskMapper](src/test/java/org/viablespark/persistence/ProposalTaskMapper.java) |
| Simple Repository Example | [ProposalRepositoryTest](src/test/java/org/viablespark/persistence/ProposalRepositoryTest.java) |

25 changes: 23 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ plugins {
}

group = "org.viablespark"
version = "1.0"
version = "1.2.0"

repositories {
mavenCentral()
Expand All @@ -43,6 +43,7 @@ java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
withSourcesJar()
}


Expand Down Expand Up @@ -74,10 +75,30 @@ publishing {
create<MavenPublication>("maven") {
groupId = "org.viablespark"
artifactId = "goodenough-jdbc"
version = "1.0"
version = "1.2.0"

from(components["java"])
pom {
description.set("Good Enough JDBC")
scm {
connection.set("scm:git:git@github.com:martin-jamszolik/goodenough-jdbc.git")
url.set("https://github.com/martin-jamszolik/goodenough-jdbc")
}
licenses {
license {
name.set("The Apache License, Version 2.0")
url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
}
}
developers {
developer {
id.set("martin-jamszolik")
name.set("Martin Jamszolik")
}
}
}
}

}
}

Expand Down
24 changes: 10 additions & 14 deletions src/main/java/org/viablespark/persistence/BaseRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,26 @@ public BaseRepository(JdbcTemplate db) {
jdbc = db;
}

public Key save(E entity) throws SQLException {
public Optional<Key> save(E entity) throws SQLException {

if (entity.isNew()) {
SqlClause withInsert = WithSql.getSQLInsertClause(entity);
String sql = "INSERT INTO " + deriveEntityName(entity.getClass()) + " " + withInsert.getClause();
KeyHolder key = execWithKey(sql, withInsert.getValues());
entity.setKey(Key.of(entity.getClass()
.getAnnotation(PrimaryKey.class)
.value(), key.getKey().longValue()));
return entity.getKey();
if ( key.getKeys() != null ) {
entity.setKey(Key.of(entity.getClass()
.getAnnotation(PrimaryKey.class)
.value(), key.getKey().longValue()));
}
return Optional.ofNullable(entity.getKey());
}

SqlClause withUpdate = WithSql.getSQLUpdateClause(entity);
String sql = "UPDATE " + deriveEntityName(entity.getClass()) + " "
+ withUpdate.getClause();
jdbc.update(sql, withUpdate.getValues());

return entity.getKey();
return Optional.ofNullable(entity.getKey());
}

public void delete(E entity) {
Expand All @@ -75,18 +77,13 @@ public Optional<E> get(Key key, Class<E> cls) throws NoSuchElementException {
return res;
}

public List<E> query(SqlQuery query, Class<E> cls) {
public List<E> queryEntity(SqlQuery query, Class<E> cls) {
return jdbc.query(
"SELECT " + WithSql.getSQLSelectClause(cls, query.getPrimaryKeyName())
+ " FROM " + deriveEntityName(cls) + " "
+ query.sql(), new PersistableRowMapper<>(cls), query.values());
}

public List<E> rowQuery(SqlQuery query, PersistableMapper<E> mapper) {
return jdbc.query(query.sql(), mapper, query.values());
}

public List<E> rowSetQuery(SqlQuery query, PersistableMapper<E> mapper) {
public List<E> query(SqlQuery query, PersistableMapper<E> mapper) {
SqlRowSet rs = jdbc.queryForRowSet(query.sql(), query.values());
List<E> list = new ArrayList<>();
while (rs.next()) {
Expand All @@ -95,7 +92,6 @@ public List<E> rowSetQuery(SqlQuery query, PersistableMapper<E> mapper) {
return list;
}


private KeyHolder execWithKey(final String sql, final Object... args) {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbc.update(
Expand Down
16 changes: 16 additions & 0 deletions src/test/java/org/viablespark/persistence/Proposal.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
import org.viablespark.persistence.dsl.Ref;
import org.viablespark.persistence.dsl.Skip;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;


@Named("est_proposal")
Expand All @@ -36,6 +38,8 @@ public class Proposal extends Model {

private Contractor contractor;

private List<ProposalTask> tasks = new ArrayList<>();

public Proposal() {
}

Expand Down Expand Up @@ -107,4 +111,16 @@ public Contractor getContractor() {
public void setContractor(Contractor contractor) {
this.contractor = contractor;
}

@Skip
public List<ProposalTask> getTasks() {
return tasks;
}

public void setTasks(List<ProposalTask> tasks) {
this.tasks = tasks;
}
public void addTask(ProposalTask task){
tasks.add(task);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,12 @@ public void testSave() throws Exception {

proposal.setContractor(new Contractor("sc_key",1L)); //foreign-key

Key key = repository.save(proposal);
Optional<Key> optionalKey = repository.save(proposal);

logger.info("Entity Saved, let's check valid keys");

assertNotNull(key);
assertTrue(optionalKey.isPresent());
var key = optionalKey.get();
assertEquals(1, key.getCount());
assertNotNull(key.getPrimaryKey());
assertEquals("pr_key",key.getPrimaryKey().key);
Expand All @@ -80,8 +81,8 @@ public void testSave() throws Exception {
var found = foundOption.get();
found.setDistance(567);
found.setPropName("Saved again");
var savedKey = repository.save(found);
assertEquals(key,savedKey);
var keyOption = repository.save(found);
keyOption.ifPresent( k -> assertEquals(key,k) );
repository.get(key,Proposal.class)
.ifPresent( f -> assertEquals("Saved again",f.getPropName()));
}
Expand All @@ -104,7 +105,7 @@ public void testGet() {
@Test
public void testQuery() {

List<Proposal> results = repository.query(SqlQuery
List<Proposal> results = repository.queryEntity(SqlQuery
.where(Pair.of("dist >= ?", 10))
.primaryKey("pr_key"),
Proposal.class);
Expand All @@ -118,7 +119,7 @@ public void testQuery() {
@Test
public void testRowQuery(){
var mapper = new ProposalMapper();
var results = repository.rowQuery(
var results = repository.query(
SqlQuery.asRawSql("select * from est_proposal p " +
"INNER JOIN contractor c on (c.sc_key = p.sc_key) "+
"where dist > 0"),
Expand All @@ -131,7 +132,7 @@ public void testRowQuery(){
@Test
public void testRowSetQuery() {

List<Proposal> results = repository.rowSetQuery(SqlQuery
List<Proposal> results = repository.query(SqlQuery
.asRawSql("select * from est_proposal"),
(rowSet,row) -> {
Proposal e = new Proposal();
Expand Down
42 changes: 42 additions & 0 deletions src/test/java/org/viablespark/persistence/ProposalTask.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2023 the original author or authors.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and limitations under the License.
*/

package org.viablespark.persistence;

import org.viablespark.persistence.dsl.PrimaryKey;
import org.viablespark.persistence.dsl.Ref;

@PrimaryKey("t_key")
public class ProposalTask extends Model {

private Task task;
private Proposal proposal;

@Ref
public Task getTask() {
return task;
}

public void setTask(Task task) {
this.task = task;
}

@Ref
public Proposal getProposal() {
return proposal;
}

public void setProposal(Proposal proposal) {
this.proposal = proposal;
}
}
49 changes: 49 additions & 0 deletions src/test/java/org/viablespark/persistence/ProposalTaskMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2023 the original author or authors.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and limitations under the License.
*/

package org.viablespark.persistence;

import org.springframework.jdbc.support.rowset.SqlRowSet;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class ProposalTaskMapper implements PersistableMapper<ProposalTask> {
private final Map<Key, Proposal> proposals = new LinkedHashMap<>();
private final Map<Key, Task> tasks = new LinkedHashMap<>();

@Override
public ProposalTask mapRow(SqlRowSet rs, int rowNum) {
var proposalTask = new PersistableRowMapper<>(ProposalTask.class).mapRow(rs, rowNum);
if (rs.getObject("pr_key") != null) {
var prop = proposals.computeIfAbsent(Key.of("pr_key", rs.getLong("pr_key")),
pr_key -> new PersistableRowMapper<>(Proposal.class).mapRow(rs, rowNum)
);
proposalTask.setProposal(prop);
prop.addTask(proposalTask);
}
if (rs.getObject("t_key") != null) {
var task = tasks.computeIfAbsent(Key.of("t_key", rs.getLong("t_key")),
t_key -> new PersistableRowMapper<>(Task.class).mapRow(rs, rowNum)
);
proposalTask.setTask(task);
}
return proposalTask;
}

public List<Proposal> getProposals() {
return new ArrayList<>(proposals.values());
}
}
Loading

0 comments on commit 9db36ce

Please sign in to comment.