Hibernate基础教程(6-1)
来源:互联网 发布:99乘法表的数c语言代码 编辑:程序博客网 时间:2024/04/29 03:38
第6章 用注解进行映射
映射可以有两种形式:单独的XML文件或者POJO源代码中嵌入的Java 5注解。在本章中,我们要讨论注解的使用方法;在下一章中,将讨论XML文件的使用方法
6.1 Java 5特性
Java 5在2004年末推出,成为Java语言的新版本。在此之前的Java版本不支持注解,所以尽管核心Hibernate3与以前的Java版本兼容,但是除非你的开发、编译和运行时工具至少支持Java 5(Java 6的代码是Mustang,已经于2006年发布了),否则无法使用本章中描述的特性。
因为假设你拥有Java 5环境,所以本章中的示例还将使用Java 5中引入的其他一些增强的语言特性,包括:
v 泛型(generic)。
v 增强的for循环
v 静态导入
v 枚举
v 自动装箱(autoboxing)
v 可变的参数列表
使用这些特性会使本章的源代码更加紧凑。同样,基于注解的映射也比等效的XML代码简洁得多。
6.2 用注解创建Hibernate映射
6.2.1 注解的缺点
使用注解就会将代码限制在Java 5环境中。这个限制会让一些开发人员根本无法使用注解,因为一些应用服务器还不支持这个版本的JVM。即使在使用最新的JVM方面不存在技术障碍,还有许多团队非常保守,不愿意贸然使用新的技术。
如果你已经有了支持代码的XML映射文件。如果没有明显的好处,你不会愿意用注解改写这些映射。
如果改写现有的POJO源代码,可能在原来没有问题的代码中引入bug。
如果在外部XML文件中维护映射信息,最大的好处在于可以直接修改映射信息来反映业务或数据库模式的变化,不需要重新编译整个应用程序。
与对XML映射文件的支持相比,Hibernate 3对基于注解的支持还不够成熟。
6.2.2 注解的优点
第一个优点(这可能是最大的优点)是,与XML映射相比,基于注解的映射要直观得多,因为它们直接放在源代码中,与相关联的属性放在一起。
注解比等效的XML映射要简洁得多。
Hibernate使用并支持EJB 3持久化注解。
6.2.3 选用哪种方法
在创建Hibernate应用程序时,如果对数据库有完全的控制能力,而且这是一个新项目,那么我们一般会建议使用注解。
如果你打算让应用程序能够移植到其他兼容EJB 3的ORM应用程序,那么必须使用注解重写映射信息。
如果要将现有的应用程序移植到Hibernate,或者要创建的新项目依赖于其他应用程序拥有的数据库,那么应该使用XML映射,XML映射的灵活性使你的项目不会受到数据库模式变化的严重影响。
6.2.4 在应用程序中使用注解
需要安装Hibernate 3注解工具集(jar包),JDK5.0。
提示 如果希望在源代码中声明映射,但是无法使用Java 5环境,那么可以通过Xdoclet工具使用javadoc风格的注释实现相似的效果。从http://xdoclet.sourceforge.net可获得Xdoclet。
应用程序需要注解工具集中提供的hibernate-annotations.jar和ejb3-persistence.jar文件。
如果使用hibernate.cfg.xml文件建立映射配置,那么需要<mapping>元素提供被注解的类的完全限定名:
<mapping class=”com.hibernatebook.annotations.Book” />
在配置SessionFactory时,需要使用AnnotationConfiguration对象,而不是在使用XML映射时使用Configuration对象,如下所示:
AnnotationConfiguration config=new AnnotationConfiguration();
config.addAnnotatedClass(Book.class);
SessionFactory factory=config.configure().buildSessionFactory();
如果需要在EJB 3容器中使用加注解的实体,那么必须使用标准的EntityManager,而不是Hibernate特有的Session。Hibernate提供了一个可以单独下载的EntityManager实现。附录A中可以找到使用Hibernate EntityManager的详细说明。
6.2.5 EJB 3持久化注解
在使用注解进行开发时,首先要编写一个Java类,然后在源代码中加上元数据注解。在J2SE 5.0中,JRE(Java Runtime Environment)会解析这些注解。Hibernate通过Java反射读取注解并且应用映射信息。如果希望使用Hibernate工具生成数据库模式,那么必须先编译包含注解的实体类。
表6-1列出了EJB 3 API中的所有持久化注解。在本节中,我们将结合一些简单的类来介绍比较重要的持久化注解,说明应用它们的方法。
表6-1 EJB 3注解
属性名
目 标
用 途
AttributeOverride/AttributeOverrides
T、M和F
覆盖嵌入的(组件)实体的默认列细节
Basic
M和F
覆盖基本字段和属性的默认读取策略和可空性
Column
M和F
将类的一个字段或属性与表中的一个列关联起来
ColumnResult
Pm
用作@SqlResultSetMapping注解的参数;允许在传统的JDBC ResultSet中以列的形式返回实体的字段
DiscriminatorColumn
T
在单一表或联结表继承策略中,覆盖识别符(discriminator)列的默认行为
DiscriminatorValue
T
在实体继承层次结构的根上,决定与识别符列中的实体相关联的值
Embeddable
T
表示一个实体是可嵌入的(组件)实体
Embedded
M和F
表示一个字段或属性由嵌入的(组件)实体组成
EmbeddedId
M和F
表示一个主键字段由嵌入的(组件)实体组成。这与@Id注解相互排斥
Entity
T
标识一个实体并允许覆盖属性(比如它的名称)的默认值
EntityListeners
T
在相关实体的生命周期中,允许调用适当的javax.persistence.EntityListener类
EntityResult
Pm
用作@SqlResultSetMapping注解的参数;允许在传统的JDBC ResultSet中以列的形式返回实体的字段
Enumerated
M和F
将一个字段或属性定义为enumerated类型
ExcludeDefaultListeners
T
在相关实体的生命周期中,不允许调用默认的EntityListener
ExcludeSuperclassListeners
T
在相关实体的生命周期中,不允许调用超类的EntityListener
FieldResult
Pm
用作@SqlResultSetMapping注解的参数;允许在传统的JDBC ResultSet中以列的形式返回实体的字段
GeneratedValue
M和F
允许为相关实体的主键值指定生成策略
Id
M和F
标识实体的主键。@Id属性的位置还决定实体类的默认访问模式是字段访问,还是属性访问
IdClass
T
表示一个实体的主键由与另一个实体的字段对应的列组成。组成主键的字段将标上@Id属性
Inheritance
T
表示一个实体是实体继承层次结构的根(也就是类继承层次结构中最高的持久类)
JoinColumn/JoinColumns
T、M和F
表示列用作另一个表的外键
JoinTable
M和F
可以在一对多或多对多关系中指定链接表的细节
Lob
M和F
表示一个字段或属性存储为大对象数据类型——通常是二进制大对象(BLOB)。可以使用这个属性取消对字符串和二进制数据的长度限制,但是通常的作用是减少查询数据的范围
ManyToMany
M和F
可以在实体之间定义多对多关联
ManyToOne
M和F
可以在实体之间定义多对一关联
MapKey
M和F
在用Map对象建立关联时,可以指定键
MappedSuperclass
T
允许用一个非持久化类为它的派生类提供基本映射信息
NamedNativeQuery/NamedNativeQueryies
Pk和T
允许在注解中存储命名的EJB QL查询
OneToMany
M和F
可以在实体之间定义一对多关联
OneToOne
M和F
可以在实体之间定义一对一关联
OrderBy
M和F
允许定义集合的次序
PersistenceContext/PersistenceContexts
T、M和F
与@EntityManager一起使用;指出一个字段或属性表示由容器注入的EntityManager
PersistenceUnit/PersistenceUnits
T、M和F
与@EntityManager一起使用;指出一个字段或属性表示由容器注入的EntityManagerFactory
PostLoad
M
表示一个方法将在实体上执行加载操作之后调用
PostPersist
M
表示一个方法将在实体上执行持久化操作之后调用
PostRemove
M
表示一个方法将在实体上执行删除操作之后调用
PostUpdate
M
表示一个方法将在实体上执行更新操作之后调用
PrePersist
M
表示一个方法将在实体上执行持久化操作之前调用
PreRemove
M
表示一个方法将在实体上执行删除操作之前调用
PreUpdate
M
表示一个方法将在实体上执行更新操作之前调用
PrimaryKeyJoinColumn/PrimaryKeyJoinColumns
T、M和F
可以指定联结辅助表和主表的列
QueryHint
Pm
可以提供与实现相关的“提示(hint)”,作为命名的查询和命名的原生查询的参数
SecondaryTable/SecondaryTables
T
可以将一个实体的基本字段和属性持久化到多个表中
SequenceGenerator
Pk、T、M和F
可以指定供一个或多个实体使用的命名的主键生成器
SqlResultSetMapping
Pk和T
可以像对待命名的原生查询一样对实体进行映射(这样就可以以传统JDBC ResultSet的形式获取这个实体)
Table
Pk、T、M和F
可以覆盖实体的主表的默认细节
TableGenerator
Pk、T、M和F
当在主键字段或属性(标有@Id注解的字段或属性)上使用生成值注解的表生成策略时,覆盖用来生成主键的表默认属性
Temporal
M和F
指定Date和Calendar字段或属性的行为(如果省略这个属性,这样的字段就表示为TIMESTAMP值)
Transient
M和F
让一个字段或属性不被持久化
UniqueConstraint
Pm
作为@Table的参数,在生成数据库模式时实施唯一约束
Version
M和F
表示字段或属性作为实体的乐观锁值
注:“目标”栏中的符号:Pk=包,T=类型,M=方法,F=字段,Pm=参数
下面示例中使用的类表示一家出版商的图书目录。首先开发一个Book类,其中不包含注解和映射信息。为了说明注解的作用,我们不使用现有的数据库模式,所以需要定义自己的关系数据库模式。
最初的Book类非常简单。它有两个字段,title和pages和一个标识符id。id是一个整数,title是一个String对象,pages也是一个整数。随着示例的发展,我们将在Book类中添加注解、字段和方法。Book和Author类的完整源代码清单在本章末尾给出,其他源文件可以在Apress网站(www.apress.com)上本章的源代码下载文件中找到。
代码清单6-3是不带注解的Book类的源代码,这是这个示例的起点。
代码清单6-3 不带注解的Book类
package com.speakermore.beginhb.ch06;
public class Book {
private String title;
private int pages;
private int id;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getPages() {
return pages;
}
public void setPages(int pages) {
this.pages = pages;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
可以看到,这是一个POJO。我们将给这个类加上注解,同时解释注解背后的概念。
6.2.6 用@Entity标出实体Bean
第一步是将Book类村为EJB 3实体bean。在传统的EJB中,我们会添加一个EJB标志性接口,从而表示这个类是实体bean。但是现在应该在Book类中添加@Entity注解,如下所示:
package com.speakermore.beginhb.ch06;
import javax.persistence.*;
@Entity
public class Book {
EJB 3标准注解都在javax.persistence包中(默然说话:使用注解需要导入JPA支持包ejb3-persistence.jar。可以上网下载一个,复制到lib目录下)。
@Entity注解将这个类标为实体bean,所以它必须有一个无参构造器,这个构造器至少在受保护(protected)范围内可见。Hibernate支持包范围作为最小范围,但是如果使用包范围,就不能移植到其他EJB 3容器中。EJB 3对实体bean类的其他要求是,类必须不是最终的(final),而且实体bean类必须是具体类。EJB 3实体Bean类和Hibernate 3持久对象的许多条件是相同的——这是因为Hibernate团队积极参与了EJB 3的设计过程,而且设计无干扰的对象-关系持久化解决方案的方式就这么几种。
可以看到,尽管我们必须添加import语句和注解,但是不必修改其余的代码。POJO本质上没有改动。
6.2.7 用@Id和@GeneratedValue标出主键
每个实体bean都必须有一个主键,可以在类中用@Id注解标出主键。主键通常是单一字段,但也可以是多个字段的组合。
@Id注解的位置决定Hibernate对这个映射使用的默认访问策略。如果这个注解应用于字段(如代码清单6-4所示),那么将使用字段访问方式。
代码清单6-4 使用字段访问的类
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Simple implements Serializable {
@Id
int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
如果这个注解应用于字段的getter方法,那么将使用属性访问方式
代码清单6-5 使用属性访问的类
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Simple implements Serializable {
private int id;
@Id
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
在这里可以看出注解方式的一个优点,因为注解嵌入在源代码中,所以可以根据映射在代码中的上下文确定一些信息,这样就能够推断出许多映射决策,而不需要显式地声明,这有助于进一步简化注解。
在默认情况下,@Id会自动决定最合适的主键生成策略;也可以通过应用@GeneratedValue注解覆盖这个设置。这需要两个属性:strategy和generator。strategy属性必须是javax.persistence. GenerationType(默然说话:书上又印错了啦)枚举中的值。如果没有指定生成器类型,那么默认值是AUTO。在GenerationType中有4个主键生成器类型,如下所示:
v AUTO:Hibernate根据数据库对主键生成的支持,决定使用的生成器类型。
v IDENTITY:数据库负责决定和分配下一个主键。
v SEQUENCE:一些数据库支持SEQUENCE列类型。参见6.2.8节。
v TABLE:这个类型用一个单独的表维护主键值。参见6.2.9节。
注意,strategy属性可用的值并不与Hibernate XML映射可用的主键生成器值完全匹配。如果需要使用Hibernate特有的主键生成策略,那么可以使用本章末尾介绍的一些Hibernate扩展——但是,如果使用Hibernate特有的特性,应用程序就无法移植到其他EJB 3环境。
对于Book类,我们使用默认的键生成策略。让Hibernate决定使用的生成器类型,这有助于代码在不同的数据库之间移植。因为我们希望Hibernate对POJO进行属性访问,所以必须把注解应用于标识符的getter,而不是它访问的字段:
@Id
@GeneratedValue
public int getId() {
return id;
}
6.2.8 用@SequenceGenerator生成主键值
序列(sequence)是一个数据库对象,可以用作主键值的来源。它与使用标识(identity)列类型相似,但是序列独立于任何表,因此可以供多个表使用。
要想声明特定的序列对象及其属性,必须在字段上包含一个@SequenceGenerator注解。下面是一个示例:
@Id
@SequenceGenerator (name=”seq1”,sequenceName=”HIB_SEQ”)
@GeneratedValue(strategy= GenerationType.SEQUENCE,generator=”seq1”)//默然说话:这里书上又是错误的
public int getId() {
return id;
}
这里声明了一个名为seq1的序列生成注解。它引用称为HIB_SEQ的数据库序列对象。然后,@GeneratedValue注解的generator属性引用seq1
只有序列生成器名称是必需的,其他属性有合理的默认值,但是为sequenceName属性提供明确的值是一种好做法。如果没有指定,那么由持久化提供者(即Hibernate 3或EJB 3)选择使用的sequenceName值。其他(可选)属性是initialValue和allocationSize,它们的默认值分别是1和50。
6.2.9 用@TableGenerator生成主键值
@TableGenerator注解的使用方式与@SequenceGenerator注解非常相似,但是因为@TableGenerator用一个标准的数据库表来获得主键值,而不是使用数据库厂商特有的序列对象,所以可以保证数据库平台之间的可移植性。
说明 要想获得最好的可移植性和性能,就不应指定表生成器,而是应使用@GeneratorValue(strategy=GenerationType.AUTO)配置,也就是让持久化提供者选择最适合所用数据库的策略。
与序列生成器一样,@TableGenerator的name属性是必需的,其他属性是可选的,表的细节由持久化提供者选择。
@Id
@TableGenerator (name=”tablegen”,table=”ID_TABLE”,pkColumnName=”ID”,valueColumnName=”NEXT_ID”)
@GeneratedValue(strategy= GenerationType.TABLE,generator=”tablegen”)
public int getId() {
return id;
}
@TableGenerator的可选属性如下:
v allocationSize:允许指定主键值的递增量。
v catalog:允许指定表所在的编目。
v initialValue:允许指定主键的初始值。
v pkColumnName:允许指出表的主键列。这个表可以包含为多个实体生成主键值所需的细节。
v pkColumnValue:允许指出包含主键生成信息的行的主键值。
v schema:允许指定表所在的模式。
v table:包含主键值的表的名称
v uniqueConstraints:允许在生成模式时对这个表应用其他约束。
v valueColumnName:允许指出包含当前实体的主键生成信息的列。
因为可以使这个表包含多个实体的主键值,所以可以对使用它的每个实体包含一行。因此,它需要自己的主键(pkColumnName),还有一列中包含要使用的下一个主键值(pkColumnValue),所以任何实体都可以从这个表中获得自己的主键值。
6.2.10 用@Id,@IdClass和@EmbeddedId组合主键
尽管由于各种原因,使用单列的代理键是有好处的,但是有时候必须使用业务键。如果主键是单列的,那么可以只使用@Id,而不必指定生成策略(这要求用户分配主键值,这样实体才能持久化)。但是,如果主键由多个列组成,那么就需要用另一种方式将这些列组合起来,让持久化引擎能够将键值作为单一对象进行操作。
必须创建一个类来表示这个主键。当然,这个类不需要自己的主键,但是它必须是一个公共类,必须有默认的构造器,必须是可序列化的,必须实现hashCode()和equals()方法,从而让Hibernate代码能够探测主键冲突(也就是说,必须用适当的数据库主键值语义实现代码)。
创建了这个主键类之后,有3种使用它的方式。
v 给主键类标上@Embeddable,在实体类中添加一个引用它的普通属性,并且加上@Id注解。
v 在实体类中添加一个引用主键类的普通属性,并且加上@Embeddable注解。
v 在实体类中添加引用主键类的所有字段的属性,给它们加上@Id注解,给实体类加上@IdClass注解并指定主键类的类名。
所有这些技术都需要使用id类,因为在调用持久化API的各部分时,必须向Hibernate提供一个主键对象。
使用@id和标有@Embeddable注解的类是最自然的方式,如代码清单6-6所示。@Embeddable可以用于非主键可嵌入值(本章后面将详细讨论@Embeddable)。它允许将组合主键当作单一属性对待,还允许在其他表中重用@Embeddable类。
代码清单6-6 使用@Id和@Embeddable注解映射组合主键
package com.speakermore.beginhb.ch06;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Account implements Serializable {
private String description;
private AccountPk id;
public Account() {
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Id
public AccountPk getId() {
return id;
}
public void setId(AccountPk id) {
this.id = id;
}
}
package com.speakermore.beginhb.ch06;
import javax.persistence.Embeddable;
@Embeddable
public class AccountPk {
private String code;
private Integer number;
public AccountPk() {
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Integer getNumber() {
return number;
}
public void setNumber(Integer number) {
this.number = number;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof AccountPk)) {
return false;
}
AccountPk other = (AccountPk) obj;
return ((this.code == null) ? (other.code == null) : this.code.equals(other.code)) && ((this.number == null) ? other.number == null : this.number.equals(other.number));
}
@Override
public int hashCode() {
int hashCode=0;
if(code!=null)
hashCode^=code.hashCode();
if(number!=null)
hashCode^=number.hashCode();
return hashCode;
}
}
另一种比较自然的方式是使用@EmbeddedId注解。在这种情况下,主键类不能在其他表中使用,因为它不是@Embeddable实体,但是它允许将这个键当作Account类的单一属性对待。注意,在代码清单6-7和代码清单6-8中,AccountPk类没有标上@Embeddable。
代码清单6-7 使用@EmbeddedId注解映射组合主键
package com.speakermore.beginhb.ch06;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Account implements Serializable {
private String description;
private AccountPk id;
public Account() {
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@EmbeddedId
public AccountPk getId() {
return id;
}
public void setId(AccountPk id) {
this.id = id;
}
}
package com.speakermore.beginhb.ch06;
import javax.persistence.Embeddable;
public class AccountPk {
private String code;
private Integer number;
public AccountPk() {
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Integer getNumber() {
return number;
}
public void setNumber(Integer number) {
this.number = number;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof AccountPk)) {
return false;
}
AccountPk other = (AccountPk) obj;
return ((this.code == null) ? (other.code == null) : this.code.equals(other.code)) && ((this.number == null) ? other.number == null : this.number.equals(other.number));
}
@Override
public int hashCode() {
int hashCode=0;
if(code!=null)
hashCode^=code.hashCode();
if(number!=null)
hashCode^=number.hashCode();
return hashCode;
}
}
最后,通过使用@IdClass和@Id注解,可以使用实体本身的属性映射组合主键类,这些属性与主键类中的属性名对应。属性名必须是对应的(没有覆盖属性名的机制),而且主键类必须承担与其他两种技术相同的责任。这种方式的唯一优点是,在实体的接口中“隐藏”了主键类。@IdClass注解有一个Class类型的值参数,这个参数必须是作为组合主键的类。与主键类的属性对应的字段都必须标上@Id注解——注意,在代码清单6-8中,Account类的getCode()和getNumber()方法标上了@Id注解,AccountPk类没有标上@Embeddable,但是@IdClass注解的参数指定了这个类。
package com.speakermore.beginhb.ch06;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
@IdClass(AccountPk.class)
public class Account implements Serializable {
private String description;
private String code;
private Integer number;
public Account() {
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public void setCode(String code){
this.code=code;
}
@Id
public String getCode(){
return this.code;
}
public void setNumber(Integer number){
this.number=number;
}
@Id
public Integer getNumber(){
return this.number;
}
}
package com.speakermore.beginhb.ch06;
import javax.persistence.Embeddable;
public class AccountPk {
private String code;
private Integer number;
public AccountPk() {
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Integer getNumber() {
return number;
}
public void setNumber(Integer number) {
this.number = number;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof AccountPk)) {
return false;
}
AccountPk other = (AccountPk) obj;
return ((this.code == null) ? (other.code == null) : this.code.equals(other.code)) && ((this.number == null) ? other.number == null : this.number.equals(other.number));
}
@Override
public int hashCode() {
int hashCode=0;
if(code!=null)
hashCode^=code.hashCode();
if(number!=null)
hashCode^=number.hashCode();
return hashCode;
}
}
代码清单6-9给出了前面代码的DDL。
代码清单6-9 从带注解的Account类生成的SQL语句
create table Account(
code varchar(255) not null,
number integer not null,
description varchar(255),
primary key(code,number)
);
- Hibernate基础教程(6-1)
- Hibernate基础教程(6-2)
- Hibernate基础教程(1)
- Hibernate基础教程(3-1)
- Hibernate基础教程1
- Hibernate基础教程
- Hibernate基础教程读书笔记(1)Hibernate3简介
- Hibernate基础教程读书笔记(2)
- Hibernate基础教程读书笔记(4)
- Hibernate基础教程读书笔记(5)
- Hibernate基础教程(2)
- Hibernate基础教程(3-2)
- Hibernate基础教程(4)
- Hibernate基础教程(5)
- hibernate基础教程2
- Hibernate基础教程读书笔记(3-2)
- 初学者想学Hibernate,初级基础教程
- Hibernate基础教程读书笔记(3-3)
- 指针和函数
- 实习?如何选择
- 程序员生涯从这里开始
- ACM内部函数4
- vivi解析大全
- Hibernate基础教程(6-1)
- Slackware for ARM
- 写程序就得知道调试
- vc6.0调试工具介绍
- C语言读取CSV文件的方法1
- [转]我的FPGA学习历程
- 初识io流条件状态
- 开心
- 刚刚加入CSDN