将嵌套的Pojo对象存储为数据库中的单个对象

我使用jackson将json字符串映射到我的HTModel类,这基本上是一个简单的Pojo。

class HTModel{} public class Post extends HTModel { public String id; public String content; public String author; } 

即使这些类嵌套在一起,这也很有效。

 public class Venue extends HTModel { public ArrayList posts; } 

我设置了一个简单的SqlLite模式来缓存和索引这些模型的类型和ID。

我的问题是,如果模型包含其他模型的字段,我不想将整个数据库中的Venue模型存储起来。 ArrayList Venue.posts中的每个post都应单独保存。

什么是最好的方法呢?

在使用JSON创建自己的数据库 – > POJO实现时,我遇到了类似的问题。 这就是我解决这个问题的方法,对我来说效果很好。

我们以Post对象为例。 它需要很容易地表示为JSON对象,并从JSON字符串创建。 此外,它需要能够保存到数据库。 我已根据这两个条件分解了我使用的类的heirachy:

 Post -> DatabaseObject -> JsonObject -> LinkedHashMap 

从最基本的表示开始,一个JsonObject ,它是一个扩展的LinkedHashMap 。 由于其键值映射, Maps可以很好地表示JSON对象。 这是JsonObject类:

 import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; /** * A JsonObject represents a JSON object, which begins and ends * with curly braces '{' '}' and contains key-value pairs separated by a * colon ':'. * 

* In implementation, this is simply an extended LinkedHashMap to * represent key-value pairs and to preserve insert order (which may be * required by some JSON implementations, though is not a standard). *

* Additionally, calling toString() on the JsonObject * will return a properly formatted String which can be posted as * a value JSON HTTP request or response. * @author Andrew * @param the value class to use. Note that all keys for a * JsonObject are Strings */ public class JsonObject extends LinkedHashMap { /** * Creates a new empty JsonObject. */ public JsonObject() { } /** * Creates a new JsonObject from the given HTTP response * String. * @param source HTTP response JSON object * @throws IllegalArgumentException if the given String is not * a JSON object, or if it is improperly formatted * @see JsonParser#getJsonObject(java.lang.String) */ public JsonObject(String source) throws IllegalArgumentException { this(JsonParser.getJsonObject(source)); } /** * Creates a new JsonObject from the given Map. * @param map a Map of key-value pairs to create the * JsonObject from */ public JsonObject(Map map) { putAll(map); } /** * Returns a JSON formatted String that properly represents * this JSON object. *

* This String may be used in an HTTP request or response. * @return JSON formatted JSON object String */ @Override public String toString() { StringBuilder sb = new StringBuilder("{"); Iterator> iter = entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = iter.next(); sb.append(JsonParser.toJson(entry.getKey())); sb.append(':'); V value = entry.getValue(); sb.append(JsonParser.toJson(value)); if (iter.hasNext()) { sb.append(','); } } sb.append("}"); return sb.toString(); } }

简单来说,它只是一个表示JSON对象的LinkedHashMap ,它可以通过调用toString()快速转换为JSON字符串,也可以使用我创建的JsonParser类从JSON字符串创建。

可能如果您已经使用像Jackson这样的JSON解析器,您可以重做一些事情来使用该API。

接下来是PostDatabaseObject ,它提供Postfunction以与数据库通信。 在我的实现中, Database对象只是一个抽象类。 我指定Database如何在其他地方保存DatabaseObjects ,无论是通过JDBC还是通过HTTP的JSON。

请记住,我们使用Map来表示我们的对象。 对于你的Post ,这意味着你有三个“属性”(我称之为文档中的键值):ID,内容和作者。

这是DatabaseObject (剪裁下来)的样子。 请注意save()方法,这是我将回答你的问题的地方。

 import java.text.ParseException; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * The DatabaseObject represents a single row of data from a * specific table within a database. * 

* The object should implement getters and setters for each column, and is * responsible for registering the correct table name and column names, as * well as default values for those columns, in case a default value is * not supported by the database table. *

* The DatabaseObject works with key-value pairs as an * extended LinkedHashMap. It defines one property, * DatabaseObject.ROW_ID which represents the unique * identifier column for a table row. This column should always be an * integer value. (Future implementations may allow for long values, but * Integer.MAX_VALUE is well suited for most databases as a maximum * row count per table). *

* The key and value pairs should be accessed by implementing getter and * setter methods, not by the get and put methods provided by the * LinkedHashMap. This is to ensure proper Class * type casting for each value. *

* A DatabaseObject itself is also an extension of a * JsonObject, and toString() may be called on * it to provide a JSON notated DatabaseObject. *

* When using JSON however, keep in mind that the keys may not correspond * exactly with the table column names, even though that is the recommendation. * The DatabaseObject should be converted back into its * implementing object form and saved when using web services. *

* The parameter T should be set to the class of the implementing * DatabaseObject. This will allow proper class casting when * returning instances of the implementation, such as in the load() * methods. * @param the type of DatabaseObject * @author Andrew */ public abstract class DatabaseObject extends JsonObject implements Cloneable{ /**The property for the row ID*/ public final static String ROW_ID = "rowId"; /** * Creates a new empty DatabaseObject. */ public DatabaseObject() { } /** * {@inheritDoc } *

* This get method will additionally check the Class of * the returned value and cast it if it is a String but * matches another Class type such as a number. * @see #doGet(java.lang.String, boolean) */ @Override public Object get(Object key) { //From here you can specify additional requirements before retrieving a value, such as class checking //This is optional of course, and doGet() calls super.get() return doGet(String.valueOf(key), true); } /** * {@inheritDoc } *

* This get method will additionally check the Class of * the given value and cast it if it is a String but * matches another Class type such as a number. * @see #doPut(java.lang.String, java.lang.Object, boolean) */ @Override public Object put(String key, Object value) { //Like doGet(), doPut() goes through additional checks before returning a value return doPut(key, value, true); } //Here are some example getter/setter methods //DatabaseObject provides an implementation for the row ID column by default /** * Retrieves the row ID of this DatabaseObject. *

* If the row ID could not be found, -1 will be returned. Note that * a -1 may indicate a new DatabaseObject, but it * does not always, since not all Databases support * retrieving the last inserted ID. *

* While the column name might not correspond to "rowId", this * method uses the DatabaseObject.ROW_ID property. All * objects must have a unique identifier. The name of the column * should be registered in the constructor of the object. * @return the DatabaseObject's row ID, or -1 if it is not set */ public int getRowId() { //getProperty(), while not included, simply returns a default specified value //if a value has not been set return getProperty(ROW_ID, -1); } /** * Sets the row ID of this DatabaseObject. *

* Note: this method should rarely be used in implementation! *

* The DatabaseObject will automatically set its row ID when * retrieving information from a Database. If the row ID * is forcibly overriden, this could overwrite another existing row entry * in the database table. * @param rowId the row ID of the DatabaseObject */ public void setRowId(int rowId) { //And again, setProperty() specifies some additional conditions before //setting a key-value pair, but its not needed for this example setProperty(ROW_ID, rowId); } //This is where your question will be answered!! //Since everything in a DatabaseObject is set as a key-value pair in a //Map, we don't have to use reflection to look up values for a //specific object. We can just iterate over the key-value entries public synchronized void save(Database database) throws SaveException { StringBuilder sql = new StringBuilder(); //You would need to check how you want to save this, let's assume it's //an UPDATE sql.append("UPDATE ").append(getTableName()).append(" SET "); Iterator iter = entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = iter.next(); String property = entry.getKey(); Object value = entry.getValue(); if (value instanceof DatabaseObject) { ((DatabaseObject)value).save(); } else if (value instanceof Collection) { for (Object v : (Collection)value) { ((DatabaseObject)value).save(); } } //However many other checks you want, such as Maps, Arrays, etc else { sql.append(property); //Assuming the property equals the column name sql.append("='").append(value).append("'"); } if (iter.hasNext()) { sql.append(", "); } } //getIdColumn() would retrieve which column is being used as the identifier sql.append("WHERE ").append(getIdColumn()).append("=").append(getRowId()); //Finally, take our SQL string and save the value to the Database //For me, I could simply call database.update(sql), and //the abstracted Database object would determine how to //send that information via HTTP as a JSON object //Of course, your save() method would vary greatly, but this is just a quick //and dirty example of how you can iterate over a Map's //key-value pairs and take into account values that are //DatabaseObjects themselves that need to be saved, or //a Collection of DatabaseObjects that need to be saved } /** * Retrieves the name of the database table that this * DatabaseObject pulls its information from. *

* It is recommended to declare a final and static class variable that * signifies the table name to reduce resource usage. * @return name of the database table */ public abstract String getTableName(); }

对于TL; DR版本:

Post是一个DatabaseObject

DatabaseObject是一个JsonObject ,它是一个LinkedHashMap

LinkedHashMap设置了存储键值对的标准。 Key =列名,值=列值。

JsonObject除了提供一种方法将LinkedHashMap打印为JSON字符串之外什么都不做。

DatabaseObject指定有关如何保存到数据库的逻辑,包括值是另一个DatabaseObject ,或者值是否包含DatabaseObject ,例如使用Collections。

^ – 这意味着您只需编写一次“保存”逻辑。 当你调用Post.save()它会保存post。 当你调用Venue.save() ,它会保存场地列(如果有的话),以及ArrayList中的每个Post

为了获得额外的乐趣,这里是PostVenue对象的样子:

 public class Post extends DatabaseObject { //The column names public final static String POST_ID = "PostID"; public final static String CONTENT = "Content"; public final static String AUTHOR = "Author"; public Post() { //Define default values } public int getPostId() { return (Integer)get(POST_ID); } public void setPostId(int id) { put(POST_ID, id); } public String getContent() { return (String)get(CONTENT); } public void setContent(String content) { put(CONTENT, content); } public String getAuthor() { return (String)getAuthor(); } public void setAuthor(String author) { put(AUTHOR, author); } @Override public String getTableName() { return "myschema.posts"; } } 

请注意,我们不再声明类变量,只是存储值的列名。 我们在getter / setter方法中定义变量的类。

 import java.util.ArrayList; import java.util.List; public class Venue extends DatabaseObject { //This is just a key property, not a column public final static String POSTS = "Posts"; public Venue() { setPosts(new ArrayList()); } public List getPosts() { return (List)get(POSTS); } public void setPosts(List posts) { put(POSTS, posts); } public void addPost(Post post) { getPosts().add(post); } @Override public String getTableName() { //Venue doesn't have a table name, it's just a container //In your DatabaseObject.save() method, you can specify logic to //account for this condition return ""; } } 

额外终极TL; DR版本:

使用Map存储变量,而不是在类中定义它们。

创建一个save()方法逻辑,该逻辑迭代Map值并将列值对保存到数据库,同时考虑作为Collections或可保存DatabaseObjects

现在你所要做的就是调用Venue.save() ,你的所有Post对象也会被妥善保存。

使用@JsonIdentityInfo / @JsonIdentityReference (将post序列化为id)和自定义反序列化器(可以通过id读取DB中的post)的可能解决方案:

 public class SerializationTest { static class HTModel{} @JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id") public static class Post extends HTModel { public int id; public String content; public String author; } static class Venue extends HTModel { @JsonIdentityReference(alwaysAsId=true) @JsonDeserialize(using = VenuePostsDeserializer.class) public ArrayList posts; } static class VenuePostsDeserializer extends JsonDeserializer> { @Override public ArrayList deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException { ArrayList posts = new ArrayList(); if (parser.getCurrentToken() != JsonToken.START_ARRAY) { return posts; } parser.nextValue(); try { do { int postId = parser.getIntValue(); // FIXME: fetch the corresponding post from DB instead of creating a stub Post post = new Post(); post.id = postId; post.content = String.format("Post #%d content", postId); post.author = String.format("Post #%d author", postId); posts.add(post); parser.nextValue(); } while(parser.getCurrentToken() != JsonToken.END_ARRAY); } catch (Exception exception) { System.out.println(exception.getMessage()); } return posts; } } public static void main(String[] args) { ObjectMapper mapper = new ObjectMapper(); Venue venue = new Venue() {{ posts = new ArrayList() {{ add(new Post() {{ id = 2; }}); add(new Post() {{ id = 42; }}); }}; }}; try { String serializedVenue = mapper.writeValueAsString(venue); System.out.println("Serialized venue: " + serializedVenue); Venue deserializedVenue = mapper.readValue(serializedVenue, Venue.class); System.out.println("Deserialized venue:"); for (Post post : deserializedVenue.posts) { System.out.println(String.format("Post: id=%d, content=%s, author=%s", post.id, post.content, post.author)); } } catch (Exception exception) { System.out.println(exception.getMessage()); } } } 

输出:

 Serialized venue: {"posts":[2,42]} Deserialized venue: Post: id=2, content=Post #2 content, author=Post #2 author Post: id=42, content=Post #42 content, author=Post #42 author 

编辑 :我将Post.id更改为int (来自String ),以便在示例中使用更简单的反序列化器。