【问题标题】:ORM supporting immutable classesORM 支持不可变类
【发布时间】:2011-02-11 12:33:19
【问题描述】:

哪种 ORM 支持不可变类型的域模型?

我想编写如下类(或 Scala 等效类):

class A {
  private final C c; //not mutable

  A(B b) {
     //init c
  }

  A doSomething(B b) {
     // build a new A
  }
}

ORM 必须使用构造函数初始化对象。因此可以在构造函数中检查不变量。对初始化的默认构造函数和字段/设置器访问是不够的,并且会使类的实现复杂化。

应该支持使用集合。如果集合被更改,它应该从用户的角度创建一个副本。 (使旧的集合状态过时。但用户代码仍然可以处理(或至少读取)它。)很像 persistent data structures 的工作。

关于动机的一些话。假设您有一个 FP 风格的域对象模型。现在您想将其保存到数据库中。你是谁做的?您希望以纯函数式风格尽可能多地做,直到邪恶的副作用出现。如果您的域对象模型不是不可变的,您可以例如不在线程之间共享对象。你必须复制、缓存或使用锁。因此,除非您的 ORM 支持不可变类型,否则您在选择解决方案时会受到限制。

【问题讨论】:

    标签: java hibernate orm scala types


    【解决方案1】:

    SORM 是一个新的 Scala ORM,它完全可以满足您的需求。下面的代码比任何文字都更能说明问题:

    // Declare a model:
    case class Artist ( name : String, genres : Set[Genre] )
    case class Genre ( name : String ) 
    
    // Initialize SORM, automatically generating schema:
    import sorm._
    object Db extends Instance (
      entities = Set() + Entity[Artist]() + Entity[Genre](),
      url = "jdbc:h2:mem:test"
    )
    
    // Store values in the db:
    val metal = Db.save( Genre("Metal") )
    val rock = Db.save( Genre("Rock") )
    Db.save( Artist("Metallica", Set() + metal + rock) )
    Db.save( Artist("Dire Straits", Set() + rock) )
    
    // Retrieve values from the db:
    val metallica = Db.query[Artist].whereEqual("name", "Metallica").fetchOne() // Option[Artist]
    val rockArtists = Db.query[Artist].whereEqual("genres.name", "Rock").fetch() // Stream[Artist]
    

    【讨论】:

    • 更新应该如何工作?假设我查询一个不可变对象“a”并执行类似 b = a.map(f: MyType => MyType) 的操作来获取“a”的更新“副本”。现在“a”已经过时,“b”是最新的,我想将“b”存储为“a”的更新。我猜 Db.save(b) 将导致插入,现在“a”和“b”都被存储了。我想告诉 SORM 用“b”代替“a”。理想情况下不处理 id,因为 SORM 希望 id 是数据库内部的(如果你问我,这不是最糟糕的主意)。
    • @user573215 如果val b = a.copy(..) 然后Db.save(b) 将导致适当的行得到更新,而不是插入。这种行为是在Persisted trait 的帮助下实现的,它基本上是用来透明地携带实体的标识信息。
    • 查看您的示例,我看到由可变数据库支持的不可变 Scala 代码。想象一下,发现了一首 Dire Straits 演奏 Metal 的单曲。然后他们将占用数据库中 Artist 表中的多少行?你能问一下 Dire Straits 在发现 Metal 单曲之前演奏了什么,与我们现在知道他们演奏什么相比吗?哪个“不变”版本的 Dire Straits 会获胜,或者他们会成为两个乐队?
    • @GlenPeterson 将不可变值视为数据库中状态的快照。该框架隐式管理与数据库中记录的关联。当您持久保存更新的值时,它会改变数据库中的适当数据,而不是像您怀疑的那样插入重复数据。 Here are the docs on how it actually is achieved.
    【解决方案2】:

    更新:我创建了一个名为JIRM 的项目专注于解决这个问题: https://github.com/agentgt/jirm

    我在使用Spring JDBCJackson Object Mapper 实现我自己的之后才发现这个问题。基本上我只需要一些最低限度的 SQL 不可变对象映射。

    简而言之,我只是使用 Springs RowMapperJackson's ObjectMapper 从数据库中来回映射对象。我仅将 JPA 注释用于元数据(如列名等)。 如果人们有兴趣,我会将其清理并放在 github 上(目前它仅在我的初创公司的私人仓库中)。

    这是一个粗略的想法,这里是一个示例 bean(注意所有字段都是最终的):

    //skip imports for brevity
    public class TestBean {
    
        @Id
        private final String stringProp;
        private final long longProp;
        @Column(name="timets")
        private final Calendar timeTS;
    
        @JsonCreator
        public TestBean(
                @JsonProperty("stringProp") String stringProp, 
                @JsonProperty("longProp") long longProp,
                @JsonProperty("timeTS") Calendar timeTS ) {
            super();
            this.stringProp = stringProp;
            this.longProp = longProp;
            this.timeTS = timeTS;
        }
    
        public String getStringProp() {
            return stringProp;
        }
        public long getLongProp() {
            return longProp;
        }
    
        public Calendar getTimeTS() {
            return timeTS;
        }
    
    }
    

    RowMapper 的样子如下(注意它主要委托给 Springs ColumnMapRowMapper,然后使用 Jackson 的 objectmapper):

    public class SqlObjectRowMapper<T> implements RowMapper<T> {
    
        private final SqlObjectDefinition<T> definition;
        private final ColumnMapRowMapper mapRowMapper;
        private final ObjectMapper objectMapper;
    
    
        public SqlObjectRowMapper(SqlObjectDefinition<T> definition, ObjectMapper objectMapper) {
            super();
            this.definition = definition;
            this.mapRowMapper = new SqlObjectMapRowMapper(definition);
            this.objectMapper = objectMapper;
        }
    
        public SqlObjectRowMapper(Class<T> k) {
            this(SqlObjectDefinition.fromClass(k), new ObjectMapper());
        }
    
    
        @Override
        public T mapRow(ResultSet rs, int rowNum) throws SQLException {
            Map<String, Object> m = mapRowMapper.mapRow(rs, rowNum);
            return objectMapper.convertValue(m, definition.getObjectType());
        }
    
    }
    

    现在我刚刚使用 Spring JDBCTemplate 并给它一个 fluent 包装器。以下是一些示例:

    @Before
    public void setUp() throws Exception {
        dao = new SqlObjectDao<TestBean>(new JdbcTemplate(ds), TestBean.class);
    
    }
    
    @Test
    public void testAll() throws Exception {
        TestBean t = new TestBean(IdUtils.generateRandomUUIDString(), 2L, Calendar.getInstance());
        dao.insert(t);
        List<TestBean> list = dao.queryForListByFilter("stringProp", "hello");
        List<TestBean> otherList = dao.select().where("stringProp", "hello").forList();
        assertEquals(list, otherList);
        long count = dao.select().forCount();
        assertTrue(count > 0);
    
        TestBean newT = new TestBean(t.getStringProp(), 50, Calendar.getInstance());
        dao.update(newT);
        TestBean reloaded = dao.reload(newT);
        assertTrue(reloaded != newT);
        assertTrue(reloaded.getStringProp().equals(newT.getStringProp()));
        assertNotNull(list);
    
    }
    
    @Test
    public void testAdding() throws Exception {
        //This will do a UPDATE test_bean SET longProp = longProp + 100
        int i = dao.update().add("longProp", 100).update();
        assertTrue(i > 0);
    
    }
    
    @Test
    public void testRowMapper() throws Exception {
        List<Crap> craps = dao.query("select string_prop as name from test_bean limit ?", Crap.class, 2);
        System.out.println(craps.get(0).getName());
    
        craps = dao.query("select string_prop as name from test_bean limit ?")
                    .with(2)
                    .forList(Crap.class);
    
        Crap c = dao.query("select string_prop as name from test_bean limit ?")
                    .with(1)
                    .forObject(Crap.class);
    
        Optional<Crap> absent 
            = dao.query("select string_prop as name from test_bean where string_prop = ? limit ?")
                .with("never")
                .with(1)
                .forOptional(Crap.class);
    
        assertTrue(! absent.isPresent());
    
    }
    
    public static class Crap {
    
        private final String name;
    
        @JsonCreator
        public Crap(@JsonProperty ("name") String name) {
            super();
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
    }
    

    请注意,将任何查询映射到不可变 POJO 是多么容易。也就是说,您不需要将实体一对一地放到表格中。还要注意Guava's optionals 的使用(最后一个查询.. 向下滚动)。我真的很讨厌 ORM 如何抛出异常或返回 null

    如果你喜欢它,请告诉我,我会花时间把它放在 github 上(仅使用 postgresql 进行测试)。否则,通过上面的信息,您可以使用 Spring JDBC 轻松实现自己的。我开始真正深入研究它,因为不可变对象更容易理解和思考。

    【讨论】:

    • 我有兴趣查看源代码。随意把它放在 GitHub 上!好奇地使用Optional。我不知道为什么这比使用null 更好?
    • 非常好。我得尽快仔细看看!
    • 我刚刚尝试使用您的项目,它似乎非常过时,或者至少它依赖于过时的工件。如果您能更新它,那就太好了。
    【解决方案3】:

    您可以使用 Ebean 和 OpenJPA 做到这一点(我认为您可以使用 Hibernate 做到这一点,但不确定)。 ORM (Ebean/OpenJPA) 将生成一个默认构造函数(假设 bean 没有)并实际设置“最终”字段的值。这听起来有点奇怪,但最终字段并不总是严格意义上的最终字段。

    【讨论】:

      【解决方案4】:

      虽然不是真正的 ORM,但MyBatis 或许能够做到这一点。不过我没试过。

      http://mybatis.org/java.html

      【讨论】:

      • 是的,MyBatis 很好地支持不可变类:你甚至可以将你的字段设置为 final。请参阅构造函数部分以映射它们:mybatis.org/core/sqlmap-xml.html
      【解决方案5】:

      AFAIK,.NET 没有完全按照您的意愿支持此功能的 ORM。但是您可以看看 BLTookit 和 LINQ to SQL - 两者都提供按比较更新的语义,并且总是在具体化时返回新对象。这几乎就是您所需要的,但我不确定那里的收藏。

      顺便说一句,你为什么需要这个功能?我知道纯函数式语言和纯不可变对象的好处(例如完全线程安全)。但如果使用 ORM,您对此类对象所做的所有事情最终都会转换为一系列 SQL 命令。所以我承认在这里使用这些对象的好处是无用的。

      【讨论】:

      • 查看编辑。问题不在于持久性部分,而在于构建在域对象上的层。
      【解决方案6】:

      Hibernate 有 @Immutable 注释。

      还有here is a guide

      【讨论】:

      • 据我所知 Hibernate 不支持最终字段和值的“构造函数注入”。
      • 你需要final吗?您可以通过缺少设置器来限制它。
      • 否则不会是不可变的。我不喜欢框架限制我对语言功能的选择。
      • 如果它不是最终的并且 ORM 没有被强制使用构造函数来初始化实例,那么事情就会变得更加复杂(stackoverflow.com/questions/1624392/…)。
      • 你错了NH:它支持只读实体语义,这与不可变对象的常见定义不同。想想收藏。
      猜你喜欢
      • 1970-01-01
      • 2014-07-23
      • 2011-04-30
      • 1970-01-01
      • 1970-01-01
      • 2020-05-08
      • 1970-01-01
      • 2010-11-15
      • 1970-01-01
      相关资源
      最近更新 更多