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

时间:2013-11-01 12:35:50

标签: java jackson

我使用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<Post> posts;
}

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

我的问题是,如果模型包含其他模型的字段,我不希望将Venue模型整体存储在数据库中。 ArrayList Venue.posts中的每个帖子都应单独保存。

最好的办法是什么?

2 个答案:

答案 0 :(得分:3)

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

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

Post
  -> DatabaseObject
    -> JsonObject
      -> LinkedHashMap

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

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * A <code>JsonObject</code> represents a JSON object, which begins and ends 
 * with curly braces '{' '}' and contains key-value pairs separated by a 
 * colon ':'.
 * <p>
 * In implementation, this is simply an extended <code>LinkedHashMap</code> to 
 * represent key-value pairs and to preserve insert order (which may be 
 * required by some JSON implementations, though is not a standard).
 * <p>
 * Additionally, calling <code>toString()</code> on the <code>JsonObject</code> 
 * will return a properly formatted <code>String</code> which can be posted as 
 * a value JSON HTTP request or response.
 * @author Andrew
 * @param <V> the value class to use. Note that all keys for a 
 *          <code>JsonObject</code> are <code>Strings</code>
 */
public class JsonObject<V> extends LinkedHashMap<String, V> {

    /**
     * Creates a new empty <code>JsonObject</code>.
     */
    public JsonObject() {

    }
    /**
     * Creates a new <code>JsonObject</code> from the given HTTP response 
     * <code>String</code>.
     * @param source HTTP response JSON object
     * @throws IllegalArgumentException if the given <code>String</code> 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 <code>JsonObject</code> from the given <code>Map</code>.
     * @param map a <code>Map</code> of key-value pairs to create the 
     *          <code>JsonObject</code> from
     */
    public JsonObject(Map<? extends String, ? extends V> map) {
        putAll(map);
    }

    /**
     * Returns a JSON formatted <code>String</code> that properly represents 
     * this JSON object.
     * <p>
     * This <code>String</code> may be used in an HTTP request or response.
     * @return JSON formatted JSON object <code>String</code>
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("{");

        Iterator<Map.Entry<String, V>> iter = entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<String, V> 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字符串,也可以使用{{从JSON字符串创建1}}我创建的类。

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

接下来是JsonParser的内容,Post赋予DatabaseObject功能以与数据库通信。在我的实现中,Post对象只是一个抽象类。我指定Database如何在其他地方保存Database,无论是通过JDBC还是通过HTTP的JSON。

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

以下是Post(修剪过的)的样子。请注意DatabaseObject方法,我将回答您的问题。

save()

对于TL; DR版本:

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 <code>DatabaseObject</code> represents a single row of data from a * specific table within a database. * <p> * 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. * <p> * The <code>DatabaseObject</code> works with key-value pairs as an * extended <code>LinkedHashMap</code>. It defines one property, * <code>DatabaseObject.ROW_ID</code> 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 * <code>Integer.MAX_VALUE</code> is well suited for most databases as a maximum * row count per table). * <p> * The key and value pairs should be accessed by implementing getter and * setter methods, not by the get and put methods provided by the * <code>LinkedHashMap</code>. This is to ensure proper <code>Class</code> * type casting for each value. * <p> * A <code>DatabaseObject</code> itself is also an extension of a * <code>JsonObject</code>, and <code>toString()</code> may be called on * it to provide a JSON notated <code>DatabaseObject</code>. * <p> * 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 <code>DatabaseObject</code> should be converted back into its * implementing object form and saved when using web services. * <p> * The parameter <code>T</code> should be set to the class of the implementing * <code>DatabaseObject</code>. This will allow proper class casting when * returning instances of the implementation, such as in the <code>load()</code> * methods. * @param <T> the type of <code>DatabaseObject</code> * @author Andrew */ public abstract class DatabaseObject<T extends DatabaseObject> extends JsonObject<Object> implements Cloneable{ /**The property for the row ID*/ public final static String ROW_ID = "rowId"; /** * Creates a new empty <code>DatabaseObject</code>. */ public DatabaseObject() { } /** * {@inheritDoc } * <p> * This get method will additionally check the <code>Class</code> of * the returned value and cast it if it is a <code>String</code> but * matches another <code>Class</code> 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 } * <p> * This get method will additionally check the <code>Class</code> of * the given value and cast it if it is a <code>String</code> but * matches another <code>Class</code> 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 <code>DatabaseObject</code>. * <p> * If the row ID could not be found, -1 will be returned. Note that * a -1 <i>may</i> indicate a new <code>DatabaseObject</code>, but it * does not always, since not all <code>Databases</code> support * retrieving the last inserted ID. * <p> * While the column name might not correspond to "rowId", this * method uses the <code>DatabaseObject.ROW_ID</code> property. All * objects must have a unique identifier. The name of the column * should be registered in the constructor of the object. * @return the <code>DatabaseObject's</code> 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 <code>DatabaseObject</code>. * <p> * <b>Note: this method should rarely be used in implementation!</b> * <p> * The <code>DatabaseObject</code> will automatically set its row ID when * retrieving information from a <code>Database</code>. 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 <code>DatabaseObject</code> */ 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<String, Object> iter = entrySet().iterator(); while (iter.hasNext()) { Map.Entry<String, Object> 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 * <code>DatabaseObject</code> pulls its information from. * <p> * 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(); } Post

DatabaseObjectDatabaseObject,是JsonObject

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

LinkedHashMap除了提供一种方法可以将JsonObject打印为JSON字符串。

LinkedHashMap指定了如何保存到数据库的逻辑,包括值是另一个DatabaseObject的情况,或者值是否包含DatabaseObject的情况,例如收藏集

^ - 这意味着您只需编写一次“保存”逻辑。当您致电DatabaseObject时,会保存帖子。当您致电Post.save()时,它会保存会场列(如果有)以及Venue.save()中的每个Post个人。

为了获得额外的乐趣,以下是您的ArrayListPost对象的外观:

Venue

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

public class Post extends DatabaseObject<Post> {

    //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";
    }

}

额外终极TL; DR版本:

使用import java.util.ArrayList; import java.util.List; public class Venue extends DatabaseObject<Venue> { //This is just a key property, not a column public final static String POSTS = "Posts"; public Venue() { setPosts(new ArrayList()); } public List<Post> getPosts() { return (List<Post>)get(POSTS); } public void setPosts(List<Post> 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 ""; } } 存储变量,而不是在班级中定义它们。

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

现在您所要做的就是致电DatabaseObjects,所有Venue.save()个对象也会被妥善保存。

答案 1 :(得分:0)

使用@JsonIdentityInfo / @JsonIdentityReference(将帖子序列化为ID)和自定义反序列化器(可以通过ID读取数据库中的帖子)的可能解决方案:

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<Post> posts;
    }

    static class VenuePostsDeserializer extends JsonDeserializer<ArrayList<Post>> {
        @Override
        public ArrayList<Post> deserialize(JsonParser parser, DeserializationContext context) throws IOException,
        JsonProcessingException {

            ArrayList<Post> posts = new ArrayList<Post>();
            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<Post>() {{
                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),以便在示例中使用更简单的解串器。