diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index f5179a5..abc8b4a 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -1 +1 @@ -coverage98.1% \ No newline at end of file +coverage98.8% \ No newline at end of file diff --git a/README.md b/README.md index 7ec3dab..cfb709c 100644 --- a/README.md +++ b/README.md @@ -5,30 +5,29 @@ [![branches](.github/badges/branches.svg)](branches.svg) ## Why? -Have you tried the latest/greatest ORM frameworks out there yet? +Have you tried the latest/greatest ORM frameworks out there yet? -There are more than I can count, each one of them makes some kind of promise to you. Most of them want to remove you from the RDBMS layer as much as possible. Some use meta-programming 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, you may find yourself needing to be close to SQL and all that +Reflecting on this, you may find yourself needing to stay 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 -insert,update and simple select statements. Find that middle ground of convention and flexibility. -How low level can we stay and still be productive with RDBMS? This little library was born from -having to work with so many frameworks, between `spring-data` and `rails` and many other -specialized libraries. +your knowledge to put together a performant query. At the same time, have a good enough api to +help you with the boring stuff. Just enough to help with the needed +insert, update and simple select statements. Find that middle ground of convention and flexibility. +How low level can we stay and still be productive with RDBMS? This little library was created as a +reflection on other frameworks I worked with `spring-data`, `rails` and the likes, among others. ## How? -Clearly, we start of with the fact that we are on the JVM. Taking advantage of one of the most +Starting of with the fact that we are on the JVM and using Java. Taking advantage of one of the most prolific frameworks out there, which is `spring-jdbc`. An already slim, low level library. 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. 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 @@ -66,7 +65,9 @@ Beyond that, extend the class and define your desired helper methods for additio 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) +[ProposalMapper](src/test/java/org/viablespark/persistence/ProposalMapper.java) This is the most laborious +part of the library, queries will need to match entities fields, so often you will have to spell out +the select statement with `column as renamed`. A tradeoff I can live with to retain fine control. ### Test Cases diff --git a/build.gradle.kts b/build.gradle.kts index 68b9cee..77d855d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,7 +18,7 @@ plugins { } group = "org.viablespark" -version = "1.3.0" +version = "1.4.0" repositories { mavenCentral() @@ -75,7 +75,7 @@ publishing { create("maven") { groupId = "org.viablespark" artifactId = "goodenough-jdbc" - version = "1.3.0" + version = "1.4.0" from(components["java"]) pom { diff --git a/src/main/java/org/viablespark/persistence/BaseRepository.java b/src/main/java/org/viablespark/persistence/BaseRepository.java index f778a75..dd862c3 100644 --- a/src/main/java/org/viablespark/persistence/BaseRepository.java +++ b/src/main/java/org/viablespark/persistence/BaseRepository.java @@ -59,18 +59,18 @@ public Optional save(E entity) throws SQLException { public void delete(E entity) { String sql = "DELETE FROM " + deriveEntityName(entity.getClass()) - + " WHERE " + entity.getKey().getPrimaryKey().key + "=? "; + + " WHERE " + entity.getKey().primaryKey().getKey() + "=? "; - jdbc.update(sql, entity.getKey().getPrimaryKey().value); + jdbc.update(sql, entity.getKey().primaryKey().getValue()); } public Optional get(Key key, Class cls) throws NoSuchElementException { List list = jdbc.query( - "SELECT " + WithSql.getSQLSelectClause(cls, key.getPrimaryKey().key) + "SELECT " + WithSql.getSQLSelectClause(cls, key.primaryKey().getKey()) + " FROM " + deriveEntityName(cls) - + " WHERE " + key.getPrimaryKey().key + "=?", + + " WHERE " + key.primaryKey().getKey() + "=?", new PersistableRowMapper<>(cls), - key.getPrimaryKey().value); + key.primaryKey().getValue()); Optional res = list.stream().findFirst(); res.ifPresent(e -> e.setKey(key)); diff --git a/src/main/java/org/viablespark/persistence/Key.java b/src/main/java/org/viablespark/persistence/Key.java index cba36d0..15a4c39 100644 --- a/src/main/java/org/viablespark/persistence/Key.java +++ b/src/main/java/org/viablespark/persistence/Key.java @@ -24,8 +24,11 @@ public class Key implements Serializable { private final Map> keys = new LinkedHashMap<>(); + public Key() { + } + public Key(Pair primaryKey) { - keys.put(primaryKey.key, primaryKey); + keys.put(primaryKey.getKey(), primaryKey); } public static Key of(String key, Long value){ @@ -47,7 +50,7 @@ public Key add(String name, Long key) { return this; } - public int getCount() { + public int count() { return keys.size(); } @@ -55,6 +58,10 @@ public Collection> getKeys() { return keys.values(); } + public void setKeys(Collection> _keys){ + _keys.forEach( pair -> keys.put(pair.getKey(),pair)); + } + public Pair getAt(int index) { int counter=0; for (Entry> entry : keys.entrySet()) { @@ -71,7 +78,7 @@ public Long getKey(String name) { if( found == null ){ throw new RuntimeException("Key " + name + " not found. Other keys?" + this); } - return found.value; + return found.getValue(); } public Optional> contains(String strKey) { @@ -88,11 +95,11 @@ public boolean equals(Object other) { return false; } - if (((Key) other).getCount() != getCount()) { + if (((Key) other).count() != count()) { return false; } - for (int i = 0; i < getCount(); i++) { + for (int i = 0; i < count(); i++) { if (!getAt(i).equals(((Key) other).getAt(i))) { return false; } @@ -104,13 +111,13 @@ public boolean equals(Object other) { @Override public int hashCode() { int hash = 0; - for (int i = 0; i < getCount(); i++) { + for (int i = 0; i < count(); i++) { hash += getAt(i).hashCode(); } return hash; } - public Pair getPrimaryKey() { + public Pair primaryKey() { return getAt(0); } @@ -118,7 +125,7 @@ public Pair getPrimaryKey() { public String toString() { StringBuilder str = new StringBuilder("Key(s) "); for (Pair key : keys.values()) { - str.append(key.key).append("=").append(key.value).append(" "); + str.append(key.getKey()).append("=").append(key.getValue()).append(" "); } return str.toString(); } diff --git a/src/main/java/org/viablespark/persistence/Pair.java b/src/main/java/org/viablespark/persistence/Pair.java index 62d68c8..fb802f3 100644 --- a/src/main/java/org/viablespark/persistence/Pair.java +++ b/src/main/java/org/viablespark/persistence/Pair.java @@ -17,14 +17,33 @@ public class Pair { - public final T key; - public final Y value; + private T key; + private Y value; public Pair(T key, Y value) { this.key = key; this.value = value; } + public Pair() { + } + + public T getKey() { + return key; + } + + public void setKey(T key) { + this.key = key; + } + + public Y getValue() { + return value; + } + + public void setValue(Y value) { + this.value = value; + } + public static Pair of(T key, Y value) { return new Pair<>(key, value); } diff --git a/src/main/java/org/viablespark/persistence/Persistable.java b/src/main/java/org/viablespark/persistence/Persistable.java index d32783f..5614cd8 100644 --- a/src/main/java/org/viablespark/persistence/Persistable.java +++ b/src/main/java/org/viablespark/persistence/Persistable.java @@ -20,7 +20,7 @@ public interface Persistable { void setKey(Key key); default Long getId() { - return getKey().getPrimaryKey().value; + return getKey().primaryKey().getValue(); } default boolean isNew() { diff --git a/src/main/java/org/viablespark/persistence/dsl/SqlQuery.java b/src/main/java/org/viablespark/persistence/dsl/SqlQuery.java index 95d1172..9e60c14 100644 --- a/src/main/java/org/viablespark/persistence/dsl/SqlQuery.java +++ b/src/main/java/org/viablespark/persistence/dsl/SqlQuery.java @@ -81,20 +81,20 @@ public String sql() { } Optional whereResult = where.stream() - .map(p -> p.key) + .map(Pair::getKey) .reduce((acc, a) -> "" + acc + " AND " + a); String whereStr = whereResult.map(s -> "WHERE " + s).orElse(" "); Optional condResult = conditions.stream() - .map(p -> p.key) + .map(Pair::getKey) .reduce((acc, a) -> "" + acc + " " + a); String condStr = condResult.orElse(""); - String orderStr = orderBy != null ? " ORDER BY " +orderBy.key + " " + orderBy.value.toString() : ""; + String orderStr = orderBy != null ? " ORDER BY " +orderBy.getKey() + " " + orderBy.getValue().toString() : ""; - String limitStr = limit != null ? " LIMIT "+limit.key : ""; + String limitStr = limit != null ? " LIMIT "+limit.getKey() : ""; return whereStr + condStr + orderStr + limitStr; } @@ -104,8 +104,8 @@ public Object[] values() { return rawValues; } List list = new LinkedList<>(); - list.addAll(where.stream().map(p -> p.value).collect(Collectors.toList())); - list.addAll(conditions.stream().map(p -> p.value).collect(Collectors.toList())); + list.addAll(where.stream().map(Pair::getValue).collect(Collectors.toList())); + list.addAll(conditions.stream().map(Pair::getValue).collect(Collectors.toList())); return list.toArray(); } diff --git a/src/main/java/org/viablespark/persistence/dsl/WithSql.java b/src/main/java/org/viablespark/persistence/dsl/WithSql.java index 51ad2b8..fc511f0 100644 --- a/src/main/java/org/viablespark/persistence/dsl/WithSql.java +++ b/src/main/java/org/viablespark/persistence/dsl/WithSql.java @@ -61,9 +61,9 @@ public static SqlClause getSQLUpdateClause(Persistable entity) throws SQLExcepti answers.add(deriveValue(m, entity)); } sql.deleteCharAt(sql.length() - 1); // remove last comma. - Pair primaryKey = entity.getKey().getPrimaryKey(); - sql.append(" WHERE ").append(primaryKey.key).append("=?"); - answers.add(primaryKey.value); + Pair primaryKey = entity.getKey().primaryKey(); + sql.append(" WHERE ").append(primaryKey.getKey()).append("=?"); + answers.add(primaryKey.getValue()); //sql = new StringBuilder(sql.toString().replaceAll(".$", "")); return new SqlClause(sql.toString(), answers.toArray()); @@ -108,7 +108,7 @@ private static String deriveName(Method m, Persistable entity) throws Exception Optional refOption = getAnnotation(m, entity.getClass(), Ref.class); if (refOption.isPresent()) { Persistable refObj = (Persistable) m.invoke(entity); - return refObj.getKey().getPrimaryKey().key; + return refObj.getKey().primaryKey().getKey(); } String name = m.getName().substring(3); @@ -120,7 +120,7 @@ private static Object deriveValue(Method m, Persistable entity) throws Exception Optional refOption = getAnnotation(m, entity.getClass(), Ref.class); if (refOption.isPresent()) { Persistable refObj = (Persistable) m.invoke(entity); - return refObj.getKey().getPrimaryKey().value; + return refObj.getKey().primaryKey().getValue(); } return m.invoke(entity); } diff --git a/src/test/java/org/viablespark/persistence/KeyTest.java b/src/test/java/org/viablespark/persistence/KeyTest.java index 5c10f7e..7f8d794 100644 --- a/src/test/java/org/viablespark/persistence/KeyTest.java +++ b/src/test/java/org/viablespark/persistence/KeyTest.java @@ -14,16 +14,15 @@ package org.viablespark.persistence; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; -import java.util.Map; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; -import org.viablespark.persistence.Key; -import org.viablespark.persistence.Pair; import static org.junit.jupiter.api.Assertions.*; import static org.springframework.test.util.AssertionErrors.assertTrue; @@ -43,7 +42,6 @@ public void setupKey(){ key = Key.of("primary", 123L); key.add("second", 345L); key.add("third", 567L); - } @Test @@ -52,13 +50,27 @@ public void testConstructorWithMap(){ map.put("primary",123L); map.put("second",345L); Key key = new Key(map); - assertEquals(123L,key.getPrimaryKey().value); + assertEquals(123L,key.primaryKey().getValue()); assertEquals(123L,key.getKey("primary")); } + @Test + public void testSetKeys(){ + List> pairs = new ArrayList<>(); + pairs.add(Pair.of("anotherKey",2333L)); + Pair p = new Pair<>(); + p.setKey("yetAgain"); + p.setValue(1243L); + pairs.add(p); + key.setKeys(pairs); + + assertEquals(Pair.of("anotherKey",2333L).getValue(), key.getKey("anotherKey") ); + assertEquals(p, key.getAt(4) ); + } @Test public void testOf() { - assertEquals(key.getPrimaryKey(), Key.of("primary", 123L).getPrimaryKey()); + assertEquals(key.primaryKey(), Key.of("primary", 123L).primaryKey()); + assertEquals(key.primaryKey(), new Key().add("primary",123L).primaryKey() ); } @Test @@ -73,7 +85,7 @@ public void testAdd_String_Number() { @Test public void testGetCount() { - assertTrue("Should count as 3", key.getCount() ==3); + assertTrue("Should count as 3", key.count() ==3); } @@ -96,10 +108,10 @@ public void testGetAt() { key.add("third", 567L); key.add("primary", 234L); - assertEquals(key.getPrimaryKey(), key.getAt(0)); - assertEquals("third", key.getAt(2).key); - assertEquals( "second", key.getAt(1).key); - assertEquals(3, key.getCount()); + assertEquals(key.primaryKey(), key.getAt(0)); + assertEquals("third", key.getAt(2).getKey()); + assertEquals( "second", key.getAt(1).getKey()); + assertEquals(3, key.count()); assertNull(key.getAt(23)); } @@ -140,7 +152,7 @@ public void testEquals() { @Test public void testGetPrimaryKey() { - assertEquals(Pair.of("primary", 123L),key.getPrimaryKey()); + assertEquals(Pair.of("primary", 123L),key.primaryKey()); } @Test diff --git a/src/test/java/org/viablespark/persistence/NoteRepositoryTest.java b/src/test/java/org/viablespark/persistence/NoteRepositoryTest.java index f1a3a1a..e0bf009 100644 --- a/src/test/java/org/viablespark/persistence/NoteRepositoryTest.java +++ b/src/test/java/org/viablespark/persistence/NoteRepositoryTest.java @@ -19,7 +19,6 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.viablespark.persistence.dsl.WithSql; import java.math.BigDecimal; @@ -43,16 +42,16 @@ public void testInsertNote() throws Exception { progress.save(entity.getProgress()); var keyOption = repository.save(entity); - keyOption.ifPresent( key -> assertEquals(2L, key.getPrimaryKey().value)); - assertEquals( 2L,entity.getProgress().getKey().getPrimaryKey().value); - assertEquals( "id",entity.getProgress().getKey().getPrimaryKey().key); + keyOption.ifPresent( key -> assertEquals(2L, key.primaryKey().getValue())); + assertEquals( 2L,entity.getProgress().getKey().primaryKey().getValue()); + assertEquals( "id",entity.getProgress().getKey().primaryKey().getKey()); } @Test public void testSelectNote() throws Exception { var found = repository.get(Key.of("n_key",1L),Note.class); - found.ifPresent( n -> assertEquals(1L, n.getKey().getPrimaryKey().value)); + found.ifPresent( n -> assertEquals(1L, n.getKey().primaryKey().getValue())); found.ifPresent( n -> assertNotNull( n.getProgress().getKey())); } diff --git a/src/test/java/org/viablespark/persistence/PersistableRowMapperTest.java b/src/test/java/org/viablespark/persistence/PersistableRowMapperTest.java index fc91f3d..5f8caf9 100644 --- a/src/test/java/org/viablespark/persistence/PersistableRowMapperTest.java +++ b/src/test/java/org/viablespark/persistence/PersistableRowMapperTest.java @@ -14,13 +14,18 @@ package org.viablespark.persistence; import org.junit.jupiter.api.*; +import org.mockito.Mock; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.support.rowset.SqlRowSet; +import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; +import java.util.concurrent.ConcurrentSkipListMap; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -31,7 +36,6 @@ class PersistableRowMapperTest { private EmbeddedDatabase db; - @Test public void testRowSetMapping(){ var f = new PersistableRowMapper<>(Contractor.class); @@ -44,6 +48,34 @@ public void testRowSetMapping(){ } } + @Test + public void testMapRowMappingUsingResultSet() throws Exception { + + var jdbc = new JdbcTemplate(db); + var f = new PersistableMapper() { + @Override + public Contractor mapRow(SqlRowSet rs, int rowNum) { + Contractor c = new Contractor(); + c.setKey(Key.of("sc_key",rs.getLong("sc_key"))); + c.setName(rs.getString("sc_name")); + c.setContact(rs.getString("contact")); + return c; + } + }; + + var sql = "select sc_key, sc_name as name, contact, phone1, fax, email from contractor c order by sc_key asc"; + + List list = jdbc.query(sql, f); + + try (var statement = db.getConnection() + .prepareStatement(sql)) { + var rs = statement.executeQuery(); + rs.next(); + Contractor entity = f.mapRow(rs,rs.getRow()); + assertEquals(entity.getKey(), list.get(rs.getRow()-1).getKey() ); + } + } + @Test public void testException(){ var f = new PersistableRowMapper<>(Contractor.class); diff --git a/src/test/java/org/viablespark/persistence/ProposalRepositoryTest.java b/src/test/java/org/viablespark/persistence/ProposalRepositoryTest.java index f0f3df4..f2a88ea 100644 --- a/src/test/java/org/viablespark/persistence/ProposalRepositoryTest.java +++ b/src/test/java/org/viablespark/persistence/ProposalRepositoryTest.java @@ -70,9 +70,9 @@ public void testSave() throws Exception { assertTrue(optionalKey.isPresent()); var key = optionalKey.get(); - assertEquals(1, key.getCount()); - assertNotNull(key.getPrimaryKey()); - assertEquals("pr_key",key.getPrimaryKey().key); + assertEquals(1, key.count()); + assertNotNull(key.primaryKey()); + assertEquals("pr_key",key.primaryKey().getKey()); logger.info("Let's update"); @@ -99,7 +99,7 @@ public void testDelete() { public void testGet() { Optional result = repository.get(Key.of("pr_key", 1L), Proposal.class); assertTrue(result.isPresent()); - assertEquals(1L, result.get().getKey().getPrimaryKey().value); + assertEquals(1L, result.get().getKey().primaryKey().getValue()); } @Test diff --git a/src/test/java/org/viablespark/persistence/ProposalTaskRepositoryTest.java b/src/test/java/org/viablespark/persistence/ProposalTaskRepositoryTest.java index 7d94fdd..7a9622e 100644 --- a/src/test/java/org/viablespark/persistence/ProposalTaskRepositoryTest.java +++ b/src/test/java/org/viablespark/persistence/ProposalTaskRepositoryTest.java @@ -15,7 +15,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; @@ -51,7 +50,7 @@ public void testInsertWithPKnoAutoGenerate() throws Exception { var keyOption = repository.save(entity); entity.setKey( entity.getTask().getKey() ); //composite pk - Not auto-generated - keyOption.ifPresent( key -> assertEquals(1L, key.getPrimaryKey().value)); + keyOption.ifPresent( key -> assertEquals(1L, key.primaryKey().getValue())); }